diff options
Diffstat (limited to 'Emby.Server.Implementations')
199 files changed, 3075 insertions, 2680 deletions
diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs index 26b4649dd..a4deeddb7 100644 --- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs +++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -34,14 +32,9 @@ namespace Emby.Server.Implementations.AppBase private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>(); /// <summary> - /// The _configuration loaded. - /// </summary> - private bool _configurationLoaded; - - /// <summary> /// The _configuration. /// </summary> - private BaseApplicationConfiguration _configuration; + private BaseApplicationConfiguration? _configuration; /// <summary> /// Initializes a new instance of the <see cref="BaseConfigurationManager" /> class. @@ -63,17 +56,17 @@ namespace Emby.Server.Implementations.AppBase /// <summary> /// Occurs when [configuration updated]. /// </summary> - public event EventHandler<EventArgs> ConfigurationUpdated; + public event EventHandler<EventArgs>? ConfigurationUpdated; /// <summary> /// Occurs when [configuration updating]. /// </summary> - public event EventHandler<ConfigurationUpdateEventArgs> NamedConfigurationUpdating; + public event EventHandler<ConfigurationUpdateEventArgs>? NamedConfigurationUpdating; /// <summary> /// Occurs when [named configuration updated]. /// </summary> - public event EventHandler<ConfigurationUpdateEventArgs> NamedConfigurationUpdated; + public event EventHandler<ConfigurationUpdateEventArgs>? NamedConfigurationUpdated; /// <summary> /// Gets the type of the configuration. @@ -107,31 +100,25 @@ namespace Emby.Server.Implementations.AppBase { get { - if (_configurationLoaded) + if (_configuration is not null) { return _configuration; } lock (_configurationSyncLock) { - if (_configurationLoaded) + if (_configuration is not null) { return _configuration; } - _configuration = (BaseApplicationConfiguration)ConfigurationHelper.GetXmlConfiguration(ConfigurationType, CommonApplicationPaths.SystemConfigurationFilePath, XmlSerializer); - - _configurationLoaded = true; - - return _configuration; + return _configuration = (BaseApplicationConfiguration)ConfigurationHelper.GetXmlConfiguration(ConfigurationType, CommonApplicationPaths.SystemConfigurationFilePath, XmlSerializer); } } protected set { _configuration = value; - - _configurationLoaded = value != null; } } @@ -144,7 +131,7 @@ namespace Emby.Server.Implementations.AppBase { IConfigurationFactory factory = Activator.CreateInstance<T>(); - if (_configurationFactories == null) + if (_configurationFactories is null) { _configurationFactories = new[] { factory }; } @@ -183,7 +170,7 @@ namespace Emby.Server.Implementations.AppBase Logger.LogInformation("Saving system configuration"); var path = CommonApplicationPaths.SystemConfigurationFilePath; - Directory.CreateDirectory(Path.GetDirectoryName(path)); + Directory.CreateDirectory(Path.GetDirectoryName(path) ?? throw new InvalidOperationException("Path can't be a root directory.")); lock (_configurationSyncLock) { @@ -306,7 +293,7 @@ namespace Emby.Server.Implementations.AppBase configurationManager._configurationStores, i => string.Equals(i.Key, k, StringComparison.OrdinalIgnoreCase)); - if (configurationInfo == null) + if (configurationInfo is null) { throw new ResourceNotFoundException("Configuration with key " + k + " not found."); } @@ -323,25 +310,20 @@ namespace Emby.Server.Implementations.AppBase private object LoadConfiguration(string path, Type configurationType) { - if (!File.Exists(path)) - { - return Activator.CreateInstance(configurationType); - } - try { - return XmlSerializer.DeserializeFromFile(configurationType, path); - } - catch (IOException) - { - return Activator.CreateInstance(configurationType); + if (File.Exists(path)) + { + return XmlSerializer.DeserializeFromFile(configurationType, path); + } } - catch (Exception ex) + catch (Exception ex) when (ex is not IOException) { Logger.LogError(ex, "Error loading configuration file: {Path}", path); - - return Activator.CreateInstance(configurationType); } + + return Activator.CreateInstance(configurationType) + ?? throw new InvalidOperationException("Configuration type can't be Nullable<T>."); } /// <inheritdoc /> @@ -367,7 +349,7 @@ namespace Emby.Server.Implementations.AppBase _configurations.AddOrUpdate(key, configuration, (_, _) => configuration); var path = GetConfigurationFile(key); - Directory.CreateDirectory(Path.GetDirectoryName(path)); + Directory.CreateDirectory(Path.GetDirectoryName(path) ?? throw new InvalidOperationException("Path can't be a root directory.")); lock (_configurationSyncLock) { diff --git a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs index f923e59ef..1c8477605 100644 --- a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs +++ b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs @@ -43,7 +43,7 @@ namespace Emby.Server.Implementations.AppBase Span<byte> newBytes = stream.GetBuffer().AsSpan(0, (int)stream.Length); // If the file didn't exist before, or if something has changed, re-save - if (buffer == null || !newBytes.SequenceEqual(buffer)) + if (buffer is null || !newBytes.SequenceEqual(buffer)) { var directory = Path.GetDirectoryName(path) ?? throw new ArgumentException($"Provided path ({path}) is not valid.", nameof(path)); diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 909972469..7969577bc 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -11,16 +11,13 @@ using System.IO; using System.Linq; using System.Net; using System.Reflection; -using System.Runtime.InteropServices; using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; using Emby.Dlna; using Emby.Dlna.Main; using Emby.Dlna.Ssdp; -using Emby.Drawing; using Emby.Naming.Common; -using Emby.Notifications; using Emby.Photos; using Emby.Server.Implementations.Channels; using Emby.Server.Implementations.Collections; @@ -45,9 +42,11 @@ using Emby.Server.Implementations.SyncPlay; using Emby.Server.Implementations.TV; using Emby.Server.Implementations.Updates; using Jellyfin.Api.Helpers; +using Jellyfin.Drawing; using Jellyfin.MediaEncoding.Hls.Playlist; using Jellyfin.Networking.Configuration; using Jellyfin.Networking.Manager; +using Jellyfin.Server.Implementations; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Events; @@ -69,7 +68,6 @@ using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Lyrics; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Notifications; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Plugins; @@ -101,6 +99,7 @@ using MediaBrowser.Providers.Subtitles; using MediaBrowser.XbmcMetadata.Providers; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -116,14 +115,10 @@ namespace Emby.Server.Implementations public abstract class ApplicationHost : IServerApplicationHost, IAsyncDisposable, IDisposable { /// <summary> - /// The environment variable prefixes to log at server startup. - /// </summary> - private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" }; - - /// <summary> /// The disposable parts. /// </summary> private readonly ConcurrentDictionary<IDisposable, byte> _disposableParts = new(); + private readonly DeviceId _deviceId; private readonly IFileSystem _fileSystemManager; private readonly IConfiguration _startupConfig; @@ -132,7 +127,6 @@ namespace Emby.Server.Implementations private readonly IPluginManager _pluginManager; private List<Type> _creatingInstances; - private IMediaEncoder _mediaEncoder; private ISessionManager _sessionManager; /// <summary> @@ -141,8 +135,6 @@ namespace Emby.Server.Implementations /// <value>All concrete types.</value> private Type[] _allConcreteTypes; - private DeviceId _deviceId; - private bool _disposed = false; /// <summary> @@ -166,6 +158,7 @@ namespace Emby.Server.Implementations Logger = LoggerFactory.CreateLogger<ApplicationHost>(); _fileSystemManager.AddShortcutHandler(new MbLinkShortcutHandler(_fileSystemManager)); + _deviceId = new DeviceId(ApplicationPaths, LoggerFactory); ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version; ApplicationVersionString = ApplicationVersion.ToString(3); @@ -191,30 +184,11 @@ namespace Emby.Server.Implementations /// </summary> private string PublishedServerUrl => _startupConfig[AddressOverrideKey]; - /// <summary> - /// Gets a value indicating whether this instance can self restart. - /// </summary> - public bool CanSelfRestart => _startupOptions.RestartPath != null; - public bool CoreStartupHasCompleted { get; private set; } - public virtual bool CanLaunchWebBrowser - { - get - { - if (!Environment.UserInteractive) - { - return false; - } - - if (_startupOptions.IsService) - { - return false; - } - - return OperatingSystem.IsWindows() || OperatingSystem.IsMacOS(); - } - } + public virtual bool CanLaunchWebBrowser => Environment.UserInteractive + && !_startupOptions.IsService + && (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS()); /// <summary> /// Gets the <see cref="INetworkManager"/> singleton instance. @@ -291,15 +265,7 @@ namespace Emby.Server.Implementations /// <value>The application name.</value> public string ApplicationProductName { get; } = FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly().Location).ProductName; - public string SystemId - { - get - { - _deviceId ??= new DeviceId(ApplicationPaths, LoggerFactory); - - return _deviceId.Value; - } - } + public string SystemId => _deviceId.Value; /// <inheritdoc/> public string Name => ApplicationProductName; @@ -309,7 +275,7 @@ namespace Emby.Server.Implementations public X509Certificate2 Certificate { get; private set; } /// <inheritdoc/> - public bool ListenWithHttps => Certificate != null && ConfigurationManager.GetNetworkConfiguration().EnableHttps; + public bool ListenWithHttps => Certificate is not null && ConfigurationManager.GetNetworkConfiguration().EnableHttps; public string FriendlyName => string.IsNullOrEmpty(ConfigurationManager.Configuration.ServerName) @@ -401,7 +367,7 @@ namespace Emby.Server.Implementations // Convert to list so this isn't executed for each iteration var parts = GetExportTypes<T>() .Select(CreateInstanceSafe) - .Where(i => i != null) + .Where(i => i is not null) .Cast<T>() .ToList(); @@ -422,7 +388,7 @@ namespace Emby.Server.Implementations // Convert to list so this isn't executed for each iteration var parts = GetExportTypes<T>() .Select(i => defaultFunc(i)) - .Where(i => i != null) + .Where(i => i is not null) .Cast<T>() .ToList(); @@ -452,7 +418,7 @@ namespace Emby.Server.Implementations ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated; ConfigurationManager.NamedConfigurationUpdated += OnConfigurationUpdated; - _mediaEncoder.SetFFmpegPath(); + Resolve<IMediaEncoder>().SetFFmpegPath(); Logger.LogInformation("ServerId: {ServerId}", SystemId); @@ -622,8 +588,6 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton<IUserViewManager, UserViewManager>(); - serviceCollection.AddSingleton<INotificationManager, NotificationManager>(); - serviceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>(); serviceCollection.AddSingleton<IChapterManager, ChapterManager>(); @@ -652,50 +616,30 @@ namespace Emby.Server.Implementations /// <returns>A task representing the service initialization operation.</returns> public async Task InitializeServices() { + var jellyfinDb = await Resolve<IDbContextFactory<JellyfinDbContext>>().CreateDbContextAsync().ConfigureAwait(false); + await using (jellyfinDb.ConfigureAwait(false)) + { + if ((await jellyfinDb.Database.GetPendingMigrationsAsync().ConfigureAwait(false)).Any()) + { + Logger.LogInformation("There are pending EFCore migrations in the database. Applying... (This may take a while, do not stop Jellyfin)"); + await jellyfinDb.Database.MigrateAsync().ConfigureAwait(false); + Logger.LogInformation("EFCore migrations applied successfully"); + } + } + + ((SqliteItemRepository)Resolve<IItemRepository>()).Initialize(); + ((SqliteUserDataRepository)Resolve<IUserDataRepository>()).Initialize(); + var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>(); await localizationManager.LoadAll().ConfigureAwait(false); - _mediaEncoder = Resolve<IMediaEncoder>(); _sessionManager = Resolve<ISessionManager>(); SetStaticProperties(); - var userDataRepo = (SqliteUserDataRepository)Resolve<IUserDataRepository>(); - ((SqliteItemRepository)Resolve<IItemRepository>()).Initialize(userDataRepo, Resolve<IUserManager>()); - FindParts(); } - public static void LogEnvironmentInfo(ILogger logger, IApplicationPaths appPaths) - { - // Distinct these to prevent users from reporting problems that aren't actually problems - var commandLineArgs = Environment - .GetCommandLineArgs() - .Distinct(); - - // Get all relevant environment variables - var allEnvVars = Environment.GetEnvironmentVariables(); - var relevantEnvVars = new Dictionary<object, object>(); - foreach (var key in allEnvVars.Keys) - { - if (_relevantEnvVarPrefixes.Any(prefix => key.ToString().StartsWith(prefix, StringComparison.OrdinalIgnoreCase))) - { - relevantEnvVars.Add(key, allEnvVars[key]); - } - } - - logger.LogInformation("Environment Variables: {EnvVars}", relevantEnvVars); - logger.LogInformation("Arguments: {Args}", commandLineArgs); - logger.LogInformation("Operating system: {OS}", MediaBrowser.Common.System.OperatingSystem.Name); - logger.LogInformation("Architecture: {Architecture}", RuntimeInformation.OSArchitecture); - logger.LogInformation("64-Bit Process: {Is64Bit}", Environment.Is64BitProcess); - logger.LogInformation("User Interactive: {IsUserInteractive}", Environment.UserInteractive); - logger.LogInformation("Processor count: {ProcessorCount}", Environment.ProcessorCount); - logger.LogInformation("Program data path: {ProgramDataPath}", appPaths.ProgramDataPath); - logger.LogInformation("Web resources path: {WebPath}", appPaths.WebPath); - logger.LogInformation("Application directory: {ApplicationPath}", appPaths.ProgramSystemPath); - } - private X509Certificate2 GetCertificate(string path, string password) { if (string.IsNullOrWhiteSpace(path)) @@ -782,13 +726,7 @@ namespace Emby.Server.Implementations Resolve<ILiveTvManager>().AddParts(GetExports<ILiveTvService>(), GetExports<ITunerHost>(), GetExports<IListingsProvider>()); - Resolve<ISubtitleManager>().AddParts(GetExports<ISubtitleProvider>()); - - Resolve<IChannelManager>().AddParts(GetExports<IChannel>()); - Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>()); - - Resolve<INotificationManager>().AddParts(GetExports<INotificationService>(), GetExports<INotificationTypeFactory>()); } /// <summary> @@ -922,17 +860,13 @@ namespace Emby.Server.Implementations /// </summary> public void Restart() { - if (!CanSelfRestart) - { - throw new PlatformNotSupportedException("The server is unable to self-restart. Please restart manually."); - } - if (IsShuttingDown) { return; } IsShuttingDown = true; + _pluginManager.UnloadAssemblies(); Task.Run(async () => { @@ -991,9 +925,6 @@ namespace Emby.Server.Implementations // Local metadata yield return typeof(BoxSetXmlSaver).Assembly; - // Notifications - yield return typeof(NotificationManager).Assembly; - // Xbmc yield return typeof(ArtistNfoProvider).Assembly; @@ -1032,15 +963,11 @@ namespace Emby.Server.Implementations ItemsByNamePath = ApplicationPaths.InternalMetadataPath, InternalMetadataPath = ApplicationPaths.InternalMetadataPath, CachePath = ApplicationPaths.CachePath, - OperatingSystem = MediaBrowser.Common.System.OperatingSystem.Id.ToString(), - OperatingSystemDisplayName = MediaBrowser.Common.System.OperatingSystem.Name, - CanSelfRestart = CanSelfRestart, CanLaunchWebBrowser = CanLaunchWebBrowser, TranscodingTempPath = ConfigurationManager.GetTranscodePath(), ServerName = FriendlyName, LocalAddress = GetSmartApiUrl(request), SupportsLibraryMonitor = true, - SystemArchitecture = RuntimeInformation.OSArchitecture, PackageName = _startupOptions.PackageName }; } @@ -1052,7 +979,6 @@ namespace Emby.Server.Implementations Version = ApplicationVersionString, ProductName = ApplicationProductName, Id = SystemId, - OperatingSystem = MediaBrowser.Common.System.OperatingSystem.Id.ToString(), ServerName = FriendlyName, LocalAddress = GetSmartApiUrl(request), StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted @@ -1088,15 +1014,7 @@ namespace Emby.Server.Implementations return GetLocalApiUrl(request.Host.Host, request.Scheme, requestPort); } - // Published server ends with a / - if (!string.IsNullOrEmpty(PublishedServerUrl)) - { - // Published server ends with a '/', so we need to remove it. - return PublishedServerUrl.Trim('/'); - } - - string smart = NetManager.GetBindInterface(request, out var port); - return GetLocalApiUrl(smart.Trim('/'), request.Scheme, port); + return GetSmartApiUrl(request.HttpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback); } /// <inheritdoc/> @@ -1270,10 +1188,13 @@ namespace Emby.Server.Implementations } } - // used for closing websockets - foreach (var session in _sessionManager.Sessions) + if (_sessionManager != null) { - await session.DisposeAsync().ConfigureAwait(false); + // used for closing websockets + foreach (var session in _sessionManager.Sessions) + { + await session.DisposeAsync().ConfigureAwait(false); + } } } } diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs index 6837cce5c..961e225e9 100644 --- a/Emby.Server.Implementations/Channels/ChannelManager.cs +++ b/Emby.Server.Implementations/Channels/ChannelManager.cs @@ -66,6 +66,7 @@ namespace Emby.Server.Implementations.Channels /// <param name="userDataManager">The user data manager.</param> /// <param name="providerManager">The provider manager.</param> /// <param name="memoryCache">The memory cache.</param> + /// <param name="channels">The channels.</param> public ChannelManager( IUserManager userManager, IDtoService dtoService, @@ -75,7 +76,8 @@ namespace Emby.Server.Implementations.Channels IFileSystem fileSystem, IUserDataManager userDataManager, IProviderManager providerManager, - IMemoryCache memoryCache) + IMemoryCache memoryCache, + IEnumerable<IChannel> channels) { _userManager = userManager; _dtoService = dtoService; @@ -86,19 +88,14 @@ namespace Emby.Server.Implementations.Channels _userDataManager = userDataManager; _providerManager = providerManager; _memoryCache = memoryCache; + Channels = channels.ToArray(); } - internal IChannel[] Channels { get; private set; } + internal IChannel[] Channels { get; } private static TimeSpan CacheLength => TimeSpan.FromHours(3); /// <inheritdoc /> - public void AddParts(IEnumerable<IChannel> channels) - { - Channels = channels.ToArray(); - } - - /// <inheritdoc /> public bool EnableMediaSourceDisplay(BaseItem item) { var internalChannel = _libraryManager.GetItemById(item.ChannelId); @@ -129,7 +126,7 @@ namespace Emby.Server.Implementations.Channels public Task DeleteItem(BaseItem item) { var internalChannel = _libraryManager.GetItemById(item.ChannelId); - if (internalChannel == null) + if (internalChannel is null) { throw new ArgumentException(nameof(item.ChannelId)); } @@ -160,16 +157,16 @@ namespace Emby.Server.Implementations.Channels } /// <inheritdoc /> - public QueryResult<Channel> GetChannelsInternal(ChannelQuery query) + public async Task<QueryResult<Channel>> GetChannelsInternalAsync(ChannelQuery query) { var user = query.UserId.Equals(default) ? null : _userManager.GetUserById(query.UserId); - var channels = GetAllChannels() - .Select(GetChannelEntity) + var channels = await GetAllChannelEntitiesAsync() .OrderBy(i => i.SortName) - .ToList(); + .ToListAsync() + .ConfigureAwait(false); if (query.IsRecordingsFolder.HasValue) { @@ -227,8 +224,9 @@ namespace Emby.Server.Implementations.Channels .ToList(); } - if (user != null) + if (user is not null) { + var userId = user.Id.ToString("N", CultureInfo.InvariantCulture); channels = channels.Where(i => { if (!i.IsVisible(user)) @@ -238,7 +236,7 @@ namespace Emby.Server.Implementations.Channels try { - return GetChannelProvider(i).IsEnabledFor(user.Id.ToString("N", CultureInfo.InvariantCulture)); + return GetChannelProvider(i).IsEnabledFor(userId); } catch { @@ -253,7 +251,7 @@ namespace Emby.Server.Implementations.Channels if (query.StartIndex.HasValue || query.Limit.HasValue) { int startIndex = query.StartIndex ?? 0; - int count = query.Limit == null ? totalCount - startIndex : Math.Min(query.Limit.Value, totalCount - startIndex); + int count = query.Limit is null ? totalCount - startIndex : Math.Min(query.Limit.Value, totalCount - startIndex); all = all.GetRange(startIndex, count); } @@ -261,7 +259,7 @@ namespace Emby.Server.Implementations.Channels { foreach (var item in all) { - RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).GetAwaiter().GetResult(); + await RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).ConfigureAwait(false); } } @@ -272,13 +270,13 @@ namespace Emby.Server.Implementations.Channels } /// <inheritdoc /> - public QueryResult<BaseItemDto> GetChannels(ChannelQuery query) + public async Task<QueryResult<BaseItemDto>> GetChannelsAsync(ChannelQuery query) { var user = query.UserId.Equals(default) ? null : _userManager.GetUserById(query.UserId); - var internalResult = GetChannelsInternal(query); + var internalResult = await GetChannelsInternalAsync(query).ConfigureAwait(false); var dtoOptions = new DtoOptions(); @@ -330,9 +328,12 @@ namespace Emby.Server.Implementations.Channels progress.Report(100); } - private Channel GetChannelEntity(IChannel channel) + private async IAsyncEnumerable<Channel> GetAllChannelEntitiesAsync() { - return GetChannel(GetInternalChannelId(channel.Name)) ?? GetChannel(channel, CancellationToken.None).GetAwaiter().GetResult(); + foreach (IChannel channel in GetAllChannels()) + { + yield return GetChannel(GetInternalChannelId(channel.Name)) ?? await GetChannel(channel, CancellationToken.None).ConfigureAwait(false); + } } private MediaSourceInfo[] GetSavedMediaSources(BaseItem item) @@ -355,7 +356,7 @@ namespace Emby.Server.Implementations.Channels { var path = Path.Combine(item.GetInternalMetadataPath(), "channelmediasourceinfos.json"); - if (mediaSources == null || mediaSources.Count == 0) + if (mediaSources is null || mediaSources.Count == 0) { try { @@ -404,7 +405,7 @@ namespace Emby.Server.Implementations.Channels } else { - results = new List<MediaSourceInfo>(); + results = Enumerable.Empty<MediaSourceInfo>(); } return results @@ -447,7 +448,7 @@ namespace Emby.Server.Implementations.Channels var item = _libraryManager.GetItemById(id) as Channel; - if (item == null) + if (item is null) { item = new Channel { @@ -601,10 +602,7 @@ namespace Emby.Server.Implementations.Channels private Guid GetInternalChannelId(string name) { - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentNullException(nameof(name)); - } + ArgumentException.ThrowIfNullOrEmpty(name); return _libraryManager.GetNewItemId("Channel " + name, typeof(Channel)); } @@ -739,7 +737,7 @@ namespace Emby.Server.Implementations.Channels query.GroupByPresentationUniqueKey = false; // null if came from cache - if (itemsResult != null) + if (itemsResult is not null) { var items = itemsResult.Items; var itemsLen = items.Count; @@ -761,7 +759,7 @@ namespace Emby.Server.Implementations.Channels foreach (var deadId in deadIds) { var deadItem = _libraryManager.GetItemById(deadId); - if (deadItem != null) + if (deadItem is not null) { _libraryManager.DeleteItem( deadItem, @@ -813,7 +811,7 @@ namespace Emby.Server.Implementations.Channels { await using FileStream jsonStream = AsyncFile.OpenRead(cachePath); var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); - if (cachedResult != null) + if (cachedResult is not null) { return null; } @@ -836,7 +834,7 @@ namespace Emby.Server.Implementations.Channels { await using FileStream jsonStream = AsyncFile.OpenRead(cachePath); var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); - if (cachedResult != null) + if (cachedResult is not null) { return null; } @@ -861,7 +859,7 @@ namespace Emby.Server.Implementations.Channels var result = await channel.GetChannelItems(query, cancellationToken).ConfigureAwait(false); - if (result == null) + if (result is null) { throw new InvalidOperationException("Channel returned a null result from GetChannelItems"); } @@ -955,7 +953,7 @@ namespace Emby.Server.Implementations.Channels _logger.LogError(ex, "Error retrieving channel item from database"); } - if (item == null) + if (item is null) { item = new T(); isNew = true; @@ -1156,7 +1154,7 @@ namespace Emby.Server.Implementations.Channels { _libraryManager.CreateItem(item, parentFolder); - if (info.People != null && info.People.Count > 0) + if (info.People is not null && info.People.Count > 0) { _libraryManager.UpdatePeople(item, info.People); } @@ -1193,7 +1191,7 @@ namespace Emby.Server.Implementations.Channels var result = GetAllChannels() .FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(channel.ChannelId) || string.Equals(i.Name, channel.Name, StringComparison.OrdinalIgnoreCase)); - if (result == null) + if (result is null) { throw new ResourceNotFoundException("No channel provider found for channel " + channel.Name); } @@ -1206,7 +1204,7 @@ namespace Emby.Server.Implementations.Channels var result = GetAllChannels() .FirstOrDefault(i => internalChannelId.Equals(GetInternalChannelId(i.Name))); - if (result == null) + if (result is null) { throw new ResourceNotFoundException("No channel provider found for channel id " + internalChannelId); } diff --git a/Emby.Server.Implementations/Collections/CollectionImageProvider.cs b/Emby.Server.Implementations/Collections/CollectionImageProvider.cs index ca8409402..c31bb4fb9 100644 --- a/Emby.Server.Implementations/Collections/CollectionImageProvider.cs +++ b/Emby.Server.Implementations/Collections/CollectionImageProvider.cs @@ -59,7 +59,7 @@ namespace Emby.Server.Implementations.Collections var episode = subItem as Episode; var series = episode?.Series; - if (series != null && series.HasImage(ImageType.Primary)) + if (series is not null && series.HasImage(ImageType.Primary)) { return series; } @@ -71,7 +71,7 @@ namespace Emby.Server.Implementations.Collections var parent = subItem.GetOwner() ?? subItem.GetParent(); - if (parent != null && parent.HasImage(ImageType.Primary)) + if (parent is not null && parent.HasImage(ImageType.Primary)) { if (parent is MusicAlbum) { @@ -81,7 +81,7 @@ namespace Emby.Server.Implementations.Collections return null; }) - .Where(i => i != null) + .Where(i => i is not null) .GroupBy(x => x!.Id) // We removed the null values .Select(x => x.First()) .ToList()!; // Again... the list doesn't contain any null values diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs index 5fc2e39a7..179683055 100644 --- a/Emby.Server.Implementations/Collections/CollectionManager.cs +++ b/Emby.Server.Implementations/Collections/CollectionManager.cs @@ -81,7 +81,7 @@ namespace Emby.Server.Implementations.Collections internal async Task<Folder?> EnsureLibraryFolder(string path, bool createIfNeeded) { var existingFolder = FindFolders(path).FirstOrDefault(); - if (existingFolder != null) + if (existingFolder is not null) { return existingFolder; } @@ -121,7 +121,7 @@ namespace Emby.Server.Implementations.Collections { var folder = GetCollectionsFolder(false).GetAwaiter().GetResult(); - return folder == null + return folder is null ? Enumerable.Empty<BoxSet>() : folder.GetChildren(user, true).OfType<BoxSet>(); } @@ -138,7 +138,7 @@ namespace Emby.Server.Implementations.Collections var parentFolder = await GetCollectionsFolder(true).ConfigureAwait(false); - if (parentFolder == null) + if (parentFolder is null) { throw new ArgumentException(nameof(parentFolder)); } @@ -206,8 +206,7 @@ namespace Emby.Server.Implementations.Collections throw new ArgumentException("No collection exists with the supplied Id"); } - var list = new List<LinkedChild>(); - var itemList = new List<BaseItem>(); + List<BaseItem>? itemList = null; var linkedChildrenList = collection.GetLinkedChildren(); var currentLinkedChildrenIds = linkedChildrenList.Select(i => i.Id).ToList(); @@ -216,26 +215,31 @@ namespace Emby.Server.Implementations.Collections { var item = _libraryManager.GetItemById(id); - if (item == null) + if (item is null) { throw new ArgumentException("No item exists with the supplied Id"); } if (!currentLinkedChildrenIds.Contains(id)) { - itemList.Add(item); + (itemList ??= new()).Add(item); - list.Add(LinkedChild.Create(item)); linkedChildrenList.Add(item); } } - if (list.Count > 0) + if (itemList is not null) { - var newList = collection.LinkedChildren.ToList(); - newList.AddRange(list); - collection.LinkedChildren = newList.ToArray(); + var originalLen = collection.LinkedChildren.Length; + var newItemCount = itemList.Count; + LinkedChild[] newChildren = new LinkedChild[originalLen + newItemCount]; + collection.LinkedChildren.CopyTo(newChildren, 0); + for (int i = 0; i < newItemCount; i++) + { + newChildren[originalLen + i] = LinkedChild.Create(itemList[i]); + } + collection.LinkedChildren = newChildren; collection.UpdateRatingToItems(linkedChildrenList); await collection.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); @@ -265,9 +269,9 @@ namespace Emby.Server.Implementations.Collections { var childItem = _libraryManager.GetItemById(guidId); - var child = collection.LinkedChildren.FirstOrDefault(i => (i.ItemId.HasValue && i.ItemId.Value.Equals(guidId)) || (childItem != null && string.Equals(childItem.Path, i.Path, StringComparison.OrdinalIgnoreCase))); + var child = collection.LinkedChildren.FirstOrDefault(i => (i.ItemId.HasValue && i.ItemId.Value.Equals(guidId)) || (childItem is not null && string.Equals(childItem.Path, i.Path, StringComparison.OrdinalIgnoreCase))); - if (child == null) + if (child is null) { _logger.LogWarning("No collection title exists with the supplied Id"); continue; @@ -275,7 +279,7 @@ namespace Emby.Server.Implementations.Collections list.Add(child); - if (childItem != null) + if (childItem is not null) { itemList.Add(childItem); } diff --git a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs index ff5602f24..6b8b1a620 100644 --- a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs +++ b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Globalization; using System.IO; @@ -36,7 +34,7 @@ namespace Emby.Server.Implementations.Configuration /// <summary> /// Configuration updating event. /// </summary> - public event EventHandler<GenericEventArgs<ServerConfiguration>> ConfigurationUpdating; + public event EventHandler<GenericEventArgs<ServerConfiguration>>? ConfigurationUpdating; /// <summary> /// Gets the type of the configuration. diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs index 01dc728c1..f0a4c8ffb 100644 --- a/Emby.Server.Implementations/ConfigurationOptions.cs +++ b/Emby.Server.Implementations/ConfigurationOptions.cs @@ -11,7 +11,7 @@ namespace Emby.Server.Implementations /// <summary> /// Gets a new copy of the default configuration options. /// </summary> - public static Dictionary<string, string> DefaultConfiguration => new Dictionary<string, string> + public static Dictionary<string, string?> DefaultConfiguration => new Dictionary<string, string?> { { HostWebClientKey, bool.TrueString }, { DefaultRedirectKey, "web/index.html" }, diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs index e9c005cea..5380c45d8 100644 --- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs +++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs @@ -2,8 +2,6 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Security.Cryptography; -using System.Text; -using MediaBrowser.Common.Extensions; using MediaBrowser.Model.Cryptography; using static MediaBrowser.Model.Cryptography.Constants; @@ -14,25 +12,6 @@ namespace Emby.Server.Implementations.Cryptography /// </summary> public class CryptographyProvider : ICryptoProvider { - // TODO: remove when not needed for backwards compat - private static readonly HashSet<string> _supportedHashMethods = new HashSet<string>() - { - "MD5", - "System.Security.Cryptography.MD5", - "SHA", - "SHA1", - "System.Security.Cryptography.SHA1", - "SHA256", - "SHA-256", - "System.Security.Cryptography.SHA256", - "SHA384", - "SHA-384", - "System.Security.Cryptography.SHA384", - "SHA512", - "SHA-512", - "System.Security.Cryptography.SHA512" - }; - /// <inheritdoc /> public string DefaultHashMethod => "PBKDF2-SHA512"; @@ -80,22 +59,7 @@ namespace Emby.Server.Implementations.Cryptography DefaultOutputLength)); } - if (!_supportedHashMethods.Contains(hash.Id)) - { - throw new CryptographicException($"Requested hash method is not supported: {hash.Id}"); - } - - using var h = HashAlgorithm.Create(hash.Id) ?? throw new ResourceNotFoundException($"Unknown hash method: {hash.Id}."); - var bytes = Encoding.UTF8.GetBytes(password.ToArray()); - if (hash.Salt.Length == 0) - { - return hash.Hash.SequenceEqual(h.ComputeHash(bytes)); - } - - byte[] salted = new byte[bytes.Length + hash.Salt.Length]; - Array.Copy(bytes, salted, bytes.Length); - hash.Salt.CopyTo(salted.AsSpan(bytes.Length)); - return hash.Hash.SequenceEqual(h.ComputeHash(salted)); + throw new NotSupportedException($"Can't verify hash with id: {hash.Id}"); } /// <inheritdoc /> diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs index 450688491..4b9ab53ae 100644 --- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs +++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; -using System.Threading; using Jellyfin.Extensions; using Microsoft.Extensions.Logging; using SQLitePCL.pretty; @@ -27,10 +26,20 @@ namespace Emby.Server.Implementations.Data /// <summary> /// Gets or sets the path to the DB file. /// </summary> - /// <value>Path to the DB file.</value> protected string DbFilePath { get; set; } /// <summary> + /// Gets or sets the number of write connections to create. + /// </summary> + /// <value>Path to the DB file.</value> + protected int WriteConnectionsCount { get; set; } = 1; + + /// <summary> + /// Gets or sets the number of read connections to create. + /// </summary> + protected int ReadConnectionsCount { get; set; } = 1; + + /// <summary> /// Gets the logger. /// </summary> /// <value>The logger.</value> @@ -61,10 +70,21 @@ namespace Emby.Server.Implementations.Data protected virtual int? CacheSize => null; /// <summary> + /// Gets the locking mode. <see href="https://www.sqlite.org/pragma.html#pragma_locking_mode" />. + /// </summary> + protected virtual string LockingMode => "NORMAL"; + + /// <summary> /// Gets the journal mode. <see href="https://www.sqlite.org/pragma.html#pragma_journal_mode" />. /// </summary> /// <value>The journal mode.</value> - protected virtual string JournalMode => "TRUNCATE"; + protected virtual string JournalMode => "WAL"; + + /// <summary> + /// Gets the journal size limit. <see href="https://www.sqlite.org/pragma.html#pragma_journal_size_limit" />. + /// </summary> + /// <value>The journal size limit.</value> + protected virtual int? JournalSizeLimit => 0; /// <summary> /// Gets the page size. @@ -77,86 +97,127 @@ namespace Emby.Server.Implementations.Data /// </summary> /// <value>The temp store mode.</value> /// <see cref="TempStoreMode"/> - protected virtual TempStoreMode TempStore => TempStoreMode.Default; + protected virtual TempStoreMode TempStore => TempStoreMode.Memory; /// <summary> /// Gets the synchronous mode. /// </summary> /// <value>The synchronous mode or null.</value> /// <see cref="SynchronousMode"/> - protected virtual SynchronousMode? Synchronous => null; + protected virtual SynchronousMode? Synchronous => SynchronousMode.Normal; /// <summary> /// Gets or sets the write lock. /// </summary> /// <value>The write lock.</value> - protected SemaphoreSlim WriteLock { get; set; } = new SemaphoreSlim(1, 1); + protected ConnectionPool WriteConnections { get; set; } /// <summary> /// Gets or sets the write connection. /// </summary> /// <value>The write connection.</value> - protected SQLiteDatabaseConnection WriteConnection { get; set; } + protected ConnectionPool ReadConnections { get; set; } - protected ManagedConnection GetConnection(bool readOnly = false) + public virtual void Initialize() { - WriteLock.Wait(); - if (WriteConnection != null) + WriteConnections = new ConnectionPool(WriteConnectionsCount, CreateWriteConnection); + ReadConnections = new ConnectionPool(ReadConnectionsCount, CreateReadConnection); + + // Configuration and pragmas can affect VACUUM so it needs to be last. + using (var connection = GetConnection()) { - return new ManagedConnection(WriteConnection, WriteLock); + connection.Execute("VACUUM"); } + } + + protected ManagedConnection GetConnection(bool readOnly = false) + => readOnly ? ReadConnections.GetConnection() : WriteConnections.GetConnection(); - WriteConnection = SQLite3.Open( + protected SQLiteDatabaseConnection CreateWriteConnection() + { + var writeConnection = SQLite3.Open( DbFilePath, DefaultConnectionFlags | ConnectionFlags.Create | ConnectionFlags.ReadWrite, null); if (CacheSize.HasValue) { - WriteConnection.Execute("PRAGMA cache_size=" + CacheSize.Value); + writeConnection.Execute("PRAGMA cache_size=" + CacheSize.Value); + } + + if (!string.IsNullOrWhiteSpace(LockingMode)) + { + writeConnection.Execute("PRAGMA locking_mode=" + LockingMode); } if (!string.IsNullOrWhiteSpace(JournalMode)) { - WriteConnection.Execute("PRAGMA journal_mode=" + JournalMode); + writeConnection.Execute("PRAGMA journal_mode=" + JournalMode); + } + + if (JournalSizeLimit.HasValue) + { + writeConnection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value); } if (Synchronous.HasValue) { - WriteConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value); + writeConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value); } if (PageSize.HasValue) { - WriteConnection.Execute("PRAGMA page_size=" + PageSize.Value); + writeConnection.Execute("PRAGMA page_size=" + PageSize.Value); } - WriteConnection.Execute("PRAGMA temp_store=" + (int)TempStore); - - // Configuration and pragmas can affect VACUUM so it needs to be last. - WriteConnection.Execute("VACUUM"); + writeConnection.Execute("PRAGMA temp_store=" + (int)TempStore); - return new ManagedConnection(WriteConnection, WriteLock); + return writeConnection; } - public IStatement PrepareStatement(ManagedConnection connection, string sql) - => connection.PrepareStatement(sql); + protected SQLiteDatabaseConnection CreateReadConnection() + { + var connection = SQLite3.Open( + DbFilePath, + DefaultConnectionFlags | ConnectionFlags.ReadOnly, + null); - public IStatement PrepareStatement(IDatabaseConnection connection, string sql) - => connection.PrepareStatement(sql); + if (CacheSize.HasValue) + { + connection.Execute("PRAGMA cache_size=" + CacheSize.Value); + } - public IStatement[] PrepareAll(IDatabaseConnection connection, IReadOnlyList<string> sql) - { - int len = sql.Count; - IStatement[] statements = new IStatement[len]; - for (int i = 0; i < len; i++) + if (!string.IsNullOrWhiteSpace(LockingMode)) + { + connection.Execute("PRAGMA locking_mode=" + LockingMode); + } + + if (!string.IsNullOrWhiteSpace(JournalMode)) + { + connection.Execute("PRAGMA journal_mode=" + JournalMode); + } + + if (JournalSizeLimit.HasValue) + { + connection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value); + } + + if (Synchronous.HasValue) { - statements[i] = connection.PrepareStatement(sql[i]); + connection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value); } - return statements; + connection.Execute("PRAGMA temp_store=" + (int)TempStore); + + return connection; } + public IStatement PrepareStatement(ManagedConnection connection, string sql) + => connection.PrepareStatement(sql); + + public IStatement PrepareStatement(IDatabaseConnection connection, string sql) + => connection.PrepareStatement(sql); + protected bool TableExists(ManagedConnection connection, string name) { return connection.RunInTransaction( @@ -231,22 +292,10 @@ namespace Emby.Server.Implementations.Data if (dispose) { - WriteLock.Wait(); - try - { - WriteConnection?.Dispose(); - } - finally - { - WriteLock.Release(); - } - - WriteLock.Dispose(); + WriteConnections.Dispose(); + ReadConnections.Dispose(); } - WriteConnection = null; - WriteLock = null; - _disposed = true; } } diff --git a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs index 3de9d6371..4516b89dc 100644 --- a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs +++ b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs @@ -44,7 +44,7 @@ namespace Emby.Server.Implementations.Data var item = _libraryManager.GetItemById(itemId); - if (item != null) + if (item is not null) { _logger.LogInformation("Cleaning item {0} type: {1} path: {2}", item.Name, item.GetType().Name, item.Path ?? string.Empty); diff --git a/Emby.Server.Implementations/Data/ConnectionPool.cs b/Emby.Server.Implementations/Data/ConnectionPool.cs new file mode 100644 index 000000000..5ea7e934f --- /dev/null +++ b/Emby.Server.Implementations/Data/ConnectionPool.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Concurrent; +using SQLitePCL.pretty; + +namespace Emby.Server.Implementations.Data; + +/// <summary> +/// A pool of SQLite Database connections. +/// </summary> +public sealed class ConnectionPool : IDisposable +{ + private readonly BlockingCollection<SQLiteDatabaseConnection> _connections = new(); + private bool _disposed; + + /// <summary> + /// Initializes a new instance of the <see cref="ConnectionPool" /> class. + /// </summary> + /// <param name="count">The number of database connection to create.</param> + /// <param name="factory">Factory function to create the database connections.</param> + public ConnectionPool(int count, Func<SQLiteDatabaseConnection> factory) + { + for (int i = 0; i < count; i++) + { + _connections.Add(factory.Invoke()); + } + } + + /// <summary> + /// Gets a database connection from the pool if one is available, otherwise blocks. + /// </summary> + /// <returns>A database connection.</returns> + public ManagedConnection GetConnection() + { + if (_disposed) + { + ThrowObjectDisposedException(); + } + + return new ManagedConnection(_connections.Take(), this); + + static void ThrowObjectDisposedException() + { + throw new ObjectDisposedException(nameof(ConnectionPool)); + } + } + + /// <summary> + /// Return a database connection to the pool. + /// </summary> + /// <param name="connection">The database connection to return.</param> + public void Return(SQLiteDatabaseConnection connection) + { + if (_disposed) + { + connection.Dispose(); + return; + } + + _connections.Add(connection); + } + + /// <inheritdoc /> + public void Dispose() + { + if (_disposed) + { + return; + } + + foreach (var connection in _connections) + { + connection.Dispose(); + } + + _connections.Dispose(); + + _disposed = true; + } +} diff --git a/Emby.Server.Implementations/Data/ManagedConnection.cs b/Emby.Server.Implementations/Data/ManagedConnection.cs index 11e33278d..e84ed8f91 100644 --- a/Emby.Server.Implementations/Data/ManagedConnection.cs +++ b/Emby.Server.Implementations/Data/ManagedConnection.cs @@ -2,23 +2,22 @@ using System; using System.Collections.Generic; -using System.Threading; using SQLitePCL.pretty; namespace Emby.Server.Implementations.Data { public sealed class ManagedConnection : IDisposable { - private readonly SemaphoreSlim _writeLock; + private readonly ConnectionPool _pool; - private SQLiteDatabaseConnection? _db; + private SQLiteDatabaseConnection _db; private bool _disposed = false; - public ManagedConnection(SQLiteDatabaseConnection db, SemaphoreSlim writeLock) + public ManagedConnection(SQLiteDatabaseConnection db, ConnectionPool pool) { _db = db; - _writeLock = writeLock; + _pool = pool; } public IStatement PrepareStatement(string sql) @@ -73,9 +72,9 @@ namespace Emby.Server.Implementations.Data return; } - _writeLock.Release(); + _pool.Return(_db); - _db = null; // Don't dispose it + _db = null!; // Don't dispose it _disposed = true; } } diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs index 736b8125d..4055b0ba1 100644 --- a/Emby.Server.Implementations/Data/SqliteExtensions.cs +++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs @@ -253,7 +253,7 @@ namespace Emby.Server.Implementations.Data { if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) { - if (value == null) + if (value is null) { bindParam.BindNull(); } diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 7622d2fe6..22d485d33 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -5,9 +5,11 @@ using System; using System.Buffers.Text; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; +using System.Runtime.CompilerServices; using System.Text; using System.Text.Json; using System.Threading; @@ -334,6 +336,7 @@ namespace Emby.Server.Implementations.Data _jsonOptions = JsonDefaults.Options; DbFilePath = Path.Combine(_config.ApplicationPaths.DataPath, "library.db"); + ReadConnectionsCount = Environment.ProcessorCount * 2; } /// <inheritdoc /> @@ -345,10 +348,10 @@ namespace Emby.Server.Implementations.Data /// <summary> /// Opens the connection to the database. /// </summary> - /// <param name="userDataRepo">The user data repository.</param> - /// <param name="userManager">The user manager.</param> - public void Initialize(SqliteUserDataRepository userDataRepo, IUserManager userManager) + public override void Initialize() { + base.Initialize(); + const string CreateMediaStreamsTableCommand = "create table if not exists mediastreams (ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, CodecTag TEXT NULL, Comment TEXT NULL, NalLengthSize TEXT NULL, IsAvc BIT NULL, Title TEXT NULL, TimeBase TEXT NULL, CodecTimeBase TEXT NULL, ColorPrimaries TEXT NULL, ColorSpace TEXT NULL, ColorTransfer TEXT NULL, DvVersionMajor INT NULL, DvVersionMinor INT NULL, DvProfile INT NULL, DvLevel INT NULL, RpuPresentFlag INT NULL, ElPresentFlag INT NULL, BlPresentFlag INT NULL, DvBlSignalCompatibilityId INT NULL, IsHearingImpaired BIT NULL, PRIMARY KEY (ItemId, StreamIndex))"; @@ -357,8 +360,6 @@ namespace Emby.Server.Implementations.Data string[] queries = { - "PRAGMA locking_mode=EXCLUSIVE", - "create table if not exists TypedBaseItems (guid GUID primary key NOT NULL, type TEXT NOT NULL, data BLOB NULL, ParentId GUID NULL, Path TEXT NULL)", "create table if not exists AncestorIds (ItemId GUID NOT NULL, AncestorId GUID NOT NULL, AncestorIdText TEXT NOT NULL, PRIMARY KEY (ItemId, AncestorId))", @@ -383,39 +384,6 @@ namespace Emby.Server.Implementations.Data string[] postQueries = { - // obsolete - "drop index if exists idx_TypedBaseItems", - "drop index if exists idx_mediastreams", - "drop index if exists idx_mediastreams1", - "drop index if exists idx_" + ChaptersTableName, - "drop index if exists idx_UserDataKeys1", - "drop index if exists idx_UserDataKeys2", - "drop index if exists idx_TypeTopParentId3", - "drop index if exists idx_TypeTopParentId2", - "drop index if exists idx_TypeTopParentId4", - "drop index if exists idx_Type", - "drop index if exists idx_TypeTopParentId", - "drop index if exists idx_GuidType", - "drop index if exists idx_TopParentId", - "drop index if exists idx_TypeTopParentId6", - "drop index if exists idx_ItemValues2", - "drop index if exists Idx_ProviderIds", - "drop index if exists idx_ItemValues3", - "drop index if exists idx_ItemValues4", - "drop index if exists idx_ItemValues5", - "drop index if exists idx_UserDataKeys3", - "drop table if exists UserDataKeys", - "drop table if exists ProviderIds", - "drop index if exists Idx_ProviderIds1", - "drop table if exists Images", - "drop index if exists idx_Images", - "drop index if exists idx_TypeSeriesPresentationUniqueKey", - "drop index if exists idx_SeriesPresentationUniqueKey", - "drop index if exists idx_TypeSeriesPresentationUniqueKey2", - "drop index if exists idx_AncestorIds3", - "drop index if exists idx_AncestorIds4", - "drop index if exists idx_AncestorIds2", - "create index if not exists idx_PathTypedBaseItems on TypedBaseItems(Path)", "create index if not exists idx_ParentIdTypedBaseItems on TypedBaseItems(ParentId)", @@ -456,6 +424,9 @@ namespace Emby.Server.Implementations.Data // Used to update inherited tags "create index if not exists idx_ItemValues8 on ItemValues(Type, ItemId, Value)", + + "CREATE INDEX IF NOT EXISTS idx_TypedBaseItemsUserDataKeyType ON TypedBaseItems(UserDataKey, Type)", + "CREATE INDEX IF NOT EXISTS idx_PeopleNameListOrder ON People(Name, ListOrder)" }; using (var connection = GetConnection()) @@ -581,8 +552,6 @@ namespace Emby.Server.Implementations.Data connection.RunQueries(postQueries); } - - userDataRepo.Initialize(userManager, WriteLock, WriteConnection); } public void SaveImages(BaseItem item) @@ -616,7 +585,7 @@ namespace Emby.Server.Implementations.Data /// <exception cref="ArgumentNullException"> /// <paramref name="items"/> or <paramref name="cancellationToken"/> is <c>null</c>. /// </exception> - public void SaveItems(IEnumerable<BaseItem> items, CancellationToken cancellationToken) + public void SaveItems(IReadOnlyList<BaseItem> items, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(items); @@ -624,9 +593,11 @@ namespace Emby.Server.Implementations.Data CheckDisposed(); - var tuples = new List<(BaseItem, List<Guid>, BaseItem, string, List<string>)>(); - foreach (var item in items) + var itemsLen = items.Count; + var tuples = new ValueTuple<BaseItem, List<Guid>, BaseItem, string, List<string>>[itemsLen]; + for (int i = 0; i < itemsLen; i++) { + var item = items[i]; var ancestorIds = item.SupportsAncestors ? item.GetAncestorIds().Distinct().ToList() : null; @@ -636,7 +607,7 @@ namespace Emby.Server.Implementations.Data var userdataKey = item.GetUserDataKeys().FirstOrDefault(); var inheritedTags = item.GetInheritedTags(); - tuples.Add((item, ancestorIds, topParent, userdataKey, inheritedTags)); + tuples[i] = (item, ancestorIds, topParent, userdataKey, inheritedTags); } using (var connection = GetConnection()) @@ -652,14 +623,8 @@ namespace Emby.Server.Implementations.Data private void SaveItemsInTransaction(IDatabaseConnection db, IEnumerable<(BaseItem Item, List<Guid> AncestorIds, BaseItem TopParent, string UserDataKey, List<string> InheritedTags)> tuples) { - var statements = PrepareAll(db, new string[] - { - SaveItemCommandText, - "delete from AncestorIds where ItemId=@ItemId" - }); - - using (var saveItemStatement = statements[0]) - using (var deleteAncestorsStatement = statements[1]) + using (var saveItemStatement = PrepareStatement(db, SaveItemCommandText)) + using (var deleteAncestorsStatement = PrepareStatement(db, "delete from AncestorIds where ItemId=@ItemId")) { var requiresReset = false; foreach (var tuple in tuples) @@ -691,7 +656,7 @@ namespace Emby.Server.Implementations.Data private string GetPathToSave(string path) { - if (path == null) + if (path is null) { return null; } @@ -890,7 +855,7 @@ namespace Emby.Server.Implementations.Data saveItemStatement.TryBind("@UnratedType", item.GetBlockUnratedType().ToString()); - if (topParent == null) + if (topParent is null) { saveItemStatement.TryBindNull("@TopParentId"); } @@ -1146,7 +1111,7 @@ namespace Emby.Server.Implementations.Data { var image = ItemImageInfoFromValueString(part); - if (image != null) + if (image is not null) { result[position++] = image; } @@ -1225,7 +1190,7 @@ namespace Emby.Server.Implementations.Data Path = RestorePath(path.ToString()) }; - if (long.TryParse(dateModified, NumberStyles.Any, CultureInfo.InvariantCulture, out var ticks) + if (long.TryParse(dateModified, CultureInfo.InvariantCulture, out var ticks) && ticks >= DateTime.MinValue.Ticks && ticks <= DateTime.MaxValue.Ticks) { @@ -1314,15 +1279,13 @@ namespace Emby.Server.Implementations.Data CheckDisposed(); using (var connection = GetConnection(true)) + using (var statement = PrepareStatement(connection, _retrieveItemColumnsSelectQuery)) { - using (var statement = PrepareStatement(connection, _retrieveItemColumnsSelectQuery)) - { - statement.TryBind("@guid", id); + statement.TryBind("@guid", id); - foreach (var row in statement.ExecuteQuery()) - { - return GetItem(row, new InternalItemsQuery()); - } + foreach (var row in statement.ExecuteQuery()) + { + return GetItem(row, new InternalItemsQuery()); } } @@ -1337,7 +1300,8 @@ namespace Emby.Server.Implementations.Data { return false; } - else if (type == typeof(UserRootFolder)) + + if (type == typeof(UserRootFolder)) { return false; } @@ -1347,55 +1311,68 @@ namespace Emby.Server.Implementations.Data { return false; } - else if (type == typeof(MusicArtist)) + + if (type == typeof(MusicArtist)) { return false; } - else if (type == typeof(Person)) + + if (type == typeof(Person)) { return false; } - else if (type == typeof(MusicGenre)) + + if (type == typeof(MusicGenre)) { return false; } - else if (type == typeof(Genre)) + + if (type == typeof(Genre)) { return false; } - else if (type == typeof(Studio)) + + if (type == typeof(Studio)) { return false; } - else if (type == typeof(PlaylistsFolder)) + + if (type == typeof(PlaylistsFolder)) { return false; } - else if (type == typeof(PhotoAlbum)) + + if (type == typeof(PhotoAlbum)) { return false; } - else if (type == typeof(Year)) + + if (type == typeof(Year)) { return false; } - else if (type == typeof(Book)) + + if (type == typeof(Book)) { return false; } - else if (type == typeof(LiveTvProgram)) + + if (type == typeof(LiveTvProgram)) { return false; } - else if (type == typeof(AudioBook)) + + if (type == typeof(AudioBook)) { return false; } - else if (type == typeof(Audio)) + + if (type == typeof(Audio)) { return false; } - else if (type == typeof(MusicAlbum)) + + if (type == typeof(MusicAlbum)) { return false; } @@ -1414,7 +1391,7 @@ namespace Emby.Server.Implementations.Data var type = _typeMapper.GetType(typeString); - if (type == null) + if (type is null) { return null; } @@ -1433,7 +1410,7 @@ namespace Emby.Server.Implementations.Data } } - if (item == null) + if (item is null) { try { @@ -1444,7 +1421,7 @@ namespace Emby.Server.Implementations.Data } } - if (item == null) + if (item is null) { return null; } @@ -1825,7 +1802,7 @@ namespace Emby.Server.Implementations.Data var hasSeries = item as IHasSeries; if (hasSeriesFields) { - if (hasSeries != null) + if (hasSeries is not null) { if (reader.TryGetGuid(index, out var seriesId)) { @@ -1938,7 +1915,7 @@ namespace Emby.Server.Implementations.Data if (HasField(query, ItemFields.SeriesPresentationUniqueKey)) { - if (hasSeries != null) + if (hasSeries is not null) { if (reader.TryGetString(index, out var seriesPresentationUniqueKey)) { @@ -1986,22 +1963,19 @@ namespace Emby.Server.Implementations.Data { CheckDisposed(); + var chapters = new List<ChapterInfo>(); using (var connection = GetConnection(true)) + using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId order by ChapterIndex asc")) { - var chapters = new List<ChapterInfo>(); + statement.TryBind("@ItemId", item.Id); - using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId order by ChapterIndex asc")) + foreach (var row in statement.ExecuteQuery()) { - statement.TryBind("@ItemId", item.Id); - - foreach (var row in statement.ExecuteQuery()) - { - chapters.Add(GetChapter(row, item)); - } + chapters.Add(GetChapter(row, item)); } - - return chapters; } + + return chapters; } /// <inheritdoc /> @@ -2010,16 +1984,14 @@ namespace Emby.Server.Implementations.Data CheckDisposed(); using (var connection = GetConnection(true)) + using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId and ChapterIndex=@ChapterIndex")) { - using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId and ChapterIndex=@ChapterIndex")) - { - statement.TryBind("@ItemId", item.Id); - statement.TryBind("@ChapterIndex", index); + statement.TryBind("@ItemId", item.Id); + statement.TryBind("@ChapterIndex", index); - foreach (var row in statement.ExecuteQuery()) - { - return GetChapter(row, item); - } + foreach (var row in statement.ExecuteQuery()) + { + return GetChapter(row, item); } } @@ -2151,7 +2123,7 @@ namespace Emby.Server.Implementations.Data private static bool EnableJoinUserData(InternalItemsQuery query) { - if (query.User == null) + if (query.User is null) { return false; } @@ -2205,7 +2177,7 @@ namespace Emby.Server.Implementations.Data private bool HasProgramAttributes(InternalItemsQuery query) { - if (query.ParentType != null && _programExcludeParentTypes.Contains(query.ParentType.Value)) + if (query.ParentType is not null && _programExcludeParentTypes.Contains(query.ParentType.Value)) { return false; } @@ -2220,7 +2192,7 @@ namespace Emby.Server.Implementations.Data private bool HasServiceName(InternalItemsQuery query) { - if (query.ParentType != null && _programExcludeParentTypes.Contains(query.ParentType.Value)) + if (query.ParentType is not null && _programExcludeParentTypes.Contains(query.ParentType.Value)) { return false; } @@ -2235,7 +2207,7 @@ namespace Emby.Server.Implementations.Data private bool HasStartDate(InternalItemsQuery query) { - if (query.ParentType != null && _programExcludeParentTypes.Contains(query.ParentType.Value)) + if (query.ParentType is not null && _programExcludeParentTypes.Contains(query.ParentType.Value)) { return false; } @@ -2270,7 +2242,7 @@ namespace Emby.Server.Implementations.Data private bool HasArtistFields(InternalItemsQuery query) { - if (query.ParentType != null && _artistExcludeParentTypes.Contains(query.ParentType.Value)) + if (query.ParentType is not null && _artistExcludeParentTypes.Contains(query.ParentType.Value)) { return false; } @@ -2392,20 +2364,24 @@ namespace Emby.Server.Implementations.Data columns.Add("UserDatas.rating"); } - if (query.SimilarTo != null) + if (query.SimilarTo is not null) { var item = query.SimilarTo; var builder = new StringBuilder(); builder.Append('('); - if (string.IsNullOrEmpty(item.OfficialRating)) + if (item.InheritedParentalRatingValue == 0) { - builder.Append("(OfficialRating is null * 10)"); + builder.Append("((InheritedParentalRatingValue=0) * 10)"); } else { - builder.Append("(OfficialRating=@ItemOfficialRating * 10)"); + builder.Append( + @"(SELECT CASE WHEN COALESCE(InheritedParentalRatingValue, 0)=0 + THEN 0 + ELSE 10.0 / (1.0 + ABS(InheritedParentalRatingValue - @InheritedParentalRatingValue)) + END)"); } if (item.ProductionYear.HasValue) @@ -2416,6 +2392,7 @@ namespace Emby.Server.Implementations.Data // genres, tags, studios, person, year? builder.Append("+ (Select count(1) * 10 from ItemValues where ItemId=Guid and CleanValue in (select CleanValue from ItemValues where ItemId=@SimilarItemId))"); + builder.Append("+ (Select count(1) * 10 from People where ItemId=Guid and Name in (select Name from People where ItemId=@SimilarItemId))"); if (item is MusicArtist) { @@ -2456,10 +2433,12 @@ namespace Emby.Server.Implementations.Data builder.Append('('); builder.Append("((CleanName like @SearchTermStartsWith or (OriginalTitle not null and OriginalTitle like @SearchTermStartsWith)) * 10)"); + builder.Append("+ ((CleanName = @SearchTermStartsWith COLLATE NOCASE or (OriginalTitle not null and OriginalTitle = @SearchTermStartsWith COLLATE NOCASE)) * 10)"); if (query.SearchTerm.Length > 1) { builder.Append("+ ((CleanName like @SearchTermContains or (OriginalTitle not null and OriginalTitle like @SearchTermContains)) * 10)"); + builder.Append("+ ((Tags not null and Tags like @SearchTermContains) * 5)"); } builder.Append(") as SearchScore"); @@ -2496,7 +2475,7 @@ namespace Emby.Server.Implementations.Data { var item = query.SimilarTo; - if (item == null) + if (item is null) { return; } @@ -2517,6 +2496,11 @@ namespace Emby.Server.Implementations.Data { statement.TryBind("@SimilarItemId", item.Id); } + + if (commandText.Contains("@InheritedParentalRatingValue", StringComparison.OrdinalIgnoreCase)) + { + statement.TryBind("@InheritedParentalRatingValue", item.InheritedParentalRatingValue); + } } private string GetJoinUserDataText(InternalItemsQuery query) @@ -2556,8 +2540,6 @@ namespace Emby.Server.Implementations.Data CheckDisposed(); - var now = DateTime.UtcNow; - // Hack for right now since we currently don't support filtering out these duplicates within a query if (query.Limit.HasValue && query.EnableGroupByMetadataKey) { @@ -2579,28 +2561,24 @@ namespace Emby.Server.Implementations.Data } var commandText = commandTextBuilder.ToString(); - int count; + + using (new QueryTimeLogger(Logger, commandText)) using (var connection = GetConnection(true)) + using (var statement = PrepareStatement(connection, commandText)) { - using (var statement = PrepareStatement(connection, commandText)) + if (EnableJoinUserData(query)) { - if (EnableJoinUserData(query)) - { - statement.TryBind("@UserId", query.User.InternalId); - } + statement.TryBind("@UserId", query.User.InternalId); + } - BindSimilarParams(query, statement); - BindSearchParams(query, statement); + BindSimilarParams(query, statement); + BindSearchParams(query, statement); - // Running this again will bind the params - GetWhereClauses(query, statement); + // Running this again will bind the params + GetWhereClauses(query, statement); - count = statement.ExecuteQuery().SelectScalarInt().First(); - } + return statement.ExecuteQuery().SelectScalarInt().First(); } - - LogQueryTime("GetCount", commandText, now); - return count; } public List<BaseItem> GetItemList(InternalItemsQuery query) @@ -2609,8 +2587,6 @@ namespace Emby.Server.Implementations.Data CheckDisposed(); - var now = DateTime.UtcNow; - // Hack for right now since we currently don't support filtering out these duplicates within a query if (query.Limit.HasValue && query.EnableGroupByMetadataKey) { @@ -2654,61 +2630,58 @@ namespace Emby.Server.Implementations.Data var commandText = commandTextBuilder.ToString(); var items = new List<BaseItem>(); + using (new QueryTimeLogger(Logger, commandText)) using (var connection = GetConnection(true)) + using (var statement = PrepareStatement(connection, commandText)) { - using (var statement = PrepareStatement(connection, commandText)) + if (EnableJoinUserData(query)) { - if (EnableJoinUserData(query)) - { - statement.TryBind("@UserId", query.User.InternalId); - } + statement.TryBind("@UserId", query.User.InternalId); + } - BindSimilarParams(query, statement); - BindSearchParams(query, statement); + BindSimilarParams(query, statement); + BindSearchParams(query, statement); - // Running this again will bind the params - GetWhereClauses(query, statement); + // Running this again will bind the params + GetWhereClauses(query, statement); - var hasEpisodeAttributes = HasEpisodeAttributes(query); - var hasServiceName = HasServiceName(query); - var hasProgramAttributes = HasProgramAttributes(query); - var hasStartDate = HasStartDate(query); - var hasTrailerTypes = HasTrailerTypes(query); - var hasArtistFields = HasArtistFields(query); - var hasSeriesFields = HasSeriesFields(query); + var hasEpisodeAttributes = HasEpisodeAttributes(query); + var hasServiceName = HasServiceName(query); + var hasProgramAttributes = HasProgramAttributes(query); + var hasStartDate = HasStartDate(query); + var hasTrailerTypes = HasTrailerTypes(query); + var hasArtistFields = HasArtistFields(query); + var hasSeriesFields = HasSeriesFields(query); - foreach (var row in statement.ExecuteQuery()) + foreach (var row in statement.ExecuteQuery()) + { + var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields); + if (item is not null) { - var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields); - if (item != null) - { - items.Add(item); - } + items.Add(item); } } + } - // Hack for right now since we currently don't support filtering out these duplicates within a query - if (query.EnableGroupByMetadataKey) + // Hack for right now since we currently don't support filtering out these duplicates within a query + if (query.EnableGroupByMetadataKey) + { + var limit = query.Limit ?? int.MaxValue; + limit -= 4; + var newList = new List<BaseItem>(); + + foreach (var item in items) { - var limit = query.Limit ?? int.MaxValue; - limit -= 4; - var newList = new List<BaseItem>(); + AddItem(newList, item); - foreach (var item in items) + if (newList.Count >= limit) { - AddItem(newList, item); - - if (newList.Count >= limit) - { - break; - } + break; } - - items = newList; } - } - LogQueryTime("GetItemList", commandText, now); + items = newList; + } return items; } @@ -2761,26 +2734,6 @@ namespace Emby.Server.Implementations.Data items.Add(newItem); } - private void LogQueryTime(string methodName, string commandText, DateTime startDate) - { - var elapsed = (DateTime.UtcNow - startDate).TotalMilliseconds; - -#if DEBUG - const int SlowThreshold = 100; -#else - const int SlowThreshold = 10; -#endif - - if (elapsed >= SlowThreshold) - { - Logger.LogDebug( - "{Method} query time (slow): {ElapsedMs}ms. Query: {Query}", - methodName, - elapsed, - commandText); - } - } - public QueryResult<BaseItem> GetItems(InternalItemsQuery query) { ArgumentNullException.ThrowIfNull(query); @@ -2796,8 +2749,6 @@ namespace Emby.Server.Implementations.Data returnList); } - var now = DateTime.UtcNow; - // Hack for right now since we currently don't support filtering out these duplicates within a query if (query.Limit.HasValue && query.EnableGroupByMetadataKey) { @@ -2893,12 +2844,10 @@ namespace Emby.Server.Implementations.Data connection.RunInTransaction( db => { - var itemQueryStatement = PrepareStatement(db, itemQuery); - var totalRecordCountQueryStatement = PrepareStatement(db, totalRecordCountQuery); - if (!isReturningZeroItems) { - using (var statement = itemQueryStatement) + using (new QueryTimeLogger(Logger, itemQuery, "GetItems.ItemQuery")) + using (var statement = PrepareStatement(db, itemQuery)) { if (EnableJoinUserData(query)) { @@ -2922,20 +2871,18 @@ namespace Emby.Server.Implementations.Data foreach (var row in statement.ExecuteQuery()) { var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields); - if (item != null) + if (item is not null) { list.Add(item); } } } - - LogQueryTime("GetItems.ItemQuery", itemQuery, now); } - now = DateTime.UtcNow; if (query.EnableTotalRecordCount) { - using (var statement = totalRecordCountQueryStatement) + using (new QueryTimeLogger(Logger, totalRecordCountQuery, "GetItems.TotalRecordCount")) + using (var statement = PrepareStatement(db, totalRecordCountQuery)) { if (EnableJoinUserData(query)) { @@ -2950,8 +2897,6 @@ namespace Emby.Server.Implementations.Data result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First(); } - - LogQueryTime("GetItems.TotalRecordCount", totalRecordCountQuery, now); } }, ReadTransactionMode); @@ -2965,7 +2910,7 @@ namespace Emby.Server.Implementations.Data private string GetOrderByText(InternalItemsQuery query) { var orderBy = query.OrderBy; - bool hasSimilar = query.SimilarTo != null; + bool hasSimilar = query.SimilarTo is not null; bool hasSearch = !string.IsNullOrEmpty(query.SearchTerm); if (hasSimilar || hasSearch) @@ -3154,6 +3099,11 @@ namespace Emby.Server.Implementations.Data return ItemSortBy.SimilarityScore; } + if (string.Equals(name, ItemSortBy.SearchScore, StringComparison.OrdinalIgnoreCase)) + { + return ItemSortBy.SearchScore; + } + // Unknown SortBy, just sort by the SortName. return ItemSortBy.SortName; } @@ -3164,8 +3114,6 @@ namespace Emby.Server.Implementations.Data CheckDisposed(); - var now = DateTime.UtcNow; - var columns = new List<string> { "guid" }; SetFinalColumnsToSelect(query, columns); var commandTextBuilder = new StringBuilder("select ", 256) @@ -3202,29 +3150,27 @@ namespace Emby.Server.Implementations.Data var commandText = commandTextBuilder.ToString(); var list = new List<Guid>(); + using (new QueryTimeLogger(Logger, commandText)) using (var connection = GetConnection(true)) + using (var statement = PrepareStatement(connection, commandText)) { - using (var statement = PrepareStatement(connection, commandText)) + if (EnableJoinUserData(query)) { - if (EnableJoinUserData(query)) - { - statement.TryBind("@UserId", query.User.InternalId); - } + statement.TryBind("@UserId", query.User.InternalId); + } - BindSimilarParams(query, statement); - BindSearchParams(query, statement); + BindSimilarParams(query, statement); + BindSearchParams(query, statement); - // Running this again will bind the params - GetWhereClauses(query, statement); + // Running this again will bind the params + GetWhereClauses(query, statement); - foreach (var row in statement.ExecuteQuery()) - { - list.Add(row[0].ReadGuidFromBlob()); - } + foreach (var row in statement.ExecuteQuery()) + { + list.Add(row[0].ReadGuidFromBlob()); } } - LogQueryTime("GetItemList", commandText, now); return list; } @@ -3256,7 +3202,8 @@ namespace Emby.Server.Implementations.Data return IsAlphaNumeric(value); } - private List<string> GetWhereClauses(InternalItemsQuery query, IStatement statement) +#nullable enable + private List<string> GetWhereClauses(InternalItemsQuery query, IStatement? statement) { if (query.IsResumable ?? false) { @@ -3390,7 +3337,7 @@ namespace Emby.Server.Implementations.Data } } - if (query.SimilarTo != null && query.MinSimilarityScore > 0) + if (query.SimilarTo is not null && query.MinSimilarityScore > 0) { whereClauses.Add("SimilarityScore > " + (query.MinSimilarityScore - 1).ToString(CultureInfo.InvariantCulture)); } @@ -3524,6 +3471,13 @@ namespace Emby.Server.Implementations.Data statement?.TryBind("@MinIndexNumber", query.MinIndexNumber.Value); } + if (query.MinParentAndIndexNumber.HasValue) + { + whereClauses.Add("((ParentIndexNumber=@MinParentAndIndexNumberParent and IndexNumber>=@MinParentAndIndexNumberIndex) or ParentIndexNumber>@MinParentAndIndexNumberParent)"); + statement?.TryBind("@MinParentAndIndexNumberParent", query.MinParentAndIndexNumber.Value.ParentIndexNumber); + statement?.TryBind("@MinParentAndIndexNumberIndex", query.MinParentAndIndexNumber.Value.IndexNumber); + } + if (query.MinDateCreated.HasValue) { whereClauses.Add("DateCreated>=@MinDateCreated"); @@ -3670,7 +3624,7 @@ namespace Emby.Server.Implementations.Data .Append(paramName) .Append("))) OR "); - if (statement != null) + if (statement is not null) { query.PersonIds[i].TryWriteBytes(idBytes); statement.TryBind(paramName, idBytes); @@ -3721,10 +3675,9 @@ namespace Emby.Server.Implementations.Data if (!string.IsNullOrWhiteSpace(nameContains)) { whereClauses.Add("(CleanName like @NameContains or OriginalTitle like @NameContains)"); - if (statement != null) + if (statement is not null) { nameContains = FixUnicodeChars(nameContains); - statement.TryBind("@NameContains", "%" + GetCleanValue(nameContains) + "%"); } } @@ -3850,13 +3803,8 @@ namespace Emby.Server.Implementations.Data foreach (var artistId in query.ArtistIds) { var paramName = "@ArtistIds" + index; - clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))"); - if (statement != null) - { - statement.TryBind(paramName, artistId); - } - + statement?.TryBind(paramName, artistId); index++; } @@ -3871,13 +3819,8 @@ namespace Emby.Server.Implementations.Data foreach (var artistId in query.AlbumArtistIds) { var paramName = "@ArtistIds" + index; - clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=1))"); - if (statement != null) - { - statement.TryBind(paramName, artistId); - } - + statement?.TryBind(paramName, artistId); index++; } @@ -3892,13 +3835,8 @@ namespace Emby.Server.Implementations.Data foreach (var artistId in query.ContributingArtistIds) { var paramName = "@ArtistIds" + index; - clauses.Add("((select CleanName from TypedBaseItems where guid=" + paramName + ") in (select CleanValue from ItemValues where ItemId=Guid and Type=0) AND (select CleanName from TypedBaseItems where guid=" + paramName + ") not in (select CleanValue from ItemValues where ItemId=Guid and Type=1))"); - if (statement != null) - { - statement.TryBind(paramName, artistId); - } - + statement?.TryBind(paramName, artistId); index++; } @@ -3913,13 +3851,8 @@ namespace Emby.Server.Implementations.Data foreach (var albumId in query.AlbumIds) { var paramName = "@AlbumIds" + index; - clauses.Add("Album in (select Name from typedbaseitems where guid=" + paramName + ")"); - if (statement != null) - { - statement.TryBind(paramName, albumId); - } - + statement?.TryBind(paramName, albumId); index++; } @@ -3934,13 +3867,8 @@ namespace Emby.Server.Implementations.Data foreach (var artistId in query.ExcludeArtistIds) { var paramName = "@ExcludeArtistId" + index; - clauses.Add("(guid not in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))"); - if (statement != null) - { - statement.TryBind(paramName, artistId); - } - + statement?.TryBind(paramName, artistId); index++; } @@ -3955,13 +3883,8 @@ namespace Emby.Server.Implementations.Data foreach (var genreId in query.GenreIds) { var paramName = "@GenreId" + index; - clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=2))"); - if (statement != null) - { - statement.TryBind(paramName, genreId); - } - + statement?.TryBind(paramName, genreId); index++; } @@ -3976,11 +3899,7 @@ namespace Emby.Server.Implementations.Data foreach (var item in query.Genres) { clauses.Add("@Genre" + index + " in (select CleanValue from ItemValues where ItemId=Guid and Type=2)"); - if (statement != null) - { - statement.TryBind("@Genre" + index, GetCleanValue(item)); - } - + statement?.TryBind("@Genre" + index, GetCleanValue(item)); index++; } @@ -3995,11 +3914,7 @@ namespace Emby.Server.Implementations.Data foreach (var item in tags) { clauses.Add("@Tag" + index + " in (select CleanValue from ItemValues where ItemId=Guid and Type=4)"); - if (statement != null) - { - statement.TryBind("@Tag" + index, GetCleanValue(item)); - } - + statement?.TryBind("@Tag" + index, GetCleanValue(item)); index++; } @@ -4014,11 +3929,7 @@ namespace Emby.Server.Implementations.Data foreach (var item in excludeTags) { clauses.Add("@ExcludeTag" + index + " not in (select CleanValue from ItemValues where ItemId=Guid and Type=4)"); - if (statement != null) - { - statement.TryBind("@ExcludeTag" + index, GetCleanValue(item)); - } - + statement?.TryBind("@ExcludeTag" + index, GetCleanValue(item)); index++; } @@ -4033,14 +3944,8 @@ namespace Emby.Server.Implementations.Data foreach (var studioId in query.StudioIds) { var paramName = "@StudioId" + index; - clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=3))"); - - if (statement != null) - { - statement.TryBind(paramName, studioId); - } - + statement?.TryBind(paramName, studioId); index++; } @@ -4055,11 +3960,7 @@ namespace Emby.Server.Implementations.Data foreach (var item in query.OfficialRatings) { clauses.Add("OfficialRating=@OfficialRating" + index); - if (statement != null) - { - statement.TryBind("@OfficialRating" + index, item); - } - + statement?.TryBind("@OfficialRating" + index, item); index++; } @@ -4067,34 +3968,96 @@ namespace Emby.Server.Implementations.Data whereClauses.Add(clause); } - if (query.MinParentalRating.HasValue) + var ratingClauseBuilder = new StringBuilder("("); + if (query.HasParentalRating ?? false) { - whereClauses.Add("InheritedParentalRatingValue>=@MinParentalRating"); - if (statement != null) + ratingClauseBuilder.Append("InheritedParentalRatingValue not null"); + if (query.MinParentalRating.HasValue) { - statement.TryBind("@MinParentalRating", query.MinParentalRating.Value); + ratingClauseBuilder.Append(" AND InheritedParentalRatingValue >= @MinParentalRating"); + statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value); } - } - if (query.MaxParentalRating.HasValue) - { - whereClauses.Add("InheritedParentalRatingValue<=@MaxParentalRating"); - if (statement != null) + if (query.MaxParentalRating.HasValue) { - statement.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); + ratingClauseBuilder.Append(" AND InheritedParentalRatingValue <= @MaxParentalRating"); + statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); } } - - if (query.HasParentalRating.HasValue) + else if (query.BlockUnratedItems.Length > 0) { - if (query.HasParentalRating.Value) + var paramName = "@UnratedType"; + var index = 0; + string blockedUnratedItems = string.Join(',', query.BlockUnratedItems.Select(_ => paramName + index++)); + ratingClauseBuilder.Append("(InheritedParentalRatingValue is null AND UnratedType not in (" + blockedUnratedItems + "))"); + + if (statement is not null) + { + for (var ind = 0; ind < query.BlockUnratedItems.Length; ind++) + { + statement.TryBind(paramName + ind, query.BlockUnratedItems[ind].ToString()); + } + } + + if (query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue) { - whereClauses.Add("InheritedParentalRatingValue > 0"); + ratingClauseBuilder.Append(" OR ("); } - else + + if (query.MinParentalRating.HasValue) + { + ratingClauseBuilder.Append("InheritedParentalRatingValue >= @MinParentalRating"); + statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value); + } + + if (query.MaxParentalRating.HasValue) + { + if (query.MinParentalRating.HasValue) + { + ratingClauseBuilder.Append(" AND "); + } + + ratingClauseBuilder.Append("InheritedParentalRatingValue <= @MaxParentalRating"); + statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); + } + + if (query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue) + { + ratingClauseBuilder.Append(")"); + } + + if (!(query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue)) + { + ratingClauseBuilder.Append(" OR InheritedParentalRatingValue not null"); + } + } + else if (query.MinParentalRating.HasValue) + { + ratingClauseBuilder.Append("InheritedParentalRatingValue is null OR (InheritedParentalRatingValue >= @MinParentalRating"); + statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value); + + if (query.MaxParentalRating.HasValue) { - whereClauses.Add("InheritedParentalRatingValue = 0"); + ratingClauseBuilder.Append(" AND InheritedParentalRatingValue <= @MaxParentalRating"); + statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); } + + ratingClauseBuilder.Append(")"); + } + else if (query.MaxParentalRating.HasValue) + { + ratingClauseBuilder.Append("InheritedParentalRatingValue is null OR InheritedParentalRatingValue <= @MaxParentalRating"); + statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); + } + else if (!query.HasParentalRating ?? false) + { + ratingClauseBuilder.Append("InheritedParentalRatingValue is null"); + } + + var ratingClauseString = ratingClauseBuilder.ToString(); + if (!string.Equals(ratingClauseString, "(", StringComparison.OrdinalIgnoreCase)) + { + whereClauses.Add(ratingClauseString + ")"); } if (query.HasOfficialRating.HasValue) @@ -4136,37 +4099,25 @@ namespace Emby.Server.Implementations.Data if (!string.IsNullOrWhiteSpace(query.HasNoAudioTrackWithLanguage)) { whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Audio' and MediaStreams.Language=@HasNoAudioTrackWithLanguage limit 1) is null)"); - if (statement != null) - { - statement.TryBind("@HasNoAudioTrackWithLanguage", query.HasNoAudioTrackWithLanguage); - } + statement?.TryBind("@HasNoAudioTrackWithLanguage", query.HasNoAudioTrackWithLanguage); } if (!string.IsNullOrWhiteSpace(query.HasNoInternalSubtitleTrackWithLanguage)) { whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' and MediaStreams.IsExternal=0 and MediaStreams.Language=@HasNoInternalSubtitleTrackWithLanguage limit 1) is null)"); - if (statement != null) - { - statement.TryBind("@HasNoInternalSubtitleTrackWithLanguage", query.HasNoInternalSubtitleTrackWithLanguage); - } + statement?.TryBind("@HasNoInternalSubtitleTrackWithLanguage", query.HasNoInternalSubtitleTrackWithLanguage); } if (!string.IsNullOrWhiteSpace(query.HasNoExternalSubtitleTrackWithLanguage)) { whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' and MediaStreams.IsExternal=1 and MediaStreams.Language=@HasNoExternalSubtitleTrackWithLanguage limit 1) is null)"); - if (statement != null) - { - statement.TryBind("@HasNoExternalSubtitleTrackWithLanguage", query.HasNoExternalSubtitleTrackWithLanguage); - } + statement?.TryBind("@HasNoExternalSubtitleTrackWithLanguage", query.HasNoExternalSubtitleTrackWithLanguage); } if (!string.IsNullOrWhiteSpace(query.HasNoSubtitleTrackWithLanguage)) { whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' and MediaStreams.Language=@HasNoSubtitleTrackWithLanguage limit 1) is null)"); - if (statement != null) - { - statement.TryBind("@HasNoSubtitleTrackWithLanguage", query.HasNoSubtitleTrackWithLanguage); - } + statement?.TryBind("@HasNoSubtitleTrackWithLanguage", query.HasNoSubtitleTrackWithLanguage); } if (query.HasSubtitles.HasValue) @@ -4216,15 +4167,11 @@ namespace Emby.Server.Implementations.Data if (query.Years.Length == 1) { whereClauses.Add("ProductionYear=@Years"); - if (statement != null) - { - statement.TryBind("@Years", query.Years[0].ToString(CultureInfo.InvariantCulture)); - } + statement?.TryBind("@Years", query.Years[0].ToString(CultureInfo.InvariantCulture)); } else if (query.Years.Length > 1) { var val = string.Join(',', query.Years); - whereClauses.Add("ProductionYear in (" + val + ")"); } @@ -4232,10 +4179,7 @@ namespace Emby.Server.Implementations.Data if (isVirtualItem.HasValue) { whereClauses.Add("IsVirtualItem=@IsVirtualItem"); - if (statement != null) - { - statement.TryBind("@IsVirtualItem", isVirtualItem.Value); - } + statement?.TryBind("@IsVirtualItem", isVirtualItem.Value); } if (query.IsSpecialSeason.HasValue) @@ -4266,31 +4210,22 @@ namespace Emby.Server.Implementations.Data if (queryMediaTypes.Length == 1) { whereClauses.Add("MediaType=@MediaTypes"); - if (statement != null) - { - statement.TryBind("@MediaTypes", queryMediaTypes[0]); - } + statement?.TryBind("@MediaTypes", queryMediaTypes[0]); } else if (queryMediaTypes.Length > 1) { var val = string.Join(',', queryMediaTypes.Select(i => "'" + i + "'")); - whereClauses.Add("MediaType in (" + val + ")"); } if (query.ItemIds.Length > 0) { var includeIds = new List<string>(); - var index = 0; foreach (var id in query.ItemIds) { includeIds.Add("Guid = @IncludeId" + index); - if (statement != null) - { - statement.TryBind("@IncludeId" + index, id); - } - + statement?.TryBind("@IncludeId" + index, id); index++; } @@ -4300,23 +4235,18 @@ namespace Emby.Server.Implementations.Data if (query.ExcludeItemIds.Length > 0) { var excludeIds = new List<string>(); - var index = 0; foreach (var id in query.ExcludeItemIds) { excludeIds.Add("Guid <> @ExcludeId" + index); - if (statement != null) - { - statement.TryBind("@ExcludeId" + index, id); - } - + statement?.TryBind("@ExcludeId" + index, id); index++; } whereClauses.Add(string.Join(" AND ", excludeIds)); } - if (query.ExcludeProviderIds != null && query.ExcludeProviderIds.Count > 0) + if (query.ExcludeProviderIds is not null && query.ExcludeProviderIds.Count > 0) { var excludeIds = new List<string>(); @@ -4330,11 +4260,7 @@ namespace Emby.Server.Implementations.Data var paramName = "@ExcludeProviderId" + index; excludeIds.Add("(ProviderIds is null or ProviderIds not like " + paramName + ")"); - if (statement != null) - { - statement.TryBind(paramName, "%" + pair.Key + "=" + pair.Value + "%"); - } - + statement?.TryBind(paramName, "%" + pair.Key + "=" + pair.Value + "%"); index++; break; @@ -4346,7 +4272,7 @@ namespace Emby.Server.Implementations.Data } } - if (query.HasAnyProviderId != null && query.HasAnyProviderId.Count > 0) + if (query.HasAnyProviderId is not null && query.HasAnyProviderId.Count > 0) { var hasProviderIds = new List<string>(); @@ -4359,7 +4285,7 @@ namespace Emby.Server.Implementations.Data } // TODO this seems to be an idea for a better schema where ProviderIds are their own table - // buut this is not implemented + // but this is not implemented // hasProviderIds.Add("(COALESCE((select value from ProviderIds where ItemId=Guid and Name = '" + pair.Key + "'), '') <> " + paramName + ")"); // TODO this is a really BAD way to do it since the pair: @@ -4373,11 +4299,7 @@ namespace Emby.Server.Implementations.Data hasProviderIds.Add("ProviderIds like " + paramName); // this replaces the placeholder with a value, here: %key=val% - if (statement != null) - { - statement.TryBind(paramName, "%" + pair.Key + "=" + pair.Value + "%"); - } - + statement?.TryBind(paramName, "%" + pair.Key + "=" + pair.Value + "%"); index++; break; @@ -4454,11 +4376,7 @@ namespace Emby.Server.Implementations.Data if (query.AncestorIds.Length == 1) { whereClauses.Add("Guid in (select itemId from AncestorIds where AncestorId=@AncestorId)"); - - if (statement != null) - { - statement.TryBind("@AncestorId", query.AncestorIds[0]); - } + statement?.TryBind("@AncestorId", query.AncestorIds[0]); } if (query.AncestorIds.Length > 1) @@ -4471,45 +4389,19 @@ namespace Emby.Server.Implementations.Data { var inClause = "select guid from TypedBaseItems where PresentationUniqueKey=@AncestorWithPresentationUniqueKey"; whereClauses.Add(string.Format(CultureInfo.InvariantCulture, "Guid in (select itemId from AncestorIds where AncestorId in ({0}))", inClause)); - if (statement != null) - { - statement.TryBind("@AncestorWithPresentationUniqueKey", query.AncestorWithPresentationUniqueKey); - } + statement?.TryBind("@AncestorWithPresentationUniqueKey", query.AncestorWithPresentationUniqueKey); } if (!string.IsNullOrWhiteSpace(query.SeriesPresentationUniqueKey)) { whereClauses.Add("SeriesPresentationUniqueKey=@SeriesPresentationUniqueKey"); - - if (statement != null) - { - statement.TryBind("@SeriesPresentationUniqueKey", query.SeriesPresentationUniqueKey); - } - } - - if (query.BlockUnratedItems.Length == 1) - { - whereClauses.Add("(InheritedParentalRatingValue > 0 or UnratedType <> @UnratedType)"); - if (statement != null) - { - statement.TryBind("@UnratedType", query.BlockUnratedItems[0].ToString()); - } - } - - if (query.BlockUnratedItems.Length > 1) - { - var inClause = string.Join(',', query.BlockUnratedItems.Select(i => "'" + i.ToString() + "'")); - whereClauses.Add( - string.Format( - CultureInfo.InvariantCulture, - "(InheritedParentalRatingValue > 0 or UnratedType not in ({0}))", - inClause)); + statement?.TryBind("@SeriesPresentationUniqueKey", query.SeriesPresentationUniqueKey); } if (query.ExcludeInheritedTags.Length > 0) { var paramName = "@ExcludeInheritedTags"; - if (statement == null) + if (statement is null) { int index = 0; string excludedTags = string.Join(',', query.ExcludeInheritedTags.Select(_ => paramName + index++)); @@ -4524,6 +4416,24 @@ namespace Emby.Server.Implementations.Data } } + if (query.IncludeInheritedTags.Length > 0) + { + var paramName = "@IncludeInheritedTags"; + if (statement is null) + { + 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)"); + } + else + { + for (int index = 0; index < query.IncludeInheritedTags.Length; index++) + { + statement.TryBind(paramName + index, GetCleanValue(query.IncludeInheritedTags[index])); + } + } + } + if (query.SeriesStatuses.Length > 0) { var statuses = new List<string>(); @@ -4634,6 +4544,7 @@ namespace Emby.Server.Implementations.Data return whereClauses; } +#nullable disable /// <summary> /// Formats a where clause for the specified provider. @@ -4719,7 +4630,7 @@ namespace Emby.Server.Implementations.Data return false; } - if (query.User == null) + if (query.User is null) { return false; } @@ -4840,22 +4751,20 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type commandText.Append(" LIMIT ").Append(query.Limit); } + var list = new List<string>(); using (var connection = GetConnection(true)) + using (var statement = PrepareStatement(connection, commandText.ToString())) { - var list = new List<string>(); - using (var statement = PrepareStatement(connection, commandText.ToString())) - { - // Run this again to bind the params - GetPeopleWhereClauses(query, statement); + // Run this again to bind the params + GetPeopleWhereClauses(query, statement); - foreach (var row in statement.ExecuteQuery()) - { - list.Add(row.GetString(0)); - } + foreach (var row in statement.ExecuteQuery()) + { + list.Add(row.GetString(0)); } - - return list; } + + return list; } public List<PersonInfo> GetPeople(InternalPeopleQuery query) @@ -4880,30 +4789,27 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type commandText += " LIMIT " + query.Limit; } + var list = new List<PersonInfo>(); using (var connection = GetConnection(true)) + using (var statement = PrepareStatement(connection, commandText)) { - var list = new List<PersonInfo>(); + // Run this again to bind the params + GetPeopleWhereClauses(query, statement); - using (var statement = PrepareStatement(connection, commandText)) + foreach (var row in statement.ExecuteQuery()) { - // Run this again to bind the params - GetPeopleWhereClauses(query, statement); - - foreach (var row in statement.ExecuteQuery()) - { - list.Add(GetPerson(row)); - } + list.Add(GetPerson(row)); } - - return list; } + + return list; } private List<string> GetPeopleWhereClauses(InternalPeopleQuery query, IStatement statement) { var whereClauses = new List<string>(); - if (query.User != null && query.IsFavorite.HasValue) + if (query.User is not null && query.IsFavorite.HasValue) { whereClauses.Add(@"p.Name IN ( SELECT Name FROM TypedBaseItems WHERE UserDataKey IN ( @@ -5098,8 +5004,6 @@ AND Type = @InternalPersonType)"); { CheckDisposed(); - var now = DateTime.UtcNow; - var stringBuilder = new StringBuilder("Select Value From ItemValues where Type", 128); if (itemValueTypes.Length == 1) { @@ -5131,6 +5035,7 @@ AND Type = @InternalPersonType)"); var commandText = stringBuilder.ToString(); var list = new List<string>(); + using (new QueryTimeLogger(Logger, commandText)) using (var connection = GetConnection(true)) using (var statement = PrepareStatement(connection, commandText)) { @@ -5143,7 +5048,6 @@ AND Type = @InternalPersonType)"); } } - LogQueryTime("GetItemValueNames", commandText, now); return list; } @@ -5158,8 +5062,6 @@ AND Type = @InternalPersonType)"); CheckDisposed(); - var now = DateTime.UtcNow; - var typeClause = itemValueTypes.Length == 1 ? ("Type=" + itemValueTypes[0]) : ("Type in (" + string.Join(',', itemValueTypes) + ")"); @@ -5280,7 +5182,7 @@ AND Type = @InternalPersonType)"); .Append(" group by PresentationUniqueKey"); if (query.OrderBy.Count != 0 - || query.SimilarTo != null + || query.SimilarTo is not null || !string.IsNullOrEmpty(query.SearchTerm)) { stringBuilder.Append(GetOrderByText(query)); @@ -5333,6 +5235,7 @@ AND Type = @InternalPersonType)"); var list = new List<(BaseItem, ItemCounts)>(); var result = new QueryResult<(BaseItem, ItemCounts)>(); + using (new QueryTimeLogger(Logger, commandText)) using (var connection = GetConnection(true)) { connection.RunInTransaction( @@ -5348,7 +5251,7 @@ AND Type = @InternalPersonType)"); statement.TryBind("@UserId", query.User.InternalId); } - if (typeSubQuery != null) + if (typeSubQuery is not null) { GetWhereClauses(typeSubQuery, null); } @@ -5369,7 +5272,7 @@ AND Type = @InternalPersonType)"); foreach (var row in statement.ExecuteQuery()) { var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields); - if (item != null) + if (item is not null) { var countStartColumn = columns.Count - 1; @@ -5389,7 +5292,7 @@ AND Type = @InternalPersonType)"); statement.TryBind("@UserId", query.User.InternalId); } - if (typeSubQuery != null) + if (typeSubQuery is not null) { GetWhereClauses(typeSubQuery, null); } @@ -5406,8 +5309,6 @@ AND Type = @InternalPersonType)"); ReadTransactionMode); } - LogQueryTime("GetItemValues", commandText, now); - if (result.TotalRecordCount == 0) { result.TotalRecordCount = list.Count; @@ -5492,6 +5393,9 @@ AND Type = @InternalPersonType)"); list.AddRange(inheritedTags.Select(i => (6, i))); + // Remove all invalid values. + list.RemoveAll(i => string.IsNullOrEmpty(i.Item2)); + return list; } @@ -5629,7 +5533,7 @@ AND Type = @InternalPersonType)"); statement.TryBind("@Name" + index, person.Name); statement.TryBind("@Role" + index, person.Role); - statement.TryBind("@PersonType" + index, person.Type); + statement.TryBind("@PersonType" + index, person.Type.ToString()); statement.TryBind("@SortOrder" + index, person.SortOrder); statement.TryBind("@ListOrder" + index, listIndex); @@ -5658,9 +5562,10 @@ AND Type = @InternalPersonType)"); item.Role = role; } - if (reader.TryGetString(3, out var type)) + if (reader.TryGetString(3, out var type) + && Enum.TryParse(type, true, out PersonKind personKind)) { - item.Type = type; + item.Type = personKind; } if (reader.TryGetInt32(4, out var sortOrder)) @@ -6232,5 +6137,48 @@ AND Type = @InternalPersonType)"); return item; } + +#nullable enable + + private readonly struct QueryTimeLogger : IDisposable + { + private readonly ILogger _logger; + private readonly string _commandText; + private readonly string _methodName; + private readonly long _startTimestamp; + + public QueryTimeLogger(ILogger logger, string commandText, [CallerMemberName] string methodName = "") + { + _logger = logger; + _commandText = commandText; + _methodName = methodName; + _startTimestamp = logger.IsEnabled(LogLevel.Debug) ? Stopwatch.GetTimestamp() : -1; + } + + public void Dispose() + { + if (_startTimestamp == -1) + { + return; + } + + var elapsedMs = Stopwatch.GetElapsedTime(_startTimestamp).TotalMilliseconds; + +#if DEBUG + const int SlowThreshold = 100; +#else + const int SlowThreshold = 10; +#endif + + if (elapsedMs >= SlowThreshold) + { + _logger.LogDebug( + "{Method} query time (slow): {ElapsedMs}ms. Query: {Query}", + _methodName, + elapsedMs, + _commandText); + } + } + } } } diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs index 8d78d644d..a1e217ad1 100644 --- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs @@ -7,7 +7,7 @@ using System.Collections.Generic; using System.IO; using System.Threading; using Jellyfin.Data.Entities; -using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; @@ -18,33 +18,32 @@ namespace Emby.Server.Implementations.Data { public class SqliteUserDataRepository : BaseSqliteRepository, IUserDataRepository { + private readonly IUserManager _userManager; + public SqliteUserDataRepository( ILogger<SqliteUserDataRepository> logger, - IApplicationPaths appPaths) + IServerConfigurationManager config, + IUserManager userManager) : base(logger) { - DbFilePath = Path.Combine(appPaths.DataPath, "library.db"); + _userManager = userManager; + + DbFilePath = Path.Combine(config.ApplicationPaths.DataPath, "library.db"); } /// <summary> /// Opens the connection to the database. /// </summary> - /// <param name="userManager">The user manager.</param> - /// <param name="dbLock">The lock to use for database IO.</param> - /// <param name="dbConnection">The connection to use for database IO.</param> - public void Initialize(IUserManager userManager, SemaphoreSlim dbLock, SQLiteDatabaseConnection dbConnection) + public override void Initialize() { - WriteLock.Dispose(); - WriteLock = dbLock; - WriteConnection?.Dispose(); - WriteConnection = dbConnection; + base.Initialize(); using (var connection = GetConnection()) { var userDatasTableExists = TableExists(connection, "UserDatas"); var userDataTableExists = TableExists(connection, "userdata"); - var users = userDatasTableExists ? null : userManager.Users; + var users = userDatasTableExists ? null : _userManager.Users; connection.RunInTransaction( db => @@ -140,10 +139,7 @@ namespace Emby.Server.Implementations.Data throw new ArgumentNullException(nameof(userId)); } - if (string.IsNullOrEmpty(key)) - { - throw new ArgumentNullException(nameof(key)); - } + ArgumentException.ThrowIfNullOrEmpty(key); PersistUserData(userId, key, userData, cancellationToken); } @@ -274,10 +270,7 @@ namespace Emby.Server.Implementations.Data throw new ArgumentNullException(nameof(userId)); } - if (string.IsNullOrEmpty(key)) - { - throw new ArgumentNullException(nameof(key)); - } + ArgumentException.ThrowIfNullOrEmpty(key); using (var connection = GetConnection(true)) { @@ -377,20 +370,5 @@ namespace Emby.Server.Implementations.Data return userData; } - -#pragma warning disable CA2215 - /// <inheritdoc/> - /// <remarks> - /// There is nothing to dispose here since <see cref="BaseSqliteRepository.WriteLock"/> and - /// <see cref="BaseSqliteRepository.WriteConnection"/> are managed by <see cref="SqliteItemRepository"/>. - /// See <see cref="Initialize(IUserManager, SemaphoreSlim, SQLiteDatabaseConnection)"/>. - /// </remarks> - protected override void Dispose(bool dispose) - { - // The write lock and connection for the item repository are shared with the user data repository - // since they point to the same database. The item repo has responsibility for disposing these two objects, - // so the user data repo should not attempt to dispose them as well - } -#pragma warning restore CA2215 } } diff --git a/Emby.Server.Implementations/Data/TypeMapper.cs b/Emby.Server.Implementations/Data/TypeMapper.cs index 064664e1f..9efcea842 100644 --- a/Emby.Server.Implementations/Data/TypeMapper.cs +++ b/Emby.Server.Implementations/Data/TypeMapper.cs @@ -23,14 +23,11 @@ namespace Emby.Server.Implementations.Data /// <exception cref="ArgumentNullException"><c>typeName</c> is null.</exception> public Type? GetType(string typeName) { - if (string.IsNullOrEmpty(typeName)) - { - throw new ArgumentNullException(nameof(typeName)); - } + ArgumentException.ThrowIfNullOrEmpty(typeName); return _typeMap.GetOrAdd(typeName, k => AppDomain.CurrentDomain.GetAssemblies() .Select(a => a.GetType(k)) - .FirstOrDefault(t => t != null)); + .FirstOrDefault(t => t is not null)); } } } diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index f6d37421a..8fa2f0566 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -7,7 +7,6 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; -using Jellyfin.Api.Helpers; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Extensions; @@ -83,22 +82,23 @@ namespace Emby.Server.Implementations.Dto /// <inheritdoc /> public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null) { - var returnItems = new BaseItemDto[items.Count]; - var programTuples = new List<(BaseItem, BaseItemDto)>(); - var channelTuples = new List<(BaseItemDto, LiveTvChannel)>(); + var accessibleItems = user is null ? items : items.Where(x => x.IsVisible(user)).ToList(); + var returnItems = new BaseItemDto[accessibleItems.Count]; + List<(BaseItem, BaseItemDto)> programTuples = null; + List<(BaseItemDto, LiveTvChannel)> channelTuples = null; - for (int index = 0; index < items.Count; index++) + for (int index = 0; index < accessibleItems.Count; index++) { - var item = items[index]; + var item = accessibleItems[index]; var dto = GetBaseItemDtoInternal(item, options, user, owner); if (item is LiveTvChannel tvChannel) { - channelTuples.Add((dto, tvChannel)); + (channelTuples ??= new()).Add((dto, tvChannel)); } else if (item is LiveTvProgram) { - programTuples.Add((item, dto)); + (programTuples ??= new()).Add((item, dto)); } if (item is IItemByName byName) @@ -121,12 +121,12 @@ namespace Emby.Server.Implementations.Dto returnItems[index] = dto; } - if (programTuples.Count > 0) + if (programTuples is not null) { LivetvManager.AddInfoToProgramDto(programTuples, options.Fields, user).GetAwaiter().GetResult(); } - if (channelTuples.Count > 0) + if (channelTuples is not null) { LivetvManager.AddChannelInfo(channelTuples, options, user); } @@ -213,7 +213,7 @@ namespace Emby.Server.Implementations.Dto dto.DisplayPreferencesId = item.DisplayPreferencesId.ToString("N", CultureInfo.InvariantCulture); } - if (user != null) + if (user is not null) { AttachUserSpecificInfo(dto, item, user, options); } @@ -235,14 +235,14 @@ namespace Emby.Server.Implementations.Dto if (options.ContainsField(ItemFields.CanDelete)) { - dto.CanDelete = user == null + dto.CanDelete = user is null ? item.CanDelete() : item.CanDelete(user); } if (options.ContainsField(ItemFields.CanDownload)) { - dto.CanDownload = user == null + dto.CanDownload = user is null ? item.CanDownload() : item.CanDownload(user); } @@ -254,7 +254,7 @@ namespace Emby.Server.Implementations.Dto var liveTvManager = LivetvManager; var activeRecording = liveTvManager.GetActiveRecordingInfo(item.Path); - if (activeRecording != null) + if (activeRecording is not null) { dto.Type = BaseItemKind.Recording; dto.CanDownload = false; @@ -317,7 +317,7 @@ namespace Emby.Server.Implementations.Dto { var dto = GetBaseItemDtoInternal(item, options, user); - if (taggedItems != null && options.ContainsField(ItemFields.ItemCounts)) + if (taggedItems is not null && options.ContainsField(ItemFields.ItemCounts)) { SetItemByNameInfo(item, dto, taggedItems); } @@ -417,7 +417,7 @@ namespace Emby.Server.Implementations.Dto if (options.ContainsField(ItemFields.BasicSyncInfo)) { - var userCanSync = user != null && user.HasPermission(PermissionKind.EnableContentDownloading); + var userCanSync = user is not null && user.HasPermission(PermissionKind.EnableContentDownloading); if (userCanSync && item.SupportsExternalTransfer) { dto.SupportsSync = true; @@ -460,7 +460,7 @@ namespace Emby.Server.Implementations.Dto var album = item.AlbumEntity; - if (album != null) + if (album is not null) { dto.Album = album.Name; dto.AlbumId = album.Id; @@ -491,7 +491,7 @@ namespace Emby.Server.Implementations.Dto { return images .Select(p => GetImageCacheTag(item, p)) - .Where(i => i != null) + .Where(i => i is not null) .ToArray(); } @@ -522,32 +522,32 @@ namespace Emby.Server.Implementations.Dto var people = _libraryManager.GetPeople(item).OrderBy(i => i.SortOrder ?? int.MaxValue) .ThenBy(i => { - if (i.IsType(PersonType.Actor)) + if (i.IsType(PersonKind.Actor)) { return 0; } - if (i.IsType(PersonType.GuestStar)) + if (i.IsType(PersonKind.GuestStar)) { return 1; } - if (i.IsType(PersonType.Director)) + if (i.IsType(PersonKind.Director)) { return 2; } - if (i.IsType(PersonType.Writer)) + if (i.IsType(PersonKind.Writer)) { return 3; } - if (i.IsType(PersonType.Producer)) + if (i.IsType(PersonKind.Producer)) { return 4; } - if (i.IsType(PersonType.Composer)) + if (i.IsType(PersonKind.Composer)) { return 4; } @@ -570,12 +570,9 @@ namespace Emby.Server.Implementations.Dto _logger.LogError(ex, "Error getting person {Name}", c); return null; } - }).Where(i => i != null) - .Where(i => user == null ? - true : - i.IsVisible(user)) - .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase) - .Select(x => x.First()) + }).Where(i => i is not null) + .Where(i => user is null || i.IsVisible(user)) + .DistinctBy(x => x.Name, StringComparer.OrdinalIgnoreCase) .ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase); for (var i = 0; i < people.Count; i++) @@ -593,13 +590,13 @@ namespace Emby.Server.Implementations.Dto { baseItemPerson.PrimaryImageTag = GetTagAndFillBlurhash(dto, entity, ImageType.Primary); baseItemPerson.Id = entity.Id; - if (dto.ImageBlurHashes != null) + if (dto.ImageBlurHashes is not null) { // Only add BlurHash for the person's image. baseItemPerson.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>(); foreach (var (imageType, blurHash) in dto.ImageBlurHashes) { - if (blurHash != null) + if (blurHash is not null) { baseItemPerson.ImageBlurHashes[imageType] = new Dictionary<string, string>(); foreach (var (imageId, blurHashValue) in blurHash) @@ -662,7 +659,7 @@ namespace Emby.Server.Implementations.Dto private string GetTagAndFillBlurhash(BaseItemDto dto, BaseItem item, ImageType imageType, int imageIndex = 0) { var image = item.GetImageInfo(imageType, imageIndex); - if (image != null) + if (image is not null) { return GetTagAndFillBlurhash(dto, item, image); } @@ -782,7 +779,7 @@ namespace Emby.Server.Implementations.Dto { var tag = GetTagAndFillBlurhash(dto, item, image); - if (tag != null) + if (tag is not null) { dto.ImageTags[image.Type] = tag; } @@ -917,7 +914,7 @@ namespace Emby.Server.Implementations.Dto var albumParent = audio.AlbumEntity; - if (albumParent != null) + if (albumParent is not null) { dto.AlbumId = albumParent.Id; dto.AlbumPrimaryImageTag = GetTagAndFillBlurhash(dto, albumParent, ImageType.Primary); @@ -967,7 +964,7 @@ namespace Emby.Server.Implementations.Dto { EnableImages = false }); - if (artist != null) + if (artist is not null) { return new NameGuidPair { @@ -977,7 +974,7 @@ namespace Emby.Server.Implementations.Dto } return null; - }).Where(i => i != null).ToArray(); + }).Where(i => i is not null).ToArray(); } if (item is IHasAlbumArtist hasAlbumArtist) @@ -1016,7 +1013,7 @@ namespace Emby.Server.Implementations.Dto { EnableImages = false }); - if (artist != null) + if (artist is not null) { return new NameGuidPair { @@ -1026,7 +1023,7 @@ namespace Emby.Server.Implementations.Dto } return null; - }).Where(i => i != null).ToArray(); + }).Where(i => i is not null).ToArray(); } // Add video info @@ -1073,7 +1070,7 @@ namespace Emby.Server.Implementations.Dto { MediaStream[] mediaStreams; - if (dto.MediaSources != null && dto.MediaSources.Length > 0) + if (dto.MediaSources is not null && dto.MediaSources.Length > 0) { if (item.SourceType == SourceType.Channel) { @@ -1140,10 +1137,10 @@ namespace Emby.Server.Implementations.Dto // if (options.ContainsField(ItemFields.SeriesPrimaryImage)) { episodeSeries ??= episode.Series; - if (episodeSeries != null) + if (episodeSeries is not null) { dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, episodeSeries, ImageType.Primary); - if (dto.ImageTags == null || !dto.ImageTags.ContainsKey(ImageType.Primary)) + if (dto.ImageTags is null || !dto.ImageTags.ContainsKey(ImageType.Primary)) { AttachPrimaryImageAspectRatio(dto, episodeSeries); } @@ -1153,7 +1150,7 @@ namespace Emby.Server.Implementations.Dto if (options.ContainsField(ItemFields.SeriesStudio)) { episodeSeries ??= episode.Series; - if (episodeSeries != null) + if (episodeSeries is not null) { dto.SeriesStudio = episodeSeries.Studios.FirstOrDefault(); } @@ -1179,7 +1176,7 @@ namespace Emby.Server.Implementations.Dto if (options.ContainsField(ItemFields.SeriesStudio)) { series ??= season.Series; - if (series != null) + if (series is not null) { dto.SeriesStudio = series.Studios.FirstOrDefault(); } @@ -1190,10 +1187,10 @@ namespace Emby.Server.Implementations.Dto // if (options.ContainsField(ItemFields.SeriesPrimaryImage)) { series ??= season.Series; - if (series != null) + if (series is not null) { dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, series, ImageType.Primary); - if (dto.ImageTags == null || !dto.ImageTags.ContainsKey(ImageType.Primary)) + if (dto.ImageTags is null || !dto.ImageTags.ContainsKey(ImageType.Primary)) { AttachPrimaryImageAspectRatio(dto, series); } @@ -1256,7 +1253,7 @@ namespace Emby.Server.Implementations.Dto if (item.SourceType == SourceType.Channel) { var channel = _libraryManager.GetItemById(item.ChannelId); - if (channel != null) + if (channel is not null) { dto.ChannelName = channel.Name; } @@ -1268,7 +1265,7 @@ namespace Emby.Server.Implementations.Dto if (currentItem is MusicAlbum musicAlbum) { var artist = musicAlbum.GetMusicArtist(new DtoOptions(false)); - if (artist != null) + if (artist is not null) { return artist; } @@ -1276,7 +1273,7 @@ namespace Emby.Server.Implementations.Dto var parent = currentItem.DisplayParent ?? currentItem.GetOwner() ?? currentItem.GetParent(); - if (parent == null && originalItem is not UserRootFolder && originalItem is not UserView && originalItem is not AggregateFolder && originalItem is not ICollectionFolder && originalItem is not Channel) + if (parent is null && originalItem is not UserRootFolder && originalItem is not UserView && originalItem is not AggregateFolder && originalItem is not ICollectionFolder && originalItem is not Channel) { parent = _libraryManager.GetCollectionFolders(originalItem).FirstOrDefault(); } @@ -1309,53 +1306,53 @@ namespace Emby.Server.Implementations.Dto var imageTags = dto.ImageTags; - while ((!(imageTags != null && imageTags.ContainsKey(ImageType.Logo)) && logoLimit > 0) - || (!(imageTags != null && imageTags.ContainsKey(ImageType.Art)) && artLimit > 0) - || (!(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && thumbLimit > 0) + while ((!(imageTags is not null && imageTags.ContainsKey(ImageType.Logo)) && logoLimit > 0) + || (!(imageTags is not null && imageTags.ContainsKey(ImageType.Art)) && artLimit > 0) + || (!(imageTags is not null && imageTags.ContainsKey(ImageType.Thumb)) && thumbLimit > 0) || parent is Series) { parent ??= isFirst ? GetImageDisplayParent(item, item) ?? owner : parent; - if (parent == null) + if (parent is null) { break; } var allImages = parent.ImageInfos; - if (logoLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Logo)) && dto.ParentLogoItemId is null) + if (logoLimit > 0 && !(imageTags is not null && imageTags.ContainsKey(ImageType.Logo)) && dto.ParentLogoItemId is null) { var image = allImages.FirstOrDefault(i => i.Type == ImageType.Logo); - if (image != null) + if (image is not null) { dto.ParentLogoItemId = parent.Id; dto.ParentLogoImageTag = GetTagAndFillBlurhash(dto, parent, image); } } - if (artLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Art)) && dto.ParentArtItemId is null) + if (artLimit > 0 && !(imageTags is not null && imageTags.ContainsKey(ImageType.Art)) && dto.ParentArtItemId is null) { var image = allImages.FirstOrDefault(i => i.Type == ImageType.Art); - if (image != null) + if (image is not null) { dto.ParentArtItemId = parent.Id; dto.ParentArtImageTag = GetTagAndFillBlurhash(dto, parent, image); } } - if (thumbLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && (dto.ParentThumbItemId is null || parent is Series) && parent is not ICollectionFolder && parent is not UserView) + if (thumbLimit > 0 && !(imageTags is not null && imageTags.ContainsKey(ImageType.Thumb)) && (dto.ParentThumbItemId is null || parent is Series) && parent is not ICollectionFolder && parent is not UserView) { var image = allImages.FirstOrDefault(i => i.Type == ImageType.Thumb); - if (image != null) + if (image is not null) { dto.ParentThumbItemId = parent.Id; dto.ParentThumbImageTag = GetTagAndFillBlurhash(dto, parent, image); } } - if (backdropLimit > 0 && !((dto.BackdropImageTags != null && dto.BackdropImageTags.Length > 0) || (dto.ParentBackdropImageTags != null && dto.ParentBackdropImageTags.Length > 0))) + if (backdropLimit > 0 && !((dto.BackdropImageTags is not null && dto.BackdropImageTags.Length > 0) || (dto.ParentBackdropImageTags is not null && dto.ParentBackdropImageTags.Length > 0))) { var images = allImages.Where(i => i.Type == ImageType.Backdrop).Take(backdropLimit).ToList(); @@ -1403,7 +1400,7 @@ namespace Emby.Server.Implementations.Dto { var imageInfo = item.GetImageInfo(ImageType.Primary, 0); - if (imageInfo == null) + if (imageInfo is null) { return null; } diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index e0f129c3d..b8655c760 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -7,7 +7,6 @@ <ItemGroup> <ProjectReference Include="..\Emby.Naming\Emby.Naming.csproj" /> - <ProjectReference Include="..\Emby.Notifications\Emby.Notifications.csproj" /> <ProjectReference Include="..\Jellyfin.Api\Jellyfin.Api.csproj" /> <ProjectReference Include="..\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj" /> <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" /> @@ -18,22 +17,22 @@ <ProjectReference Include="..\Emby.Dlna\Emby.Dlna.csproj" /> <ProjectReference Include="..\MediaBrowser.LocalMetadata\MediaBrowser.LocalMetadata.csproj" /> <ProjectReference Include="..\Emby.Photos\Emby.Photos.csproj" /> - <ProjectReference Include="..\Emby.Drawing\Emby.Drawing.csproj" /> + <ProjectReference Include="..\src\Jellyfin.Drawing\Jellyfin.Drawing.csproj" /> <ProjectReference Include="..\MediaBrowser.MediaEncoding\MediaBrowser.MediaEncoding.csproj" /> </ItemGroup> <ItemGroup> - <PackageReference Include="DiscUtils.Udf" Version="0.16.13" /> - <PackageReference Include="Jellyfin.XmlTv" Version="10.8.0" /> - <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" /> - <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" /> - <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.9" /> - <PackageReference Include="Mono.Nat" Version="3.0.4" /> - <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.3.0" /> - <PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" /> - <PackageReference Include="DotNet.Glob" Version="3.1.3" /> + <PackageReference Include="DiscUtils.Udf" /> + <PackageReference Include="Jellyfin.XmlTv" /> + <PackageReference Include="Microsoft.Extensions.DependencyInjection" /> + <PackageReference Include="Microsoft.Extensions.Caching.Memory" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" /> + <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" /> + <PackageReference Include="Mono.Nat" /> + <PackageReference Include="prometheus-net.DotNetRuntime" /> + <PackageReference Include="SQLitePCL.pretty.netstandard" /> + <PackageReference Include="DotNet.Glob" /> </ItemGroup> <ItemGroup> @@ -41,7 +40,7 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>net6.0</TargetFramework> + <TargetFramework>net7.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 --> @@ -49,18 +48,18 @@ </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> - <TreatWarningsAsErrors>false</TreatWarningsAsErrors> + <CodeAnalysisTreatWarningsAsErrors>false</CodeAnalysisTreatWarningsAsErrors> </PropertyGroup> <!-- Code Analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3"> + <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> - <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" /> - <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> + <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" /> + <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" /> </ItemGroup> <ItemGroup> diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs index 9e35d83aa..2e3988f9e 100644 --- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs @@ -115,7 +115,7 @@ namespace Emby.Server.Implementations.EntryPoints { } - var collectionFolders = _libraryManager.GetCollectionFolders(item).ToList(); + var collectionFolders = _libraryManager.GetCollectionFolders(item); foreach (var collectionFolder in collectionFolders) { @@ -191,7 +191,7 @@ namespace Emby.Server.Implementations.EntryPoints lock (_libraryChangedSyncLock) { - if (LibraryUpdateTimer == null) + if (LibraryUpdateTimer is null) { LibraryUpdateTimer = new Timer( LibraryUpdateTimerCallback, @@ -227,7 +227,7 @@ namespace Emby.Server.Implementations.EntryPoints lock (_libraryChangedSyncLock) { - if (LibraryUpdateTimer == null) + if (LibraryUpdateTimer is null) { LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration, Timeout.Infinite); } @@ -254,7 +254,7 @@ namespace Emby.Server.Implementations.EntryPoints lock (_libraryChangedSyncLock) { - if (LibraryUpdateTimer == null) + if (LibraryUpdateTimer is null) { LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration, Timeout.Infinite); } @@ -276,30 +276,33 @@ namespace Emby.Server.Implementations.EntryPoints /// Libraries the update timer callback. /// </summary> /// <param name="state">The state.</param> - private void LibraryUpdateTimerCallback(object state) + private async void LibraryUpdateTimerCallback(object state) { + List<Folder> foldersAddedTo; + List<Folder> foldersRemovedFrom; + List<BaseItem> itemsUpdated; + List<BaseItem> itemsAdded; + List<BaseItem> itemsRemoved; lock (_libraryChangedSyncLock) { // Remove dupes in case some were saved multiple times - var foldersAddedTo = _foldersAddedTo - .GroupBy(x => x.Id) - .Select(x => x.First()) + foldersAddedTo = _foldersAddedTo + .DistinctBy(x => x.Id) .ToList(); - var foldersRemovedFrom = _foldersRemovedFrom - .GroupBy(x => x.Id) - .Select(x => x.First()) + foldersRemovedFrom = _foldersRemovedFrom + .DistinctBy(x => x.Id) .ToList(); - var itemsUpdated = _itemsUpdated + itemsUpdated = _itemsUpdated .Where(i => !_itemsAdded.Contains(i)) - .GroupBy(x => x.Id) - .Select(x => x.First()) + .DistinctBy(x => x.Id) .ToList(); - SendChangeNotifications(_itemsAdded.ToList(), itemsUpdated, _itemsRemoved.ToList(), foldersAddedTo, foldersRemovedFrom, CancellationToken.None).GetAwaiter().GetResult(); + itemsAdded = _itemsAdded.ToList(); + itemsRemoved = _itemsRemoved.ToList(); - if (LibraryUpdateTimer != null) + if (LibraryUpdateTimer is not null) { LibraryUpdateTimer.Dispose(); LibraryUpdateTimer = null; @@ -311,6 +314,8 @@ namespace Emby.Server.Implementations.EntryPoints _foldersAddedTo.Clear(); _foldersRemovedFrom.Clear(); } + + await SendChangeNotifications(itemsAdded, itemsUpdated, itemsRemoved, foldersAddedTo, foldersRemovedFrom, CancellationToken.None).ConfigureAwait(false); } /// <summary> @@ -475,7 +480,7 @@ namespace Emby.Server.Implementations.EntryPoints { if (dispose) { - if (LibraryUpdateTimer != null) + if (LibraryUpdateTimer is not null) { LibraryUpdateTimer.Dispose(); LibraryUpdateTimer = null; diff --git a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs index 82c8d3ab6..d32759017 100644 --- a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs @@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.EntryPoints lock (_syncLock) { - if (_updateTimer == null) + if (_updateTimer is null) { _updateTimer = new Timer( UpdateTimerCallback, @@ -75,11 +75,11 @@ namespace Emby.Server.Implementations.EntryPoints var baseItem = e.Item; // Go up one level for indicators - if (baseItem != null) + if (baseItem is not null) { var parent = baseItem.GetOwner() ?? baseItem.GetParent(); - if (parent != null) + if (parent is not null) { keys.Add(parent); } @@ -87,29 +87,30 @@ namespace Emby.Server.Implementations.EntryPoints } } - private void UpdateTimerCallback(object? state) + private async void UpdateTimerCallback(object? state) { + List<KeyValuePair<Guid, List<BaseItem>>> changes; lock (_syncLock) { // Remove dupes in case some were saved multiple times - var changes = _changedItems.ToList(); + changes = _changedItems.ToList(); _changedItems.Clear(); - SendNotifications(changes, CancellationToken.None).GetAwaiter().GetResult(); - - if (_updateTimer != null) + if (_updateTimer is not null) { _updateTimer.Dispose(); _updateTimer = null; } } + + await SendNotifications(changes, CancellationToken.None).ConfigureAwait(false); } private async Task SendNotifications(List<KeyValuePair<Guid, List<BaseItem>>> changes, CancellationToken cancellationToken) { - foreach (var pair in changes) + foreach ((var key, var value) in changes) { - await SendNotifications(pair.Key, pair.Value, cancellationToken).ConfigureAwait(false); + await SendNotifications(key, value, cancellationToken).ConfigureAwait(false); } } @@ -123,8 +124,7 @@ namespace Emby.Server.Implementations.EntryPoints var user = _userManager.GetUserById(userId); var dtoList = changedItems - .GroupBy(x => x.Id) - .Select(x => x.First()) + .DistinctBy(x => x.Id) .Select(i => { var dto = _userDataManager.GetUserDataDto(i, user); @@ -145,7 +145,7 @@ namespace Emby.Server.Implementations.EntryPoints public void Dispose() { - if (_updateTimer != null) + if (_updateTimer is not null) { _updateTimer.Dispose(); _updateTimer = null; diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs index d095248fa..b1a99853a 100644 --- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs +++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs @@ -164,7 +164,7 @@ namespace Emby.Server.Implementations.HttpServer ReadResult result = await reader.ReadAsync().ConfigureAwait(false); ReadOnlySequence<byte> buffer = result.Buffer; - if (OnReceive == null) + if (OnReceive is null) { // Tell the PipeReader how much of the buffer we have consumed reader.AdvanceTo(buffer.End); @@ -185,7 +185,7 @@ namespace Emby.Server.Implementations.HttpServer return; } - if (stub == null) + if (stub is null) { _logger.LogError("Error processing web socket message"); return; diff --git a/Emby.Server.Implementations/IO/ExtendedFileSystemInfo.cs b/Emby.Server.Implementations/IO/ExtendedFileSystemInfo.cs deleted file mode 100644 index 545d73e05..000000000 --- a/Emby.Server.Implementations/IO/ExtendedFileSystemInfo.cs +++ /dev/null @@ -1,13 +0,0 @@ -#pragma warning disable CS1591 - -namespace Emby.Server.Implementations.IO -{ - public class ExtendedFileSystemInfo - { - public bool IsHidden { get; set; } - - public bool IsReadOnly { get; set; } - - public bool Exists { get; set; } - } -} diff --git a/Emby.Server.Implementations/IO/FileRefresher.cs b/Emby.Server.Implementations/IO/FileRefresher.cs index 6326208f7..0ad81b653 100644 --- a/Emby.Server.Implementations/IO/FileRefresher.cs +++ b/Emby.Server.Implementations/IO/FileRefresher.cs @@ -40,10 +40,7 @@ namespace Emby.Server.Implementations.IO private void AddAffectedPath(string path) { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException(nameof(path)); - } + ArgumentException.ThrowIfNullOrEmpty(path); if (!_affectedPaths.Contains(path, StringComparer.Ordinal)) { @@ -53,10 +50,7 @@ namespace Emby.Server.Implementations.IO public void AddPath(string path) { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException(nameof(path)); - } + ArgumentException.ThrowIfNullOrEmpty(path); lock (_timerLock) { @@ -80,7 +74,7 @@ namespace Emby.Server.Implementations.IO return; } - if (_timer == null) + if (_timer is null) { _timer = new Timer(OnTimerCallback, null, TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryMonitorDelay), TimeSpan.FromMilliseconds(-1)); } @@ -138,9 +132,8 @@ namespace Emby.Server.Implementations.IO IEnumerable<BaseItem> itemsToRefresh = paths .Distinct(StringComparer.OrdinalIgnoreCase) .Select(GetAffectedBaseItem) - .Where(item => item != null) - .GroupBy(x => x!.Id) // Removed null values in the previous .Where() - .Select(x => x.First())!; + .Where(item => item is not null) + .DistinctBy(x => x!.Id)!; // Removed null values in the previous .Where() foreach (var item in itemsToRefresh) { @@ -178,21 +171,21 @@ namespace Emby.Server.Implementations.IO { BaseItem? item = null; - while (item == null && !string.IsNullOrEmpty(path)) + while (item is null && !string.IsNullOrEmpty(path)) { item = _libraryManager.FindByPath(path, null); path = System.IO.Path.GetDirectoryName(path) ?? string.Empty; } - if (item != null) + if (item is not null) { // If the item has been deleted find the first valid parent that still exists while (!Directory.Exists(item.Path) && !File.Exists(item.Path)) { item = item.GetOwner() ?? item.GetParent(); - if (item == null) + if (item is null) { break; } @@ -206,7 +199,7 @@ namespace Emby.Server.Implementations.IO { lock (_timerLock) { - if (_timer != null) + if (_timer is not null) { _timer.Dispose(); _timer = null; diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs index 657daac3f..f67a02be8 100644 --- a/Emby.Server.Implementations/IO/LibraryMonitor.cs +++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs @@ -71,28 +71,14 @@ namespace Emby.Server.Implementations.IO public void ReportFileSystemChangeBeginning(string path) { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException(nameof(path)); - } + ArgumentException.ThrowIfNullOrEmpty(path); TemporarilyIgnore(path); } - public bool IsPathLocked(string path) - { - // This method is not used by the core but it used by auto-organize - - var lockedPaths = _tempIgnoredPaths.Keys.ToList(); - return lockedPaths.Any(i => _fileSystem.AreEqual(i, path) || _fileSystem.ContainsSubPath(i, path)); - } - public async void ReportFileSystemChangeComplete(string path, bool refreshPath) { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException(nameof(path)); - } + ArgumentException.ThrowIfNullOrEmpty(path); // This is an arbitrary amount of time, but delay it because file system writes often trigger events long after the file was actually written to. // Seeing long delays in some situations, especially over the network, sometimes up to 45 seconds @@ -123,7 +109,7 @@ namespace Emby.Server.Implementations.IO var options = _libraryManager.GetLibraryOptions(item); - if (options != null) + if (options is not null) { return options.EnableRealtimeMonitor; } @@ -145,8 +131,7 @@ namespace Emby.Server.Implementations.IO .OfType<Folder>() .SelectMany(f => f.PhysicalLocations) .Distinct(StringComparer.OrdinalIgnoreCase) - .OrderBy(i => i) - .ToList(); + .Order(); foreach (var path in paths) { @@ -206,10 +191,7 @@ namespace Emby.Server.Implementations.IO /// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c>.</exception> private static bool ContainsParentFolder(IEnumerable<string> lst, string path) { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException(nameof(path)); - } + ArgumentException.ThrowIfNullOrEmpty(path); path = path.TrimEnd(Path.DirectorySeparatorChar); @@ -365,18 +347,12 @@ namespace Emby.Server.Implementations.IO public void ReportFileSystemChanged(string path) { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException(nameof(path)); - } + ArgumentException.ThrowIfNullOrEmpty(path); var monitorPath = !IgnorePatterns.ShouldIgnore(path); - // Ignore certain files - var tempIgnorePaths = _tempIgnoredPaths.Keys.ToList(); - - // If the parent of an ignored path has a change event, ignore that too - if (tempIgnorePaths.Any(i => + // Ignore certain files, If the parent of an ignored path has a change event, ignore that too + if (_tempIgnoredPaths.Keys.Any(i => { if (_fileSystem.AreEqual(i, path)) { @@ -491,7 +467,7 @@ namespace Emby.Server.Implementations.IO { lock (_activeRefreshers) { - foreach (var refresher in _activeRefreshers.ToList()) + foreach (var refresher in _activeRefreshers) { refresher.Completed -= OnNewRefresherCompleted; refresher.Dispose(); diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 120b1812a..1fffdfbfa 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -48,10 +48,7 @@ namespace Emby.Server.Implementations.IO /// <exception cref="ArgumentNullException"><paramref name="filename"/> is <c>null</c>.</exception> public virtual bool IsShortcut(string filename) { - if (string.IsNullOrEmpty(filename)) - { - throw new ArgumentNullException(nameof(filename)); - } + ArgumentException.ThrowIfNullOrEmpty(filename); var extension = Path.GetExtension(filename); return _shortcutHandlers.Any(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase)); @@ -65,10 +62,7 @@ namespace Emby.Server.Implementations.IO /// <exception cref="ArgumentNullException"><paramref name="filename"/> is <c>null</c>.</exception> public virtual string? ResolveShortcut(string filename) { - if (string.IsNullOrEmpty(filename)) - { - throw new ArgumentNullException(nameof(filename)); - } + ArgumentException.ThrowIfNullOrEmpty(filename); var extension = Path.GetExtension(filename); var handler = _shortcutHandlers.Find(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase)); @@ -136,20 +130,13 @@ namespace Emby.Server.Implementations.IO /// <exception cref="ArgumentNullException">The shortcutPath or target is null.</exception> public virtual void CreateShortcut(string shortcutPath, string target) { - if (string.IsNullOrEmpty(shortcutPath)) - { - throw new ArgumentNullException(nameof(shortcutPath)); - } - - if (string.IsNullOrEmpty(target)) - { - throw new ArgumentNullException(nameof(target)); - } + ArgumentException.ThrowIfNullOrEmpty(shortcutPath); + ArgumentException.ThrowIfNullOrEmpty(target); var extension = Path.GetExtension(shortcutPath); var handler = _shortcutHandlers.Find(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase)); - if (handler != null) + if (handler is not null) { handler.Create(shortcutPath, target); } @@ -280,25 +267,6 @@ namespace Emby.Server.Implementations.IO return result; } - private static ExtendedFileSystemInfo GetExtendedFileSystemInfo(string path) - { - var result = new ExtendedFileSystemInfo(); - - var info = new FileInfo(path); - - if (info.Exists) - { - result.Exists = true; - - var attributes = info.Attributes; - - result.IsHidden = (attributes & FileAttributes.Hidden) == FileAttributes.Hidden; - result.IsReadOnly = (attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly; - } - - return result; - } - /// <summary> /// Takes a filename and removes invalid characters. /// </summary> @@ -416,19 +384,18 @@ namespace Emby.Server.Implementations.IO return; } - var info = GetExtendedFileSystemInfo(path); + var info = new FileInfo(path); - if (info.Exists && info.IsHidden != isHidden) + if (info.Exists && + ((info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) != isHidden) { if (isHidden) { - File.SetAttributes(path, File.GetAttributes(path) | FileAttributes.Hidden); + File.SetAttributes(path, info.Attributes | FileAttributes.Hidden); } else { - var attributes = File.GetAttributes(path); - attributes = RemoveAttribute(attributes, FileAttributes.Hidden); - File.SetAttributes(path, attributes); + File.SetAttributes(path, info.Attributes & ~FileAttributes.Hidden); } } } @@ -441,19 +408,20 @@ namespace Emby.Server.Implementations.IO return; } - var info = GetExtendedFileSystemInfo(path); + var info = new FileInfo(path); if (!info.Exists) { return; } - if (info.IsReadOnly == readOnly && info.IsHidden == isHidden) + if (((info.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) == readOnly + && ((info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) == isHidden) { return; } - var attributes = File.GetAttributes(path); + var attributes = info.Attributes; if (readOnly) { @@ -461,7 +429,7 @@ namespace Emby.Server.Implementations.IO } else { - attributes = RemoveAttribute(attributes, FileAttributes.ReadOnly); + attributes &= ~FileAttributes.ReadOnly; } if (isHidden) @@ -470,17 +438,12 @@ namespace Emby.Server.Implementations.IO } else { - attributes = RemoveAttribute(attributes, FileAttributes.Hidden); + attributes &= ~FileAttributes.Hidden; } File.SetAttributes(path, attributes); } - private static FileAttributes RemoveAttribute(FileAttributes attributes, FileAttributes attributesToRemove) - { - return attributes & ~attributesToRemove; - } - /// <summary> /// Swaps the files. /// </summary> @@ -488,15 +451,8 @@ namespace Emby.Server.Implementations.IO /// <param name="file2">The file2.</param> public virtual void SwapFiles(string file1, string file2) { - if (string.IsNullOrEmpty(file1)) - { - throw new ArgumentNullException(nameof(file1)); - } - - if (string.IsNullOrEmpty(file2)) - { - throw new ArgumentNullException(nameof(file2)); - } + ArgumentException.ThrowIfNullOrEmpty(file1); + ArgumentException.ThrowIfNullOrEmpty(file2); var temp1 = Path.Combine(_tempPath, Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)); @@ -514,15 +470,8 @@ namespace Emby.Server.Implementations.IO /// <inheritdoc /> public virtual bool ContainsSubPath(string parentPath, string path) { - if (string.IsNullOrEmpty(parentPath)) - { - throw new ArgumentNullException(nameof(parentPath)); - } - - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException(nameof(path)); - } + ArgumentException.ThrowIfNullOrEmpty(parentPath); + ArgumentException.ThrowIfNullOrEmpty(path); return path.Contains( Path.TrimEndingDirectorySeparator(parentPath) + Path.DirectorySeparatorChar, @@ -532,10 +481,7 @@ namespace Emby.Server.Implementations.IO /// <inheritdoc /> public virtual string NormalizePath(string path) { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException(nameof(path)); - } + ArgumentException.ThrowIfNullOrEmpty(path); if (path.EndsWith(":\\", StringComparison.OrdinalIgnoreCase)) { @@ -621,14 +567,14 @@ namespace Emby.Server.Implementations.IO // On linux and osx the search pattern is case sensitive // If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method - if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions != null && extensions.Count == 1) + if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions is not null && extensions.Count == 1) { return ToMetadata(new DirectoryInfo(path).EnumerateFiles("*" + extensions[0], enumerationOptions)); } var files = new DirectoryInfo(path).EnumerateFiles("*", enumerationOptions); - if (extensions != null && extensions.Count > 0) + if (extensions is not null && extensions.Count > 0) { files = files.Where(i => { @@ -678,14 +624,14 @@ namespace Emby.Server.Implementations.IO // On linux and osx the search pattern is case sensitive // If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method - if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions != null && extensions.Length == 1) + if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions is not null && extensions.Length == 1) { return Directory.EnumerateFiles(path, "*" + extensions[0], enumerationOptions); } var files = Directory.EnumerateFiles(path, "*", enumerationOptions); - if (extensions != null && extensions.Length > 0) + if (extensions is not null && extensions.Length > 0) { files = files.Where(i => { diff --git a/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs b/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs index 76c58d5dc..c2aab3879 100644 --- a/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs +++ b/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs @@ -19,10 +19,7 @@ namespace Emby.Server.Implementations.IO public string? Resolve(string shortcutPath) { - if (string.IsNullOrEmpty(shortcutPath)) - { - throw new ArgumentException("Shortcut path is empty or null.", nameof(shortcutPath)); - } + ArgumentException.ThrowIfNullOrEmpty(shortcutPath); if (string.Equals(Path.GetExtension(shortcutPath), ".mblink", StringComparison.OrdinalIgnoreCase)) { @@ -36,15 +33,8 @@ namespace Emby.Server.Implementations.IO public void Create(string shortcutPath, string targetPath) { - if (string.IsNullOrEmpty(shortcutPath)) - { - throw new ArgumentNullException(nameof(shortcutPath)); - } - - if (string.IsNullOrEmpty(targetPath)) - { - throw new ArgumentNullException(nameof(targetPath)); - } + ArgumentException.ThrowIfNullOrEmpty(shortcutPath); + ArgumentException.ThrowIfNullOrEmpty(targetPath); File.WriteAllText(shortcutPath, targetPath); } diff --git a/Emby.Server.Implementations/IO/StreamHelper.cs b/Emby.Server.Implementations/IO/StreamHelper.cs index f55c16d6d..6eaf22ce4 100644 --- a/Emby.Server.Implementations/IO/StreamHelper.cs +++ b/Emby.Server.Implementations/IO/StreamHelper.cs @@ -23,7 +23,7 @@ namespace Emby.Server.Implementations.IO await destination.WriteAsync(buffer.AsMemory(0, read), cancellationToken).ConfigureAwait(false); - if (onStarted != null) + if (onStarted is not null) { onStarted(); onStarted = null; diff --git a/Emby.Server.Implementations/IStartupOptions.cs b/Emby.Server.Implementations/IStartupOptions.cs index 3769ae4dd..b7bcaace1 100644 --- a/Emby.Server.Implementations/IStartupOptions.cs +++ b/Emby.Server.Implementations/IStartupOptions.cs @@ -21,16 +21,6 @@ namespace Emby.Server.Implementations string? PackageName { get; } /// <summary> - /// Gets the value of the --restartpath command line option. - /// </summary> - string? RestartPath { get; } - - /// <summary> - /// Gets the value of the --restartargs command line option. - /// </summary> - string? RestartArgs { get; } - - /// <summary> /// Gets the value of the --published-server-url command line option. /// </summary> string? PublishedServerUrl { get; } diff --git a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs index 57c2f1a5e..0a3d740cc 100644 --- a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs +++ b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs @@ -81,7 +81,7 @@ namespace Emby.Server.Implementations.Images { var image = item.GetImageInfo(imageType, 0); - if (image != null) + if (image is not null) { if (!image.IsLocalFile) { @@ -143,20 +143,20 @@ namespace Emby.Server.Implementations.Images if (useBackdrop) { var backdrop = i.GetImageInfo(ImageType.Backdrop, 0); - if (backdrop != null && backdrop.IsLocalFile) + if (backdrop is not null && backdrop.IsLocalFile) { return backdrop.Path; } } var image = i.GetImageInfo(ImageType.Primary, 0); - if (image != null && image.IsLocalFile) + if (image is not null && image.IsLocalFile) { return image.Path; } image = i.GetImageInfo(ImageType.Thumb, 0); - if (image != null && image.IsLocalFile) + if (image is not null && image.IsLocalFile) { return image.Path; } @@ -268,7 +268,7 @@ namespace Emby.Server.Implementations.Images { var image = item.GetImageInfo(type, 0); - if (image != null) + if (image is not null) { if (!image.IsLocalFile) { diff --git a/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs index 6fc7f1ac3..84c21931c 100644 --- a/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs +++ b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System.Collections.Generic; diff --git a/Emby.Server.Implementations/Images/DynamicImageProvider.cs b/Emby.Server.Implementations/Images/DynamicImageProvider.cs index 0faa0f8fa..0bd5fdce0 100644 --- a/Emby.Server.Implementations/Images/DynamicImageProvider.cs +++ b/Emby.Server.Implementations/Images/DynamicImageProvider.cs @@ -52,7 +52,7 @@ namespace Emby.Server.Implementations.Images if (i is Episode episode) { var series = episode.Series; - if (series != null) + if (series is not null) { return series; } @@ -63,7 +63,7 @@ namespace Emby.Server.Implementations.Images if (i is Season season) { var series = season.Series; - if (series != null) + if (series is not null) { return series; } @@ -74,15 +74,14 @@ namespace Emby.Server.Implementations.Images if (i is Audio audio) { var album = audio.AlbumEntity; - if (album != null && album.HasImage(ImageType.Primary)) + if (album is not null && album.HasImage(ImageType.Primary)) { return album; } } return i; - }).GroupBy(x => x.Id) - .Select(x => x.First()); + }).DistinctBy(x => x.Id); List<BaseItem> returnItems; if (isUsingCollectionStrip) diff --git a/Emby.Server.Implementations/Images/FolderImageProvider.cs b/Emby.Server.Implementations/Images/FolderImageProvider.cs index 4376bd356..90f7568a9 100644 --- a/Emby.Server.Implementations/Images/FolderImageProvider.cs +++ b/Emby.Server.Implementations/Images/FolderImageProvider.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using MediaBrowser.Common.Configuration; diff --git a/Emby.Server.Implementations/Images/GenreImageProvider.cs b/Emby.Server.Implementations/Images/GenreImageProvider.cs index 968bf5fa3..c9b41f819 100644 --- a/Emby.Server.Implementations/Images/GenreImageProvider.cs +++ b/Emby.Server.Implementations/Images/GenreImageProvider.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System.Collections.Generic; diff --git a/Emby.Server.Implementations/Images/PlaylistImageProvider.cs b/Emby.Server.Implementations/Images/PlaylistImageProvider.cs index b8f0f0d65..3326d21ac 100644 --- a/Emby.Server.Implementations/Images/PlaylistImageProvider.cs +++ b/Emby.Server.Implementations/Images/PlaylistImageProvider.cs @@ -34,7 +34,7 @@ namespace Emby.Server.Implementations.Images if (subItem is Episode episode) { var series = episode.Series; - if (series != null && series.HasImage(ImageType.Primary)) + if (series is not null && series.HasImage(ImageType.Primary)) { return series; } @@ -47,7 +47,7 @@ namespace Emby.Server.Implementations.Images var parent = subItem.GetOwner() ?? subItem.GetParent(); - if (parent != null && parent.HasImage(ImageType.Primary)) + if (parent is not null && parent.HasImage(ImageType.Primary)) { if (parent is MusicAlbum) { @@ -57,9 +57,8 @@ namespace Emby.Server.Implementations.Images return null; }) - .Where(i => i != null) - .GroupBy(x => x.Id) - .Select(x => x.First()) + .Where(i => i is not null) + .DistinctBy(x => x.Id) .ToList(); } } diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs index e558fbe27..665d70a41 100644 --- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs @@ -52,7 +52,7 @@ namespace Emby.Server.Implementations.Library if (fileInfo.IsDirectory) { - if (parent != null) + if (parent is not null) { // Ignore extras folders but allow it at the collection level if (_namingOptions.AllExtrasTypesFolderNames.ContainsKey(filename) @@ -65,7 +65,7 @@ namespace Emby.Server.Implementations.Library } else { - if (parent != null) + if (parent is not null) { // Don't resolve these into audio files if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFileName, StringComparison.Ordinal) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index cef82ebbc..75a1a5a4d 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -113,6 +113,7 @@ namespace Emby.Server.Implementations.Library /// <param name="imageProcessor">The image processor.</param> /// <param name="memoryCache">The memory cache.</param> /// <param name="namingOptions">The naming options.</param> + /// <param name="directoryService">The directory service.</param> public LibraryManager( IServerApplicationHost appHost, ILoggerFactory loggerFactory, @@ -128,7 +129,8 @@ namespace Emby.Server.Implementations.Library IItemRepository itemRepository, IImageProcessor imageProcessor, IMemoryCache memoryCache, - NamingOptions namingOptions) + NamingOptions namingOptions, + IDirectoryService directoryService) { _appHost = appHost; _logger = loggerFactory.CreateLogger<LibraryManager>(); @@ -146,7 +148,7 @@ namespace Emby.Server.Implementations.Library _memoryCache = memoryCache; _namingOptions = namingOptions; - _extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions); + _extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService); _configurationManager.ConfigurationUpdated += ConfigurationUpdated; @@ -176,7 +178,7 @@ namespace Emby.Server.Implementations.Library { get { - if (_rootFolder == null) + if (_rootFolder is null) { lock (_rootFolderSyncLock) { @@ -356,8 +358,8 @@ namespace Emby.Server.Implementations.Library } var children = item.IsFolder - ? ((Folder)item).GetRecursiveChildren(false).ToList() - : new List<BaseItem>(); + ? ((Folder)item).GetRecursiveChildren(false) + : Enumerable.Empty<BaseItem>(); foreach (var metadataPath in GetMetadataPaths(item, children)) { @@ -465,9 +467,9 @@ namespace Emby.Server.Implementations.Library private BaseItem ResolveItem(ItemResolveArgs args, IItemResolver[] resolvers) { var item = (resolvers ?? EntityResolvers).Select(r => Resolve(args, r)) - .FirstOrDefault(i => i != null); + .FirstOrDefault(i => i is not null); - if (item != null) + if (item is not null) { ResolverHelper.SetInitialItemValues(item, args, _fileSystem, this); } @@ -495,11 +497,7 @@ namespace Emby.Server.Implementations.Library private Guid GetNewItemIdInternal(string key, Type type, bool forceCaseInsensitive) { - if (string.IsNullOrEmpty(key)) - { - throw new ArgumentNullException(nameof(key)); - } - + ArgumentException.ThrowIfNullOrEmpty(key); ArgumentNullException.ThrowIfNull(type); string programDataPath = _configurationManager.ApplicationPaths.ProgramDataPath; @@ -536,12 +534,12 @@ namespace Emby.Server.Implementations.Library var fullPath = fileInfo.FullName; - if (string.IsNullOrEmpty(collectionType) && parent != null) + if (string.IsNullOrEmpty(collectionType) && parent is not null) { collectionType = GetContentTypeOverride(fullPath, true); } - var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, directoryService) + var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, this) { Parent = parent, FileInfo = fileInfo, @@ -572,7 +570,7 @@ namespace Emby.Server.Implementations.Library } catch (Exception ex) { - if (parent != null && parent.IsPhysicalRoot) + if (parent is not null && parent.IsPhysicalRoot) { _logger.LogError(ex, "Error in GetFilteredFileSystemEntries isPhysicalRoot: {0} IsVf: {1}", isPhysicalRoot, isVf); @@ -654,9 +652,9 @@ namespace Emby.Server.Implementations.Library { var fileList = files.Where(i => !IgnoreFile(i, parent)).ToList(); - if (parent != null) + if (parent is not null) { - var multiItemResolvers = resolvers == null ? MultiItemResolvers : resolvers.OfType<IMultiItemResolver>().ToArray(); + var multiItemResolvers = resolvers is null ? MultiItemResolvers : resolvers.OfType<IMultiItemResolver>().ToArray(); foreach (var resolver in multiItemResolvers) { @@ -697,7 +695,7 @@ namespace Emby.Server.Implementations.Library _logger.LogError(ex, "Error resolving path {Path}", file.FullName); } - if (result != null) + if (result is not null) { yield return result; } @@ -750,7 +748,7 @@ namespace Emby.Server.Implementations.Library var dbItem = GetItemById(folder.Id) as BasePluginFolder; - if (dbItem != null && string.Equals(dbItem.Path, folder.Path, StringComparison.OrdinalIgnoreCase)) + if (dbItem is not null && string.Equals(dbItem.Path, folder.Path, StringComparison.OrdinalIgnoreCase)) { folder = dbItem; } @@ -770,11 +768,11 @@ namespace Emby.Server.Implementations.Library public Folder GetUserRootFolder() { - if (_userRootFolder == null) + if (_userRootFolder is null) { lock (_userRootFolderSyncLock) { - if (_userRootFolder == null) + if (_userRootFolder is null) { var userRootPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath; @@ -792,7 +790,7 @@ namespace Emby.Server.Implementations.Library _logger.LogError(ex, "Error creating UserRootFolder {Path}", newItemId); } - if (tmpItem == null) + if (tmpItem is null) { _logger.LogDebug("Creating new userRootFolder with DeepCopy"); tmpItem = ((Folder)ResolvePath(_fileSystem.GetDirectoryInfo(userRootPath))).DeepCopy<Folder, UserRootFolder>(); @@ -818,10 +816,7 @@ namespace Emby.Server.Implementations.Library { // If this returns multiple items it could be tricky figuring out which one is correct. // In most cases, the newest one will be and the others obsolete but not yet cleaned up - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException(nameof(path)); - } + ArgumentException.ThrowIfNullOrEmpty(path); var query = new InternalItemsQuery { @@ -952,7 +947,7 @@ namespace Emby.Server.Implementations.Library .Cast<T>() .FirstOrDefault(); - if (existing != null) + if (existing is not null) { return existing; } @@ -961,7 +956,7 @@ namespace Emby.Server.Implementations.Library var path = getPathFn(name); var id = GetItemByNameId<T>(path); var item = GetItemById(id) as T; - if (item == null) + if (item is null) { item = new T { @@ -1161,7 +1156,7 @@ namespace Emby.Server.Implementations.Library .ToList(); } - private VirtualFolderInfo GetVirtualFolderInfo(string dir, List<BaseItem> allCollectionFolders, Dictionary<Guid, Guid> refreshQueue) + private VirtualFolderInfo GetVirtualFolderInfo(string dir, List<BaseItem> allCollectionFolders, HashSet<Guid> refreshQueue) { var info = new VirtualFolderInfo { @@ -1181,30 +1176,30 @@ namespace Emby.Server.Implementations.Library return null; } }) - .Where(i => i != null) - .OrderBy(i => i) + .Where(i => i is not null) + .Order() .ToArray(), CollectionType = GetCollectionType(dir) }; var libraryFolder = allCollectionFolders.FirstOrDefault(i => string.Equals(i.Path, dir, StringComparison.OrdinalIgnoreCase)); - - if (libraryFolder != null && libraryFolder.HasImage(ImageType.Primary)) + if (libraryFolder is not null) { - info.PrimaryImageItemId = libraryFolder.Id.ToString("N", CultureInfo.InvariantCulture); - } + var libraryFolderId = libraryFolder.Id.ToString("N", CultureInfo.InvariantCulture); + info.ItemId = libraryFolderId; + if (libraryFolder.HasImage(ImageType.Primary)) + { + info.PrimaryImageItemId = libraryFolderId; + } - if (libraryFolder != null) - { - info.ItemId = libraryFolder.Id.ToString("N", CultureInfo.InvariantCulture); info.LibraryOptions = GetLibraryOptions(libraryFolder); - if (refreshQueue != null) + if (refreshQueue is not null) { info.RefreshProgress = libraryFolder.GetRefreshProgress(); - info.RefreshStatus = info.RefreshProgress.HasValue ? "Active" : refreshQueue.ContainsKey(libraryFolder.Id) ? "Queued" : "Idle"; + info.RefreshStatus = info.RefreshProgress.HasValue ? "Active" : refreshQueue.Contains(libraryFolder.Id) ? "Queued" : "Idle"; } } @@ -1245,7 +1240,7 @@ namespace Emby.Server.Implementations.Library item = RetrieveItem(id); - if (item != null) + if (item is not null) { RegisterItem(item); } @@ -1258,13 +1253,13 @@ namespace Emby.Server.Implementations.Library if (query.Recursive && !query.ParentId.Equals(default)) { var parent = GetItemById(query.ParentId); - if (parent != null) + if (parent is not null) { - SetTopParentIdsOrAncestors(query, new List<BaseItem> { parent }); + SetTopParentIdsOrAncestors(query, new[] { parent }); } } - if (query.User != null) + if (query.User is not null) { AddUserToQuery(query, query.User, allowExternalContent); } @@ -1282,13 +1277,13 @@ namespace Emby.Server.Implementations.Library if (query.Recursive && !query.ParentId.Equals(default)) { var parent = GetItemById(query.ParentId); - if (parent != null) + if (parent is not null) { - SetTopParentIdsOrAncestors(query, new List<BaseItem> { parent }); + SetTopParentIdsOrAncestors(query, new[] { parent }); } } - if (query.User != null) + if (query.User is not null) { AddUserToQuery(query, query.User); } @@ -1302,7 +1297,7 @@ namespace Emby.Server.Implementations.Library if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0) { - if (query.User != null) + if (query.User is not null) { AddUserToQuery(query, query.User); } @@ -1313,7 +1308,7 @@ namespace Emby.Server.Implementations.Library public QueryResult<BaseItem> QueryItems(InternalItemsQuery query) { - if (query.User != null) + if (query.User is not null) { AddUserToQuery(query, query.User); } @@ -1331,7 +1326,7 @@ namespace Emby.Server.Implementations.Library public List<Guid> GetItemIds(InternalItemsQuery query) { - if (query.User != null) + if (query.User is not null) { AddUserToQuery(query, query.User); } @@ -1341,7 +1336,7 @@ namespace Emby.Server.Implementations.Library public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery query) { - if (query.User != null) + if (query.User is not null) { AddUserToQuery(query, query.User); } @@ -1352,7 +1347,7 @@ namespace Emby.Server.Implementations.Library public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery query) { - if (query.User != null) + if (query.User is not null) { AddUserToQuery(query, query.User); } @@ -1363,7 +1358,7 @@ namespace Emby.Server.Implementations.Library public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery query) { - if (query.User != null) + if (query.User is not null) { AddUserToQuery(query, query.User); } @@ -1374,7 +1369,7 @@ namespace Emby.Server.Implementations.Library public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery query) { - if (query.User != null) + if (query.User is not null) { AddUserToQuery(query, query.User); } @@ -1385,7 +1380,7 @@ namespace Emby.Server.Implementations.Library public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery query) { - if (query.User != null) + if (query.User is not null) { AddUserToQuery(query, query.User); } @@ -1426,7 +1421,7 @@ namespace Emby.Server.Implementations.Library public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery query) { - if (query.User != null) + if (query.User is not null) { AddUserToQuery(query, query.User); } @@ -1440,13 +1435,13 @@ namespace Emby.Server.Implementations.Library if (query.Recursive && !query.ParentId.Equals(default)) { var parent = GetItemById(query.ParentId); - if (parent != null) + if (parent is not null) { - SetTopParentIdsOrAncestors(query, new List<BaseItem> { parent }); + SetTopParentIdsOrAncestors(query, new[] { parent }); } } - if (query.User != null) + if (query.User is not null) { AddUserToQuery(query, query.User); } @@ -1462,7 +1457,7 @@ namespace Emby.Server.Implementations.Library _itemRepository.GetItemList(query)); } - private void SetTopParentIdsOrAncestors(InternalItemsQuery query, List<BaseItem> parents) + private void SetTopParentIdsOrAncestors(InternalItemsQuery query, IReadOnlyCollection<BaseItem> parents) { if (parents.All(i => i is ICollectionFolder || i is UserView)) { @@ -1508,6 +1503,12 @@ namespace Emby.Server.Implementations.Library }); query.TopParentIds = userViews.SelectMany(i => GetTopParentIdsForQuery(i, user)).ToArray(); + + // Prevent searching in all libraries due to empty filter + if (query.TopParentIds.Length == 0) + { + query.TopParentIds = new[] { Guid.NewGuid() }; + } } } @@ -1524,7 +1525,7 @@ namespace Emby.Server.Implementations.Library if (!view.DisplayParentId.Equals(default)) { var displayParent = GetItemById(view.DisplayParentId); - if (displayParent != null) + if (displayParent is not null) { return GetTopParentIdsForQuery(displayParent, user); } @@ -1535,7 +1536,7 @@ namespace Emby.Server.Implementations.Library if (!view.ParentId.Equals(default)) { var displayParent = GetItemById(view.ParentId); - if (displayParent != null) + if (displayParent is not null) { return GetTopParentIdsForQuery(displayParent, user); } @@ -1544,7 +1545,7 @@ namespace Emby.Server.Implementations.Library } // Handle grouping - if (user != null && !string.IsNullOrEmpty(view.ViewType) && UserView.IsEligibleForGrouping(view.ViewType) + if (user is not null && !string.IsNullOrEmpty(view.ViewType) && UserView.IsEligibleForGrouping(view.ViewType) && user.GetPreference(PreferenceKind.GroupedFolders).Length > 0) { return GetUserRootFolder() @@ -1564,7 +1565,7 @@ namespace Emby.Server.Implementations.Library } var topParent = item.GetTopParent(); - if (topParent != null) + if (topParent is not null) { return new[] { topParent.Id }; } @@ -1589,7 +1590,7 @@ namespace Emby.Server.Implementations.Library return items .SelectMany(i => i.ToArray()) .Select(ResolveIntro) - .Where(i => i != null); + .Where(i => i is not null); } /// <summary> @@ -1609,7 +1610,7 @@ namespace Emby.Server.Implementations.Library { _logger.LogError(ex, "Error getting intros"); - return new List<IntroInfo>(); + return Enumerable.Empty<IntroInfo>(); } } @@ -1627,7 +1628,7 @@ namespace Emby.Server.Implementations.Library // Get an existing item by Id video = GetItemById(info.ItemId.Value) as Video; - if (video == null) + if (video is null) { _logger.LogError("Unable to locate item with Id {ID}.", info.ItemId.Value); } @@ -1639,7 +1640,7 @@ namespace Emby.Server.Implementations.Library // Try to resolve the path into a video video = ResolvePath(_fileSystem.GetFileSystemInfo(info.Path)) as Video; - if (video == null) + if (video is null) { _logger.LogError("Intro resolver returned null for {Path}.", info.Path); } @@ -1648,7 +1649,7 @@ namespace Emby.Server.Implementations.Library // Pull the saved db item that will include metadata var dbItem = GetItemById(video.Id) as Video; - if (dbItem != null) + if (dbItem is not null) { video = dbItem; } @@ -1685,7 +1686,7 @@ namespace Emby.Server.Implementations.Library IOrderedEnumerable<BaseItem> orderedItems = null; - foreach (var orderBy in sortBy.Select(o => GetComparer(o, user)).Where(c => c != null)) + foreach (var orderBy in sortBy.Select(o => GetComparer(o, user)).Where(c => c is not null)) { if (isFirst) { @@ -1711,7 +1712,7 @@ namespace Emby.Server.Implementations.Library foreach (var (name, sortOrder) in orderBy) { var comparer = GetComparer(name, user); - if (comparer == null) + if (comparer is null) { continue; } @@ -1781,7 +1782,7 @@ namespace Emby.Server.Implementations.Library RegisterItem(item); } - if (ItemAdded != null) + if (ItemAdded is not null) { foreach (var item in items) { @@ -1811,7 +1812,7 @@ namespace Emby.Server.Implementations.Library private bool ImageNeedsRefresh(ItemImageInfo image) { - if (image.Path != null && image.IsLocalFile) + if (image.Path is not null && image.IsLocalFile) { if (image.Width == 0 || image.Height == 0 || string.IsNullOrEmpty(image.BlurHash)) { @@ -1829,7 +1830,7 @@ namespace Emby.Server.Implementations.Library } } - return image.Path != null && !image.IsLocalFile; + return image.Path is not null && !image.IsLocalFile; } /// <inheritdoc /> @@ -1838,7 +1839,7 @@ namespace Emby.Server.Implementations.Library ArgumentNullException.ThrowIfNull(item); var outdated = forceUpdate - ? item.ImageInfos.Where(i => i.Path != null).ToArray() + ? item.ImageInfos.Where(i => i.Path is not null).ToArray() : item.ImageInfos.Where(ImageNeedsRefresh).ToArray(); // Skip image processing if current or live tv source if (outdated.Length == 0 || item.SourceType != SourceType.Library) @@ -1884,7 +1885,7 @@ namespace Emby.Server.Implementations.Library catch (Exception ex) { _logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path); - size = new ImageDimensions(0, 0); + size = default; image.Width = 0; image.Height = 0; } @@ -1923,7 +1924,7 @@ namespace Emby.Server.Implementations.Library _itemRepository.SaveItems(items, cancellationToken); - if (ItemUpdated != null) + if (ItemUpdated is not null) { foreach (var item in items) { @@ -1975,7 +1976,7 @@ namespace Emby.Server.Implementations.Library /// <param name="parent">The parent item.</param> public void ReportItemRemoved(BaseItem item, BaseItem parent) { - if (ItemRemoved != null) + if (ItemRemoved is not null) { try { @@ -2006,41 +2007,38 @@ namespace Emby.Server.Implementations.Library public List<Folder> GetCollectionFolders(BaseItem item) { - while (item != null) + return GetCollectionFolders(item, GetUserRootFolder().Children.OfType<Folder>()); + } + + public List<Folder> GetCollectionFolders(BaseItem item, IEnumerable<Folder> allUserRootChildren) + { + while (item is not null) { var parent = item.GetParent(); - if (parent == null || parent is AggregateFolder) + if (parent is AggregateFolder) { break; } - item = parent; - } - - if (item == null) - { - return new List<Folder>(); - } - - return GetCollectionFoldersInternal(item, GetUserRootFolder().Children.OfType<Folder>()); - } + if (parent is null) + { + var owner = item.GetOwner(); - public List<Folder> GetCollectionFolders(BaseItem item, List<Folder> allUserRootChildren) - { - while (item != null) - { - var parent = item.GetParent(); + if (owner is null) + { + break; + } - if (parent == null || parent is AggregateFolder) + item = owner; + } + else { - break; + item = parent; } - - item = parent; } - if (item == null) + if (item is null) { return new List<Folder>(); } @@ -2064,7 +2062,7 @@ namespace Emby.Server.Implementations.Library .Find(folder => folder is CollectionFolder) as CollectionFolder; } - return collectionFolder == null ? new LibraryOptions() : collectionFolder.GetLibraryOptions(); + return collectionFolder is null ? new LibraryOptions() : collectionFolder.GetLibraryOptions(); } public string GetContentType(BaseItem item) @@ -2129,7 +2127,7 @@ namespace Emby.Server.Implementations.Library private string GetTopFolderContentType(BaseItem item) { - if (item == null) + if (item is null) { return null; } @@ -2137,7 +2135,7 @@ namespace Emby.Server.Implementations.Library while (!item.ParentId.Equals(default)) { var parent = item.GetParent(); - if (parent == null || parent is AggregateFolder) + if (parent is null || parent is AggregateFolder) { break; } @@ -2177,7 +2175,7 @@ namespace Emby.Server.Implementations.Library var refresh = false; - if (item == null || !string.Equals(item.Path, path, StringComparison.OrdinalIgnoreCase)) + if (item is null || !string.Equals(item.Path, path, StringComparison.OrdinalIgnoreCase)) { Directory.CreateDirectory(path); @@ -2225,7 +2223,7 @@ namespace Emby.Server.Implementations.Library var isNew = false; - if (item == null) + if (item is null) { Directory.CreateDirectory(path); @@ -2251,7 +2249,7 @@ namespace Emby.Server.Implementations.Library if (!refresh && !item.DisplayParentId.Equals(default)) { var displayParent = GetItemById(item.DisplayParentId); - refresh = displayParent != null && displayParent.DateLastSaved > item.DateLastRefreshed; + refresh = displayParent is not null && displayParent.DateLastSaved > item.DateLastRefreshed; } if (refresh) @@ -2289,7 +2287,7 @@ namespace Emby.Server.Implementations.Library var isNew = false; - if (item == null) + if (item is null) { Directory.CreateDirectory(path); @@ -2315,7 +2313,7 @@ namespace Emby.Server.Implementations.Library if (!refresh && !item.DisplayParentId.Equals(default)) { var displayParent = GetItemById(item.DisplayParentId); - refresh = displayParent != null && displayParent.DateLastSaved > item.DateLastRefreshed; + refresh = displayParent is not null && displayParent.DateLastSaved > item.DateLastRefreshed; } if (refresh) @@ -2340,10 +2338,7 @@ namespace Emby.Server.Implementations.Library string sortName, string uniqueId) { - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentNullException(nameof(name)); - } + ArgumentException.ThrowIfNullOrEmpty(name); var parentIdString = parentId.Equals(default) ? null @@ -2362,7 +2357,7 @@ namespace Emby.Server.Implementations.Library var isNew = false; - if (item == null) + if (item is null) { Directory.CreateDirectory(path); @@ -2394,7 +2389,7 @@ namespace Emby.Server.Implementations.Library if (!refresh && !item.DisplayParentId.Equals(default)) { var displayParent = GetItemById(item.DisplayParentId); - refresh = displayParent != null && displayParent.DateLastSaved > item.DateLastRefreshed; + refresh = displayParent is not null && displayParent.DateLastSaved > item.DateLastRefreshed; } if (refresh) @@ -2441,7 +2436,7 @@ namespace Emby.Server.Implementations.Library public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh) { var series = episode.Series; - bool? isAbsoluteNaming = series != null && string.Equals(series.DisplayOrder, "absolute", StringComparison.OrdinalIgnoreCase); + bool? isAbsoluteNaming = series is not null && string.Equals(series.DisplayOrder, "absolute", StringComparison.OrdinalIgnoreCase); if (!isAbsoluteNaming.Value) { // In other words, no filter applied @@ -2459,10 +2454,10 @@ namespace Emby.Server.Implementations.Library episodeInfo = resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming); // Resolve from parent folder if it's not the Season folder var parent = episode.GetParent(); - if (episodeInfo == null && parent.GetType() == typeof(Folder)) + if (episodeInfo is null && parent.GetType() == typeof(Folder)) { episodeInfo = resolver.Resolve(parent.Path, true, null, null, isAbsoluteNaming); - if (episodeInfo != null) + if (episodeInfo is not null) { // add the container episodeInfo.Container = Path.GetExtension(episode.Path)?.TrimStart('.'); @@ -2582,7 +2577,7 @@ namespace Emby.Server.Implementations.Library { var season = episode.Season; - if (season != null) + if (season is not null) { episode.ParentIndexNumber = season.IndexNumber; } @@ -2590,9 +2585,9 @@ namespace Emby.Server.Implementations.Library { /* Anime series don't generally have a season in their file name, however, - tvdb needs a season to correctly get the metadata. + TVDb needs a season to correctly get the metadata. Hence, a null season needs to be filled with something. */ - // FIXME perhaps this would be better for tvdb parser to ask for season 1 if no season is specified + // FIXME perhaps this would be better for TVDb parser to ask for season 1 if no season is specified episode.ParentIndexNumber = 1; } @@ -2620,7 +2615,7 @@ namespace Emby.Server.Implementations.Library public IEnumerable<BaseItem> FindExtras(BaseItem owner, IReadOnlyList<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService) { var ownerVideoInfo = VideoResolver.Resolve(owner.Path, owner.IsFolder, _namingOptions); - if (ownerVideoInfo == null) + if (ownerVideoInfo is null) { yield break; } @@ -2640,7 +2635,7 @@ namespace Emby.Server.Implementations.Library } var extra = GetExtra(file, extraType.Value); - if (extra != null) + if (extra is not null) { yield return extra; } @@ -2649,7 +2644,7 @@ namespace Emby.Server.Implementations.Library else if (!current.IsDirectory && _extraResolver.TryGetExtraTypeForOwner(current.FullName, ownerVideoInfo, out var extraType)) { var extra = GetExtra(current, extraType.Value); - if (extra != null) + if (extra is not null) { yield return extra; } @@ -2666,7 +2661,7 @@ namespace Emby.Server.Implementations.Library // Try to retrieve it from the db. If we don't find it, use the resolved version var itemById = GetItemById(extra.Id); - if (itemById != null) + if (itemById is not null) { extra = itemById; } @@ -2681,10 +2676,10 @@ namespace Emby.Server.Implementations.Library public string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem) { string newPath; - if (ownerItem != null) + if (ownerItem is not null) { var libraryOptions = GetLibraryOptions(ownerItem); - if (libraryOptions != null) + if (libraryOptions is not null) { foreach (var pathInfo in libraryOptions.PathInfos) { @@ -2753,10 +2748,8 @@ namespace Emby.Server.Implementations.Library return null; } }) - .Where(i => i != null) - .Where(i => query.User == null ? - true : - i.IsVisible(query.User)) + .Where(i => i is not null) + .Where(i => query.User is null || i.IsVisible(query.User)) .ToList(); } @@ -2838,10 +2831,10 @@ namespace Emby.Server.Implementations.Library } var mediaPathInfos = options.PathInfos; - if (mediaPathInfos != null) + if (mediaPathInfos is not null) { var invalidpath = mediaPathInfos.FirstOrDefault(i => !Directory.Exists(i.Path)); - if (invalidpath != null) + if (invalidpath is not null) { throw new ArgumentException("The specified path does not exist: " + invalidpath.Path + "."); } @@ -2853,7 +2846,7 @@ namespace Emby.Server.Implementations.Library { Directory.CreateDirectory(virtualFolderPath); - if (collectionType != null) + if (collectionType is not null) { var path = Path.Combine(virtualFolderPath, collectionType.ToString().ToLowerInvariant() + ".collection"); @@ -2862,7 +2855,7 @@ namespace Emby.Server.Implementations.Library CollectionFolder.SaveLibraryOptions(virtualFolderPath, options); - if (mediaPathInfos != null) + if (mediaPathInfos is not null) { foreach (var path in mediaPathInfos) { @@ -2889,7 +2882,7 @@ namespace Emby.Server.Implementations.Library private async Task SavePeopleMetadataAsync(IEnumerable<PersonInfo> people, CancellationToken cancellationToken) { - var personsToSave = new List<BaseItem>(); + List<BaseItem> personsToSave = null; foreach (var person in people) { @@ -2931,12 +2924,12 @@ namespace Emby.Server.Implementations.Library if (saveEntity) { - personsToSave.Add(personEntity); + (personsToSave ??= new()).Add(personEntity); await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false); } } - if (personsToSave.Count > 0) + if (personsToSave is not null) { CreateItems(personsToSave, null, CancellationToken.None); } @@ -3098,22 +3091,19 @@ namespace Emby.Server.Implementations.Library throw new ArgumentNullException(nameof(path)); } - var removeList = new List<NameValuePair>(); + List<NameValuePair> removeList = null; foreach (var contentType in _configurationManager.Configuration.ContentTypes) { - if (string.IsNullOrWhiteSpace(contentType.Name)) - { - removeList.Add(contentType); - } - else if (_fileSystem.AreEqual(path, contentType.Name) + if (string.IsNullOrWhiteSpace(contentType.Name) + || _fileSystem.AreEqual(path, contentType.Name) || _fileSystem.ContainsSubPath(path, contentType.Name)) { - removeList.Add(contentType); + (removeList ??= new()).Add(contentType); } } - if (removeList.Count > 0) + if (removeList is not null) { _configurationManager.Configuration.ContentTypes = _configurationManager.Configuration.ContentTypes .Except(removeList) @@ -3125,10 +3115,7 @@ namespace Emby.Server.Implementations.Library public void RemoveMediaPath(string virtualFolderName, string mediaPath) { - if (string.IsNullOrEmpty(mediaPath)) - { - throw new ArgumentNullException(nameof(mediaPath)); - } + ArgumentException.ThrowIfNullOrEmpty(mediaPath); var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath; var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName); diff --git a/Emby.Server.Implementations/Library/LiveStreamHelper.cs b/Emby.Server.Implementations/Library/LiveStreamHelper.cs index 20624cc7a..936a08da8 100644 --- a/Emby.Server.Implementations/Library/LiveStreamHelper.cs +++ b/Emby.Server.Implementations/Library/LiveStreamHelper.cs @@ -60,7 +60,7 @@ namespace Emby.Server.Implementations.Library } } - if (mediaInfo == null) + if (mediaInfo is null) { if (addProbeDelay) { @@ -81,7 +81,7 @@ namespace Emby.Server.Implementations.Library }, cancellationToken).ConfigureAwait(false); - if (cacheFilePath != null) + if (cacheFilePath is not null) { Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath)); await using FileStream createStream = AsyncFile.OpenWrite(cacheFilePath); @@ -130,7 +130,7 @@ namespace Emby.Server.Implementations.Library var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio); - if (audioStream == null || audioStream.Index == -1) + if (audioStream is null || audioStream.Index == -1) { mediaSource.DefaultAudioStreamIndex = null; } @@ -140,7 +140,7 @@ namespace Emby.Server.Implementations.Library } var videoStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video); - if (videoStream != null) + if (videoStream is not null) { if (!videoStream.BitRate.HasValue) { diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index bfccc7db7..c9a26a30f 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -154,8 +154,8 @@ namespace Emby.Server.Implementations.Library // If file is strm or main media stream is missing, force a metadata refresh with remote probing if (allowMediaProbe && mediaSources[0].Type != MediaSourceType.Placeholder && (item.Path.EndsWith(".strm", StringComparison.OrdinalIgnoreCase) - || (item.MediaType == MediaType.Video && !mediaSources[0].MediaStreams.Any(i => i.Type == MediaStreamType.Video)) - || (item.MediaType == MediaType.Audio && !mediaSources[0].MediaStreams.Any(i => i.Type == MediaStreamType.Audio)))) + || (item.MediaType == MediaType.Video && mediaSources[0].MediaStreams.All(i => i.Type != MediaStreamType.Video)) + || (item.MediaType == MediaType.Audio && mediaSources[0].MediaStreams.All(i => i.Type != MediaStreamType.Audio)))) { await item.RefreshMetadata( new MetadataRefreshOptions(_directoryService) @@ -182,7 +182,7 @@ namespace Emby.Server.Implementations.Library source.SupportsDirectStream = SupportsDirectStream(source.Path, source.Protocol); } - if (user != null) + if (user is not null) { SetDefaultAudioAndSubtitleStreamIndexes(item, source, user); @@ -248,7 +248,7 @@ namespace Emby.Server.Implementations.Library if (protocol == MediaProtocol.Http) { - if (path != null) + if (path is not null) { if (path.Contains(".m3u", StringComparison.OrdinalIgnoreCase)) { @@ -328,7 +328,7 @@ namespace Emby.Server.Implementations.Library var sources = hasMediaSources.GetMediaSources(enablePathSubstitution); - if (user != null) + if (user is not null) { foreach (var source in sources) { @@ -357,7 +357,7 @@ namespace Emby.Server.Implementations.Library } var culture = _localizationManager.FindLanguageInfo(language); - if (culture != null) + if (culture is not null) { return culture.ThreeLetterISOLanguageNames; } @@ -383,7 +383,7 @@ namespace Emby.Server.Implementations.Library var preferredSubs = NormalizeLanguage(user.SubtitleLanguagePreference); var defaultAudioIndex = source.DefaultAudioStreamIndex; - var audioLangage = defaultAudioIndex == null + var audioLangage = defaultAudioIndex is null ? null : source.MediaStreams.Where(i => i.Type == MediaStreamType.Audio && i.Index == defaultAudioIndex).Select(i => i.Language).FirstOrDefault(); @@ -417,13 +417,13 @@ namespace Emby.Server.Implementations.Library public void SetDefaultAudioAndSubtitleStreamIndexes(BaseItem item, MediaSourceInfo source, User user) { // Item would only be null if the app didn't supply ItemId as part of the live stream open request - var mediaType = item == null ? MediaType.Video : item.MediaType; + var mediaType = item is null ? MediaType.Video : item.MediaType; if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) { - var userData = item == null ? new UserItemData() : _userDataManager.GetUserData(user, item); + var userData = item is null ? new UserItemData() : _userDataManager.GetUserData(user, item); - var allowRememberingSelection = item == null || item.EnableRememberingTrackSelections; + var allowRememberingSelection = item is null || item.EnableRememberingTrackSelections; SetDefaultAudioStreamIndex(source, userData, user, allowRememberingSelection); SetDefaultSubtitleStreamIndex(source, userData, user, allowRememberingSelection); @@ -432,7 +432,7 @@ namespace Emby.Server.Implementations.Library { var audio = source.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio); - if (audio != null) + if (audio is not null) { source.DefaultAudioStreamIndex = audio.Index; } @@ -543,7 +543,7 @@ namespace Emby.Server.Implementations.Library var audioStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio); - if (audioStream == null || audioStream.Index == -1) + if (audioStream is null || audioStream.Index == -1) { mediaSource.DefaultAudioStreamIndex = null; } @@ -553,7 +553,7 @@ namespace Emby.Server.Implementations.Library } var videoStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video); - if (videoStream != null) + if (videoStream is not null) { if (!videoStream.BitRate.HasValue) { @@ -638,7 +638,7 @@ namespace Emby.Server.Implementations.Library } } - if (mediaInfo == null) + if (mediaInfo is null) { if (addProbeDelay) { @@ -661,7 +661,7 @@ namespace Emby.Server.Implementations.Library }, cancellationToken).ConfigureAwait(false); - if (cacheFilePath != null) + if (cacheFilePath is not null) { Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath)); await using FileStream createStream = File.Create(cacheFilePath); @@ -713,7 +713,7 @@ namespace Emby.Server.Implementations.Library var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio); - if (audioStream == null || audioStream.Index == -1) + if (audioStream is null || audioStream.Index == -1) { mediaSource.DefaultAudioStreamIndex = null; } @@ -723,7 +723,7 @@ namespace Emby.Server.Implementations.Library } var videoStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video); - if (videoStream != null) + if (videoStream is not null) { if (!videoStream.BitRate.HasValue) { @@ -762,10 +762,7 @@ namespace Emby.Server.Implementations.Library public Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetLiveStreamWithDirectStreamProvider(string id, CancellationToken cancellationToken) { - if (string.IsNullOrEmpty(id)) - { - throw new ArgumentNullException(nameof(id)); - } + ArgumentException.ThrowIfNullOrEmpty(id); // TODO probably shouldn't throw here but it is kept for "backwards compatibility" var info = GetLiveStreamInfo(id) ?? throw new ResourceNotFoundException(); @@ -774,10 +771,7 @@ namespace Emby.Server.Implementations.Library public ILiveStream GetLiveStreamInfo(string id) { - if (string.IsNullOrEmpty(id)) - { - throw new ArgumentNullException(nameof(id)); - } + ArgumentException.ThrowIfNullOrEmpty(id); if (_openStreams.TryGetValue(id, out ILiveStream info)) { @@ -801,10 +795,7 @@ namespace Emby.Server.Implementations.Library public async Task CloseLiveStream(string id) { - if (string.IsNullOrEmpty(id)) - { - throw new ArgumentNullException(nameof(id)); - } + ArgumentException.ThrowIfNullOrEmpty(id); await _liveStreamSemaphore.WaitAsync().ConfigureAwait(false); @@ -835,10 +826,7 @@ namespace Emby.Server.Implementations.Library private (IMediaSourceProvider MediaSourceProvider, string KeyId) GetProvider(string key) { - if (string.IsNullOrEmpty(key)) - { - throw new ArgumentException("Key can't be empty.", nameof(key)); - } + ArgumentException.ThrowIfNullOrEmpty(key); var keys = key.Split(LiveStreamIdDelimeter, 2); diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs index 609b95772..6aef87c52 100644 --- a/Emby.Server.Implementations/Library/MediaStreamSelector.cs +++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs @@ -19,7 +19,7 @@ namespace Emby.Server.Implementations.Library { var defaultStream = sortedStreams.FirstOrDefault(i => i.IsDefault); - if (defaultStream != null) + if (defaultStream is not null) { return defaultStream.Index; } @@ -89,17 +89,7 @@ namespace Emby.Server.Implementations.Library // Give some preference to external text subs for better performance return streams .Where(i => i.Type == type) - .OrderBy(i => - { - var index = languagePreferences.FindIndex(x => string.Equals(x, i.Language, StringComparison.OrdinalIgnoreCase)); - - return index == -1 ? 100 : index; - }) - .ThenBy(i => GetBooleanOrderBy(i.IsDefault)) - .ThenBy(i => GetBooleanOrderBy(i.SupportsExternalStream)) - .ThenBy(i => GetBooleanOrderBy(i.IsTextSubtitleStream)) - .ThenBy(i => GetBooleanOrderBy(i.IsExternal)) - .ThenBy(i => i.Index); + .OrderByDescending(i => GetStreamScore(i, languagePreferences)); } public static void SetSubtitleStreamScores( @@ -113,9 +103,9 @@ namespace Emby.Server.Implementations.Library return; } - var sortedStreams = GetSortedStreams(streams, MediaStreamType.Subtitle, preferredLanguages); + var sortedStreams = GetSortedStreams(streams, MediaStreamType.Subtitle, preferredLanguages).ToList(); - var filteredStreams = new List<MediaStream>(); + List<MediaStream>? filteredStreams = null; if (mode == SubtitlePlaybackMode.Default) { @@ -144,46 +134,26 @@ namespace Emby.Server.Implementations.Library } // load forced subs if we have found no suitable full subtitles - var iterStreams = filteredStreams.Count == 0 + var iterStreams = filteredStreams is null || filteredStreams.Count == 0 ? sortedStreams.Where(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) : filteredStreams; foreach (var stream in iterStreams) { - stream.Score = GetSubtitleScore(stream, preferredLanguages); + stream.Score = GetStreamScore(stream, preferredLanguages); } } - private static int GetSubtitleScore(MediaStream stream, IReadOnlyList<string> languagePreferences) + internal static int GetStreamScore(MediaStream stream, IReadOnlyList<string> languagePreferences) { - var values = new List<int>(); - var index = languagePreferences.FindIndex(x => string.Equals(x, stream.Language, StringComparison.OrdinalIgnoreCase)); - - values.Add(index == -1 ? 0 : 100 - index); - - values.Add(stream.IsForced ? 1 : 0); - values.Add(stream.IsDefault ? 1 : 0); - values.Add(stream.SupportsExternalStream ? 1 : 0); - values.Add(stream.IsTextSubtitleStream ? 1 : 0); - values.Add(stream.IsExternal ? 1 : 0); - - values.Reverse(); - var scale = 1; - var score = 0; - - foreach (var value in values) - { - score += scale * (value + 1); - scale *= 10; - } - + var score = index == -1 ? 1 : 101 - index; + score = (score * 10) + (stream.IsForced ? 2 : 1); + score = (score * 10) + (stream.IsDefault ? 2 : 1); + score = (score * 10) + (stream.SupportsExternalStream ? 2 : 1); + score = (score * 10) + (stream.IsTextSubtitleStream ? 2 : 1); + score = (score * 10) + (stream.IsExternal ? 2 : 1); return score; } - - private static int GetBooleanOrderBy(bool value) - { - return value ? 0 : 1; - } } } diff --git a/Emby.Server.Implementations/Library/ResolverHelper.cs b/Emby.Server.Implementations/Library/ResolverHelper.cs index 4100a74a5..7a61e2607 100644 --- a/Emby.Server.Implementations/Library/ResolverHelper.cs +++ b/Emby.Server.Implementations/Library/ResolverHelper.cs @@ -25,13 +25,10 @@ namespace Emby.Server.Implementations.Library public static bool SetInitialItemValues(BaseItem item, Folder? parent, ILibraryManager libraryManager, IDirectoryService directoryService) { // This version of the below method has no ItemResolveArgs, so we have to require the path already being set - if (string.IsNullOrEmpty(item.Path)) - { - throw new ArgumentException("Item must have a Path"); - } + ArgumentException.ThrowIfNullOrEmpty(item.Path); // If the resolver didn't specify this - if (parent != null) + if (parent is not null) { item.SetParent(parent); } @@ -43,7 +40,7 @@ namespace Emby.Server.Implementations.Library // Make sure DateCreated and DateModified have values var fileInfo = directoryService.GetFile(item.Path); - if (fileInfo == null) + if (fileInfo is null) { return false; } @@ -71,7 +68,7 @@ namespace Emby.Server.Implementations.Library } // If the resolver didn't specify this - if (args.Parent != null) + if (args.Parent is not null) { item.SetParent(args.Parent); } @@ -113,7 +110,7 @@ namespace Emby.Server.Implementations.Library { var childData = args.IsDirectory ? args.GetFileSystemEntryByPath(item.Path) : null; - if (childData != null) + if (childData is not null) { SetDateCreated(item, childData); } @@ -140,7 +137,7 @@ namespace Emby.Server.Implementations.Library if (config.UseFileCreationTimeForDateAdded) { // directoryService.getFile may return null - if (info != null) + if (info is not null) { var dateCreated = info.CreationTimeUtc; diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs index 7a6aea9c1..a74f82475 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs @@ -45,7 +45,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio { var result = ResolveMultipleInternal(parent, files, collectionType); - if (result != null) + if (result is not null) { foreach (var item in result.Items) { @@ -116,7 +116,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio // Use regular audio type for mixed libraries, owned items and music if (isMixedCollectionType || - args.Parent == null || + args.Parent is null || isMusicCollectionType) { item = new MediaBrowser.Controller.Entities.Audio.Audio(); @@ -126,7 +126,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio item = new AudioBook(); } - if (item != null) + if (item is not null) { item.IsShortcut = string.Equals(extension, ".strm", StringComparison.OrdinalIgnoreCase); @@ -144,7 +144,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio // TODO: Allow GetMultiDiscMovie in here var result = ResolveMultipleAudio(args.Parent, args.GetActualFileSystemChildren(), parseName); - if (result == null || result.Items.Count != 1 || result.Items[0] is not AudioBook item) + if (result is null || result.Items.Count != 1 || result.Items[0] is not AudioBook item) { return null; } @@ -158,7 +158,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio private MultiItemResolverResult ResolveMultipleAudio(Folder parent, IEnumerable<FileSystemMetadata> fileSystemEntries, bool parseName) { var files = new List<FileSystemMetadata>(); - var items = new List<BaseItem>(); var leftOver = new List<FileSystemMetadata>(); // Loop through each child file/folder and see if we find a video @@ -180,10 +179,10 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio var result = new MultiItemResolverResult { ExtraFiles = leftOver, - Items = items + Items = new List<BaseItem>() }; - var isInMixedFolder = resolverResult.Count > 1 || (parent != null && parent.IsTopParent); + var isInMixedFolder = resolverResult.Count > 1 || (parent is not null && parent.IsTopParent); foreach (var resolvedItem in resolverResult) { @@ -193,7 +192,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio continue; } - if (resolvedItem.Files.Count == 0) + // Until multi-part books are handled letting files stack hides them from browsing in the client + if (resolvedItem.Files.Count == 0 || resolvedItem.Extras.Count > 0 || resolvedItem.AlternateVersions.Count > 0) { continue; } diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs index a922e3685..bbc70701c 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs @@ -25,16 +25,19 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio { private readonly ILogger<MusicAlbumResolver> _logger; private readonly NamingOptions _namingOptions; + private readonly IDirectoryService _directoryService; /// <summary> /// Initializes a new instance of the <see cref="MusicAlbumResolver"/> class. /// </summary> /// <param name="logger">The logger.</param> /// <param name="namingOptions">The naming options.</param> - public MusicAlbumResolver(ILogger<MusicAlbumResolver> logger, NamingOptions namingOptions) + /// <param name="directoryService">The directory service.</param> + public MusicAlbumResolver(ILogger<MusicAlbumResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService) { _logger = logger; _namingOptions = namingOptions; + _directoryService = directoryService; } /// <summary> @@ -109,7 +112,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio } // If args contains music it's a music album - if (ContainsMusic(args.FileSystemChildren, true, args.DirectoryService)) + if (ContainsMusic(args.FileSystemChildren, true, _directoryService)) { return true; } diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs index 2538c2b5b..c858dc53d 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Emby.Naming.Common; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Model.Entities; using Microsoft.Extensions.Logging; @@ -18,19 +19,23 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio public class MusicArtistResolver : ItemResolver<MusicArtist> { private readonly ILogger<MusicAlbumResolver> _logger; - private NamingOptions _namingOptions; + private readonly NamingOptions _namingOptions; + private readonly IDirectoryService _directoryService; /// <summary> /// Initializes a new instance of the <see cref="MusicArtistResolver"/> class. /// </summary> /// <param name="logger">Instance of the <see cref="MusicAlbumResolver"/> interface.</param> /// <param name="namingOptions">The <see cref="NamingOptions"/>.</param> + /// <param name="directoryService">The directory service.</param> public MusicArtistResolver( ILogger<MusicAlbumResolver> logger, - NamingOptions namingOptions) + NamingOptions namingOptions, + IDirectoryService directoryService) { _logger = logger; _namingOptions = namingOptions; + _directoryService = directoryService; } /// <summary> @@ -78,9 +83,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio return null; } - var directoryService = args.DirectoryService; - - var albumResolver = new MusicAlbumResolver(_logger, _namingOptions); + var albumResolver = new MusicAlbumResolver(_logger, _namingOptions, _directoryService); var directories = args.FileSystemChildren.Where(i => i.IsDirectory); @@ -97,7 +100,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio } // If we contain a music album assume we are an artist folder - if (albumResolver.IsMusicAlbum(fileSystemInfo.FullName, directoryService)) + if (albumResolver.IsMusicAlbum(fileSystemInfo.FullName, _directoryService)) { // Stop once we see a music album state.Stop(); diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs index b2a7abb1b..381796d0e 100644 --- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs @@ -25,14 +25,17 @@ namespace Emby.Server.Implementations.Library.Resolvers { private readonly ILogger _logger; - protected BaseVideoResolver(ILogger logger, NamingOptions namingOptions) + protected BaseVideoResolver(ILogger logger, NamingOptions namingOptions, IDirectoryService directoryService) { _logger = logger; NamingOptions = namingOptions; + DirectoryService = directoryService; } protected NamingOptions NamingOptions { get; } + protected IDirectoryService DirectoryService { get; } + /// <summary> /// Resolves the specified args. /// </summary> @@ -65,13 +68,26 @@ namespace Emby.Server.Implementations.Library.Resolvers var filename = child.Name; if (child.IsDirectory) { - if (IsDvdDirectory(child.FullName, filename, args.DirectoryService)) + if (IsDvdDirectory(child.FullName, filename, DirectoryService)) { - videoType = VideoType.Dvd; + var videoTmp = new TVideoType + { + Path = args.Path, + VideoType = VideoType.Dvd + }; + Set3DFormat(videoTmp); + return videoTmp; } - else if (IsBluRayDirectory(filename)) + + if (IsBluRayDirectory(filename)) { - videoType = VideoType.BluRay; + var videoTmp = new TVideoType + { + Path = args.Path, + VideoType = VideoType.BluRay + }; + Set3DFormat(videoTmp); + return videoTmp; } } else if (IsDvdFile(filename)) @@ -79,7 +95,7 @@ namespace Emby.Server.Implementations.Library.Resolvers videoType = VideoType.Dvd; } - if (videoType == null) + if (videoType is null) { continue; } @@ -93,7 +109,7 @@ namespace Emby.Server.Implementations.Library.Resolvers videoInfo = VideoResolver.Resolve(args.Path, false, NamingOptions, parseName); } - if (videoInfo == null || (!videoInfo.IsStub && !VideoResolver.IsVideoFile(args.Path, NamingOptions))) + if (videoInfo is null || (!videoInfo.IsStub && !VideoResolver.IsVideoFile(args.Path, NamingOptions))) { return null; } @@ -163,17 +179,15 @@ namespace Emby.Server.Implementations.Library.Resolvers try { // use disc-utils, both DVDs and BDs use UDF filesystem - using (var videoFileStream = File.Open(video.Path, FileMode.Open, FileAccess.Read)) - using (UdfReader udfReader = new UdfReader(videoFileStream)) + using var videoFileStream = File.Open(video.Path, FileMode.Open, FileAccess.Read, FileShare.Read); + using UdfReader udfReader = new UdfReader(videoFileStream); + if (udfReader.DirectoryExists("VIDEO_TS")) { - if (udfReader.DirectoryExists("VIDEO_TS")) - { - video.IsoType = IsoType.Dvd; - } - else if (udfReader.DirectoryExists("BDMV")) - { - video.IsoType = IsoType.BluRay; - } + video.IsoType = IsoType.Dvd; + } + else if (udfReader.DirectoryExists("BDMV")) + { + video.IsoType = IsoType.BluRay; } } catch (Exception ex) diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs index 6fc200e3b..042422c6f 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs @@ -34,7 +34,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books var extension = Path.GetExtension(args.Path); - if (extension != null && _validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) + if (extension is not null && _validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) { // It's a book return new Book diff --git a/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs b/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs index 408e640f9..b4791b945 100644 --- a/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs @@ -4,6 +4,8 @@ using System.IO; using Emby.Naming.Common; using Emby.Naming.Video; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Model.Entities; using Microsoft.Extensions.Logging; @@ -14,7 +16,7 @@ namespace Emby.Server.Implementations.Library.Resolvers /// <summary> /// Resolves a Path into a Video or Video subclass. /// </summary> - internal class ExtraResolver + internal class ExtraResolver : BaseVideoResolver<Video> { private readonly NamingOptions _namingOptions; private readonly IItemResolver[] _trailerResolvers; @@ -25,11 +27,18 @@ namespace Emby.Server.Implementations.Library.Resolvers /// </summary> /// <param name="logger">The logger.</param> /// <param name="namingOptions">An instance of <see cref="NamingOptions"/>.</param> - public ExtraResolver(ILogger<ExtraResolver> logger, NamingOptions namingOptions) + /// <param name="directoryService">The directory service.</param> + public ExtraResolver(ILogger<ExtraResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService) + : base(logger, namingOptions, directoryService) { _namingOptions = namingOptions; - _trailerResolvers = new IItemResolver[] { new GenericVideoResolver<Trailer>(logger, namingOptions) }; - _videoResolvers = new IItemResolver[] { new GenericVideoResolver<Video>(logger, namingOptions) }; + _trailerResolvers = new IItemResolver[] { new GenericVideoResolver<Trailer>(logger, namingOptions, directoryService) }; + _videoResolvers = new IItemResolver[] { this }; + } + + protected override Video Resolve(ItemResolveArgs args) + { + return ResolveVideo<Video>(args, true); } /// <summary> @@ -48,7 +57,7 @@ namespace Emby.Server.Implementations.Library.Resolvers public bool TryGetExtraTypeForOwner(string path, VideoFileInfo ownerVideoFileInfo, [NotNullWhen(true)] out ExtraType? extraType) { var extraResult = GetExtraInfo(path, _namingOptions); - if (extraResult.ExtraType == null) + if (extraResult.ExtraType is null) { extraType = null; return false; diff --git a/Emby.Server.Implementations/Library/Resolvers/GenericFolderResolver.cs b/Emby.Server.Implementations/Library/Resolvers/GenericFolderResolver.cs index 079962282..1c2de912a 100644 --- a/Emby.Server.Implementations/Library/Resolvers/GenericFolderResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/GenericFolderResolver.cs @@ -22,7 +22,7 @@ namespace Emby.Server.Implementations.Library.Resolvers { base.SetInitialItemValues(item, args); - item.IsRoot = args.Parent == null; + item.IsRoot = args.Parent is null; } } } diff --git a/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs index 5e33b402d..ba320266a 100644 --- a/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs @@ -2,6 +2,7 @@ using Emby.Naming.Common; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Library.Resolvers @@ -18,8 +19,9 @@ namespace Emby.Server.Implementations.Library.Resolvers /// </summary> /// <param name="logger">The logger.</param> /// <param name="namingOptions">The naming options.</param> - public GenericVideoResolver(ILogger logger, NamingOptions namingOptions) - : base(logger, namingOptions) + /// <param name="directoryService">The directory service.</param> + public GenericVideoResolver(ILogger logger, NamingOptions namingOptions, IDirectoryService directoryService) + : base(logger, namingOptions, directoryService) { } } diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index 8f9e5f01b..ea980b992 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -43,8 +43,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies /// <param name="imageProcessor">The image processor.</param> /// <param name="logger">The logger.</param> /// <param name="namingOptions">The naming options.</param> - public MovieResolver(IImageProcessor imageProcessor, ILogger<MovieResolver> logger, NamingOptions namingOptions) - : base(logger, namingOptions) + /// <param name="directoryService">The directory service.</param> + public MovieResolver(IImageProcessor imageProcessor, ILogger<MovieResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService) + : base(logger, namingOptions, directoryService) { _imageProcessor = imageProcessor; } @@ -64,7 +65,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies { var result = ResolveMultipleInternal(parent, files, collectionType); - if (result != null) + if (result is not null) { foreach (var item in result.Items) { @@ -97,18 +98,18 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase)) { - movie = FindMovie<MusicVideo>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false); + movie = FindMovie<MusicVideo>(args, args.Path, args.Parent, files, DirectoryService, collectionType, false); } if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase)) { - movie = FindMovie<Video>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false); + movie = FindMovie<Video>(args, args.Path, args.Parent, files, DirectoryService, collectionType, false); } if (string.IsNullOrEmpty(collectionType)) { // Owned items will be caught by the video extra resolver - if (args.Parent == null) + if (args.Parent is null) { return null; } @@ -118,19 +119,19 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies return null; } - movie = FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true); + movie = FindMovie<Movie>(args, args.Path, args.Parent, files, DirectoryService, collectionType, true); } if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase)) { - movie = FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true); + movie = FindMovie<Movie>(args, args.Path, args.Parent, files, DirectoryService, collectionType, true); } // ignore extras - return movie?.ExtraType == null ? movie : null; + return movie?.ExtraType is null ? movie : null; } - if (args.Parent == null) + if (args.Parent is null) { return base.Resolve(args); } @@ -168,12 +169,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies } // Ignore extras - if (item?.ExtraType != null) + if (item?.ExtraType is not null) { return null; } - if (item != null) + if (item is not null) { item.IsInMixedFolder = true; } @@ -205,7 +206,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies if (string.IsNullOrEmpty(collectionType)) { // Owned items should just use the plain video type - if (parent == null) + if (parent is null) { return ResolveVideos<Video>(parent, files, false, collectionType, false); } @@ -268,7 +269,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies var videoInfos = files .Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, NamingOptions, parseName)) - .Where(f => f != null) + .Where(f => f is not null) .ToList(); var resolverResult = VideoListResolver.Resolve(videoInfos, NamingOptions, supportMultiEditions, parseName); @@ -284,7 +285,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies { var firstVideo = video.Files[0]; var path = firstVideo.Path; - if (video.ExtraType != null) + if (video.ExtraType is not null) { result.ExtraFiles.Add(files.Find(f => string.Equals(f.FullName, path, StringComparison.OrdinalIgnoreCase))); continue; @@ -313,13 +314,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies return result; } - private static bool IsIgnored(string filename) - { - // Ignore samples - Match m = Regex.Match(filename, @"\bsample\b", RegexOptions.IgnoreCase | RegexOptions.Compiled); - - return m.Success; - } + private static bool IsIgnored(ReadOnlySpan<char> filename) + => Regex.IsMatch(filename, @"\bsample\b", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static bool ContainsFile(IReadOnlyList<VideoInfo> result, FileSystemMetadata file) { @@ -376,7 +372,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies if (!justName.IsEmpty) { - // check for tmdb id + // Check for TMDb id var tmdbid = justName.GetAttributeValue("tmdbid"); if (!string.IsNullOrWhiteSpace(tmdbid)) @@ -387,7 +383,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies if (!string.IsNullOrEmpty(item.Path)) { - // check for imdb id - we use full media path, as we can assume, that this will match in any use case (either id in parent dir or in file name) + // Check for IMDb id - we use full media path, as we can assume that this will match in any use case (whether id in parent dir or in file name) var imdbid = item.Path.AsSpan().GetAttributeValue("imdbid"); if (!string.IsNullOrWhiteSpace(imdbid)) @@ -529,7 +525,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies } return false; - }).OrderBy(i => i).ToList(); + }).Order().ToList(); // If different video types were found, don't allow this if (videoTypes.Distinct().Count() > 1) @@ -568,7 +564,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies private bool IsInvalid(Folder parent, ReadOnlySpan<char> collectionType) { - if (parent != null) + if (parent is not null) { if (parent.IsRoot) { diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs index e11fb262e..9026160ff 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs @@ -1,7 +1,5 @@ #nullable disable -#pragma warning disable CS1591 - using System; using System.Collections.Generic; using System.IO; @@ -12,15 +10,20 @@ using Jellyfin.Extensions; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Model.Entities; namespace Emby.Server.Implementations.Library.Resolvers { + /// <summary> + /// Class PhotoResolver. + /// </summary> public class PhotoResolver : ItemResolver<Photo> { private readonly IImageProcessor _imageProcessor; private readonly NamingOptions _namingOptions; + private readonly IDirectoryService _directoryService; private static readonly HashSet<string> _ignoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { @@ -35,10 +38,17 @@ namespace Emby.Server.Implementations.Library.Resolvers "default" }; - public PhotoResolver(IImageProcessor imageProcessor, NamingOptions namingOptions) + /// <summary> + /// Initializes a new instance of the <see cref="PhotoResolver"/> class. + /// </summary> + /// <param name="imageProcessor">The image processor.</param> + /// <param name="namingOptions">The naming options.</param> + /// <param name="directoryService">The directory service.</param> + public PhotoResolver(IImageProcessor imageProcessor, NamingOptions namingOptions, IDirectoryService directoryService) { _imageProcessor = imageProcessor; _namingOptions = namingOptions; + _directoryService = directoryService; } /// <summary> @@ -61,7 +71,7 @@ namespace Emby.Server.Implementations.Library.Resolvers var filename = Path.GetFileNameWithoutExtension(args.Path); // Make sure the image doesn't belong to a video file - var files = args.DirectoryService.GetFiles(Path.GetDirectoryName(args.Path)); + var files = _directoryService.GetFiles(Path.GetDirectoryName(args.Path)); foreach (var file in files) { diff --git a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs index 6b0dfe986..7a2b3da3a 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs @@ -31,16 +31,18 @@ namespace Emby.Server.Implementations.Library.Resolvers if (args.IsDirectory) { // It's a boxset if the path is a directory with [playlist] in it's the name - // TODO: Should this use Path.GetDirectoryName() instead? - bool isBoxSet = Path.GetFileName(args.Path) - ?.Contains("[playlist]", StringComparison.OrdinalIgnoreCase) - ?? false; - if (isBoxSet) + var filename = Path.GetFileName(Path.TrimEndingDirectorySeparator(args.Path)); + if (string.IsNullOrEmpty(filename)) + { + return null; + } + + if (filename.Contains("[playlist]", StringComparison.OrdinalIgnoreCase)) { return new Playlist { Path = args.Path, - Name = Path.GetFileName(args.Path).Replace("[playlist]", string.Empty, StringComparison.OrdinalIgnoreCase).Trim() + Name = filename.Replace("[playlist]", string.Empty, StringComparison.OrdinalIgnoreCase).Trim() }; } @@ -51,7 +53,7 @@ namespace Emby.Server.Implementations.Library.Resolvers return new Playlist { Path = args.Path, - Name = Path.GetFileName(args.Path) + Name = filename }; } } @@ -60,8 +62,8 @@ namespace Emby.Server.Implementations.Library.Resolvers // It should have the correct collection type and a supported file extension else if (_musicPlaylistCollectionTypes.Contains(args.CollectionType ?? string.Empty, StringComparison.OrdinalIgnoreCase)) { - var extension = Path.GetExtension(args.Path); - if (Playlist.SupportedExtensions.Contains(extension ?? string.Empty, StringComparison.OrdinalIgnoreCase)) + var extension = Path.GetExtension(args.Path.AsSpan()); + if (Playlist.SupportedExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) { return new Playlist { diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs index 9ba079edf..392ee4c77 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs @@ -5,6 +5,7 @@ using System.Linq; using Emby.Naming.Common; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using Microsoft.Extensions.Logging; @@ -20,8 +21,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV /// </summary> /// <param name="logger">The logger.</param> /// <param name="namingOptions">The naming options.</param> - public EpisodeResolver(ILogger<EpisodeResolver> logger, NamingOptions namingOptions) - : base(logger, namingOptions) + /// <param name="directoryService">The directory service.</param> + public EpisodeResolver(ILogger<EpisodeResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService) + : base(logger, namingOptions, directoryService) { } @@ -34,7 +36,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV { var parent = args.Parent; - if (parent == null) + if (parent is null) { return null; } @@ -46,34 +48,34 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV // If the parent is a Season or Series and the parent is not an extras folder, then this is an Episode if the VideoResolver returns something // Also handle flat tv folders - if (season != null || + if (season is not null || string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) || args.HasParent<Series>()) { var episode = ResolveVideo<Episode>(args, false); // Ignore extras - if (episode == null || episode.ExtraType != null) + if (episode is null || episode.ExtraType is not null) { return null; } var series = parent as Series ?? parent.GetParents().OfType<Series>().FirstOrDefault(); - if (series != null) + if (series is not null) { episode.SeriesId = series.Id; episode.SeriesName = series.Name; } - if (season != null) + if (season is not null) { episode.SeasonId = season.Id; episode.SeasonName = season.Name; } // Assume season 1 if there's no season folder and a season number could not be determined - if (season == null && !episode.ParentIndexNumber.HasValue && (episode.IndexNumber.HasValue || episode.PremiereDate.HasValue)) + if (season is null && !episode.ParentIndexNumber.HasValue && (episode.IndexNumber.HasValue || episode.PremiereDate.HasValue)) { episode.ParentIndexNumber = 1; } diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs index ea4851458..62a524d2e 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs @@ -66,7 +66,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV var episodeInfo = resolver.Resolve(testPath, true); - if (episodeInfo?.EpisodeNumber != null && episodeInfo.SeasonNumber.HasValue) + if (episodeInfo?.EpisodeNumber is not null && episodeInfo.SeasonNumber.HasValue) { _logger.LogDebug( "Found folder underneath series with episode number: {0}. Season {1}. Episode {2}", diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs index f5ac3c665..8f69175d0 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs @@ -76,7 +76,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV { if (args.ContainsFileSystemEntryByName("tvshow.nfo")) { - if (args.Parent != null && args.Parent.IsRoot) + if (args.Parent is not null && args.Parent.IsRoot) { // For now, return null, but if we want to allow this in the future then add some additional checks to guard against a misplaced tvshow.nfo return null; @@ -89,7 +89,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV }; } - if (args.Parent != null && args.Parent.IsRoot) + if (args.Parent is not null && args.Parent.IsRoot) { return null; } @@ -138,7 +138,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV var episodeResolver = new Naming.TV.EpisodeResolver(namingOptions); var episodeInfo = episodeResolver.Resolve(fullName, false, true, false, fillExtendedInfo: false); - if (episodeInfo != null && episodeInfo.EpisodeNumber.HasValue) + if (episodeInfo is not null && episodeInfo.EpisodeNumber.HasValue) { return true; } diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs index 60778a443..b916b9170 100644 --- a/Emby.Server.Implementations/Library/SearchEngine.cs +++ b/Emby.Server.Implementations/Library/SearchEngine.cs @@ -73,10 +73,7 @@ namespace Emby.Server.Implementations.Library { var searchTerm = query.SearchTerm; - if (string.IsNullOrEmpty(searchTerm)) - { - throw new ArgumentException("SearchTerm can't be empty.", nameof(query)); - } + ArgumentException.ThrowIfNullOrEmpty(searchTerm); searchTerm = searchTerm.Trim().RemoveDiacritics(); diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs index aecab7d9c..a0a90b129 100644 --- a/Emby.Server.Implementations/Library/UserDataManager.cs +++ b/Emby.Server.Implementations/Library/UserDataManager.cs @@ -126,7 +126,7 @@ namespace Emby.Server.Implementations.Library { var userData = _repository.GetUserData(internalUserId, keys); - if (userData != null) + if (userData is not null) { return userData; } diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs index ec411aa3b..17f1d1905 100644 --- a/Emby.Server.Implementations/Library/UserViewManager.cs +++ b/Emby.Server.Implementations/Library/UserViewManager.cs @@ -47,7 +47,7 @@ namespace Emby.Server.Implementations.Library { var user = _userManager.GetUserById(query.UserId); - if (user == null) + if (user is null) { throw new ArgumentException("User Id specified in the query does not exist.", nameof(query)); } @@ -72,7 +72,7 @@ namespace Emby.Server.Implementations.Library continue; } - if (collectionFolder != null && UserView.IsEligibleForGrouping(folder) && user.IsFolderGrouped(folder.Id)) + if (collectionFolder is not null && UserView.IsEligibleForGrouping(folder) && user.IsFolderGrouped(folder.Id)) { groupedFolders.Add(collectionFolder); continue; @@ -111,10 +111,10 @@ namespace Emby.Server.Implementations.Library if (query.IncludeExternalContent) { - var channelResult = _channelManager.GetChannelsInternal(new ChannelQuery + var channelResult = _channelManager.GetChannelsInternalAsync(new ChannelQuery { UserId = query.UserId - }); + }).GetAwaiter().GetResult(); var channels = channelResult.Items; @@ -208,15 +208,15 @@ namespace Emby.Server.Implementations.Library // Only grab the index container for media var container = item.IsFolder || !request.GroupItems ? null : item.LatestItemsIndexContainer; - if (container == null) + if (container is null) { list.Add(new Tuple<BaseItem, List<BaseItem>>(null, new List<BaseItem> { item })); } else { - var current = list.FirstOrDefault(i => i.Item1 != null && i.Item1.Id.Equals(container.Id)); + var current = list.FirstOrDefault(i => i.Item1 is not null && i.Item1.Id.Equals(container.Id)); - if (current != null) + if (current is not null) { current.Item2.Add(item); } @@ -286,7 +286,7 @@ namespace Emby.Server.Implementations.Library if (parents.Count == 0) { - return new List<BaseItem>(); + return Array.Empty<BaseItem>(); } if (includeItemTypes.Length == 0) diff --git a/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs index 88b93a211..df45793c3 100644 --- a/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs +++ b/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs @@ -118,7 +118,7 @@ namespace Emby.Server.Implementations.Library.Validators try { var boxSet = boxSets.FirstOrDefault(b => b?.Name == collectionName) as BoxSet; - if (boxSet == null) + if (boxSet is null) { // won't automatically create collection if only one movie in it if (movieIds.Count >= 2) diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs index b2d25fdae..49833de73 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs @@ -34,7 +34,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV public Task Record(IDirectStreamProvider? directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) { - if (directStreamProvider != null) + if (directStreamProvider is not null) { return RecordFromDirectStreamProvider(directStreamProvider, targetFile, duration, onStarted, cancellationToken); } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index 74321a256..b9d0f170a 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -295,7 +295,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } var program = GetProgramInfoFromCache(timer); - if (program == null) + if (program is null) { OnTimerOutOfDate(timer); continue; @@ -367,7 +367,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { var epgChannel = GetEpgChannelFromTunerChannel(info, tunerChannel, epgChannels); - if (epgChannel != null) + if (epgChannel is not null) { if (!string.IsNullOrWhiteSpace(epgChannel.Name)) { @@ -450,7 +450,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV var channel = epgChannelData.GetChannelById(mappedTunerChannelId); - if (channel != null) + if (channel is not null) { return channel; } @@ -473,7 +473,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV var channel = epgChannelData.GetChannelById(mappedTunerChannelId); - if (channel != null) + if (channel is not null) { return channel; } @@ -490,7 +490,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV var channel = epgChannelData.GetChannelByNumber(tunerChannelNumber); - if (channel != null) + if (channel is not null) { return channel; } @@ -502,7 +502,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV var channel = epgChannelData.GetChannelByName(normalizedName); - if (channel != null) + if (channel is not null) { return channel; } @@ -552,7 +552,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } var remove = _seriesTimerProvider.GetAll().FirstOrDefault(r => string.Equals(r.Id, timerId, StringComparison.OrdinalIgnoreCase)); - if (remove != null) + if (remove is not null) { _seriesTimerProvider.Delete(remove); } @@ -563,7 +563,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private void CancelTimerInternal(string timerId, bool isSeriesCancelled, bool isManualCancellation) { var timer = _timerProvider.GetTimer(timerId); - if (timer != null) + if (timer is not null) { var statusChanging = timer.Status != RecordingStatus.Cancelled; timer.Status = RecordingStatus.Cancelled; @@ -582,7 +582,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV _timerProvider.AddOrUpdate(timer, false); } - if (statusChanging && TimerCancelled != null) + if (statusChanging && TimerCancelled is not null) { TimerCancelled(this, new GenericEventArgs<string>(timerId)); } @@ -617,7 +617,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV null : _timerProvider.GetTimerByProgramId(info.ProgramId); - if (existingTimer != null) + if (existingTimer is not null) { if (existingTimer.Status == RecordingStatus.Cancelled || existingTimer.Status == RecordingStatus.Completed) @@ -627,10 +627,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV _timerProvider.Update(existingTimer); return Task.FromResult(existingTimer.Id); } - else - { - throw new ArgumentException("A scheduled recording already exists for this program."); - } + + throw new ArgumentException("A scheduled recording already exists for this program."); } info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); @@ -642,13 +640,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV programInfo = GetProgramInfoFromCache(info); } - if (programInfo == null) + if (programInfo is null) { _logger.LogInformation("Unable to find program with Id {0}. Will search using start date", info.ProgramId); programInfo = GetProgramInfoFromCache(info.ChannelId, info.StartDate); } - if (programInfo != null) + if (programInfo is not null) { CopyProgramInfoToTimerInfo(programInfo, info); } @@ -668,7 +666,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV // populate info.seriesID var program = GetProgramInfoFromCache(info.ProgramId); - if (program != null) + if (program is not null) { info.SeriesId = program.ExternalSeriesId; } @@ -714,7 +712,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { var instance = _seriesTimerProvider.GetAll().FirstOrDefault(i => string.Equals(i.Id, info.Id, StringComparison.OrdinalIgnoreCase)); - if (instance != null) + if (instance is not null) { instance.ChannelId = info.ChannelId; instance.Days = info.Days; @@ -744,7 +742,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { var existingTimer = _timerProvider.GetTimer(updatedTimer.Id); - if (existingTimer == null) + if (existingTimer is null) { throw new ResourceNotFoundException(); } @@ -861,7 +859,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } }; - if (program != null) + if (program is not null) { defaults.SeriesId = program.SeriesId; defaults.ProgramId = program.Id; @@ -912,7 +910,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV var epgChannel = await GetEpgChannelFromTunerChannel(provider.Item1, provider.Item2, channel, cancellationToken).ConfigureAwait(false); - if (epgChannel == null) + if (epgChannel is null) { _logger.LogDebug("EPG channel not found for tuner channel {0}-{1} from {2}-{3}", channel.Number, channel.Name, provider.Item1.Name, provider.Item2.ListingsId ?? string.Empty); continue; @@ -945,9 +943,9 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { var provider = _liveTvManager.ListingProviders.FirstOrDefault(l => string.Equals(l.Type, i.Type, StringComparison.OrdinalIgnoreCase)); - return provider == null ? null : new Tuple<IListingsProvider, ListingsProviderInfo>(provider, i); + return provider is null ? null : new Tuple<IListingsProvider, ListingsProviderInfo>(provider, i); }) - .Where(i => i != null) + .Where(i => i is not null) .ToList(); } @@ -964,7 +962,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV null : currentLiveStreams.FirstOrDefault(i => string.Equals(i.OriginalStreamId, streamId, StringComparison.OrdinalIgnoreCase)); - if (result != null && result.EnableStreamSharing) + if (result is not null && result.EnableStreamSharing) { result.ConsumerCount++; @@ -1134,7 +1132,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV // trim trailing period from the folder name var folderName = _fileSystem.GetValidFilename(timer.Name).Trim().TrimEnd('.').Trim(); - if (metadata != null && metadata.ProductionYear.HasValue) + if (metadata is not null && metadata.ProductionYear.HasValue) { folderName += " (" + metadata.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")"; } @@ -1232,13 +1230,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV programInfo = GetProgramInfoFromCache(timer); } - if (programInfo == null) + if (programInfo is null) { _logger.LogInformation("Unable to find program with Id {0}. Will search using start date", timer.ProgramId); programInfo = GetProgramInfoFromCache(timer.ChannelId, timer.StartDate); } - if (programInfo != null) + if (programInfo is not null) { CopyProgramInfoToTimerInfo(programInfo, timer); } @@ -1412,7 +1410,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV var item = GetAffectedBaseItem(Path.GetDirectoryName(path)); - if (item != null) + if (item is not null) { _logger.LogInformation("Refreshing recording parent {Path}", item.Path); @@ -1437,19 +1435,19 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV var parentPath = Path.GetDirectoryName(path); - while (item == null && !string.IsNullOrEmpty(path)) + while (item is null && !string.IsNullOrEmpty(path)) { item = _libraryManager.FindByPath(path, null); path = Path.GetDirectoryName(path); } - if (item != null) + if (item is not null) { if (item.GetType() == typeof(Folder) && string.Equals(item.Path, parentPath, StringComparison.OrdinalIgnoreCase)) { var parentItem = item.GetParent(); - if (parentItem != null && parentItem is not AggregateFolder) + if (parentItem is not null && parentItem is not AggregateFolder) { item = parentItem; } @@ -1474,7 +1472,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV var seriesTimerId = timer.SeriesTimerId; var seriesTimer = _seriesTimerProvider.GetAll().FirstOrDefault(i => string.Equals(i.Id, seriesTimerId, StringComparison.OrdinalIgnoreCase)); - if (seriesTimer == null || seriesTimer.KeepUpTo <= 0) + if (seriesTimer is null || seriesTimer.KeepUpTo <= 0) { return; } @@ -1569,7 +1567,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { var libraryItem = _libraryManager.FindByPath(timer.RecordingPath, false); - if (libraryItem != null) + if (libraryItem is not null) { _libraryManager.DeleteItem( libraryItem, @@ -1695,7 +1693,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV _ => null }; - if (imageSaveFilenameWithoutExtension == null) + if (imageSaveFilenameWithoutExtension is null) { return; } @@ -1714,7 +1712,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV (program.GetImageInfo(ImageType.Thumb, 0) ?? program.GetImageInfo(ImageType.Primary, 0)) : (program.GetImageInfo(ImageType.Primary, 0) ?? program.GetImageInfo(ImageType.Thumb, 0)); - if (image != null) + if (image is not null) { try { @@ -1729,7 +1727,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV if (!program.IsSeries) { image = program.GetImageInfo(ImageType.Backdrop, 0); - if (image != null) + if (image is not null) { try { @@ -1742,7 +1740,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } image = program.GetImageInfo(ImageType.Thumb, 0); - if (image != null) + if (image is not null) { try { @@ -1755,7 +1753,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } image = program.GetImageInfo(ImageType.Logo, 0); - if (image != null) + if (image is not null) { try { @@ -1782,7 +1780,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV }).FirstOrDefault() as LiveTvProgram; // dummy this up - if (program == null) + if (program is null) { program = new LiveTvProgram { @@ -1814,21 +1812,29 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV program.AddGenre("News"); } - if (timer.IsProgramSeries) - { - await SaveSeriesNfoAsync(timer, seriesPath).ConfigureAwait(false); - await SaveVideoNfoAsync(timer, recordingPath, program, false).ConfigureAwait(false); - } - else if (!timer.IsMovie || timer.IsSports || timer.IsNews) + var config = GetConfiguration(); + + if (config.SaveRecordingNFO) { - await SaveVideoNfoAsync(timer, recordingPath, program, true).ConfigureAwait(false); + if (timer.IsProgramSeries) + { + await SaveSeriesNfoAsync(timer, seriesPath).ConfigureAwait(false); + await SaveVideoNfoAsync(timer, recordingPath, program, false).ConfigureAwait(false); + } + else if (!timer.IsMovie || timer.IsSports || timer.IsNews) + { + await SaveVideoNfoAsync(timer, recordingPath, program, true).ConfigureAwait(false); + } + else + { + await SaveVideoNfoAsync(timer, recordingPath, program, false).ConfigureAwait(false); + } } - else + + if (config.SaveRecordingImages) { - await SaveVideoNfoAsync(timer, recordingPath, program, false).ConfigureAwait(false); + await SaveRecordingImages(recordingPath, program).ConfigureAwait(false); } - - await SaveRecordingImages(recordingPath, program).ConfigureAwait(false); } catch (Exception ex) { @@ -1858,8 +1864,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { await writer.WriteStartDocumentAsync(true).ConfigureAwait(false); await writer.WriteStartElementAsync(null, "tvshow", null).ConfigureAwait(false); - string id; - if (timer.SeriesProviderIds.TryGetValue(MetadataProvider.Tvdb.ToString(), out id)) + if (timer.SeriesProviderIds.TryGetValue(MetadataProvider.Tvdb.ToString(), out var id)) { await writer.WriteElementStringAsync(null, "id", null, id).ConfigureAwait(false); } @@ -2024,7 +2029,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV var people = item.Id.Equals(default) ? new List<PersonInfo>() : _libraryManager.GetPeople(item); var directors = people - .Where(i => IsPersonType(i, PersonType.Director)) + .Where(i => i.IsType(PersonKind.Director)) .Select(i => i.Name) .ToList(); @@ -2034,7 +2039,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } var writers = people - .Where(i => IsPersonType(i, PersonType.Writer)) + .Where(i => i.IsType(PersonKind.Writer)) .Select(i => i.Name) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); @@ -2114,10 +2119,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } } - private static bool IsPersonType(PersonInfo person, string type) - => string.Equals(person.Type, type, StringComparison.OrdinalIgnoreCase) - || string.Equals(person.Role, type, StringComparison.OrdinalIgnoreCase); - private LiveTvProgram GetProgramInfoFromCache(string programId) { var query = new InternalItemsQuery @@ -2192,16 +2193,15 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private void HandleDuplicateShowIds(List<TimerInfo> timers) { - foreach (var timer in timers.Skip(1)) + // sort showings by HD channels first, then by startDate, record earliest showing possible + foreach (var timer in timers.OrderByDescending(t => _liveTvManager.GetLiveTvChannel(t, this).IsHD).ThenBy(t => t.StartDate).Skip(1)) { - // TODO: Get smarter, prefer HD, etc - timer.Status = RecordingStatus.Cancelled; _timerProvider.Update(timer); } } - private void SearchForDuplicateShowIds(List<TimerInfo> timers) + private void SearchForDuplicateShowIds(IEnumerable<TimerInfo> timers) { var groups = timers.ToLookup(i => i.ShowId ?? string.Empty).ToList(); @@ -2241,7 +2241,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV ? null : _timerProvider.GetTimerByProgramId(timer.ProgramId)); - if (existingTimer == null) + if (existingTimer is null) { if (ShouldCancelTimerForSeriesTimer(seriesTimer, timer)) { @@ -2282,39 +2282,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV if (updateTimerSettings) { - // Only update if not currently active - test both new timer and existing in case Id's are different - // Id's could be different if the timer was created manually prior to series timer creation - if (!_activeRecordings.TryGetValue(timer.Id, out _) && !_activeRecordings.TryGetValue(existingTimer.Id, out _)) - { - UpdateExistingTimerWithNewMetadata(existingTimer, timer); - - // Needed by ShouldCancelTimerForSeriesTimer - timer.IsManual = existingTimer.IsManual; - - if (ShouldCancelTimerForSeriesTimer(seriesTimer, timer)) - { - existingTimer.Status = RecordingStatus.Cancelled; - } - else if (!existingTimer.IsManual) - { - existingTimer.Status = RecordingStatus.New; - } - - if (existingTimer.Status != RecordingStatus.Cancelled) - { - enabledTimersForSeries.Add(existingTimer); - } - - existingTimer.KeepUntil = seriesTimer.KeepUntil; - existingTimer.IsPostPaddingRequired = seriesTimer.IsPostPaddingRequired; - existingTimer.IsPrePaddingRequired = seriesTimer.IsPrePaddingRequired; - existingTimer.PostPaddingSeconds = seriesTimer.PostPaddingSeconds; - existingTimer.PrePaddingSeconds = seriesTimer.PrePaddingSeconds; - existingTimer.Priority = seriesTimer.Priority; - existingTimer.SeriesTimerId = seriesTimer.Id; - - _timerProvider.Update(existingTimer); - } + existingTimer.KeepUntil = seriesTimer.KeepUntil; + existingTimer.IsPostPaddingRequired = seriesTimer.IsPostPaddingRequired; + existingTimer.IsPrePaddingRequired = seriesTimer.IsPrePaddingRequired; + existingTimer.PostPaddingSeconds = seriesTimer.PostPaddingSeconds; + existingTimer.PrePaddingSeconds = seriesTimer.PrePaddingSeconds; + existingTimer.Priority = seriesTimer.Priority; + existingTimer.SeriesTimerId = seriesTimer.Id; } existingTimer.SeriesTimerId = seriesTimer.Id; @@ -2394,13 +2368,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV DtoOptions = new DtoOptions() }).FirstOrDefault() as LiveTvChannel; - if (channel != null && !string.IsNullOrWhiteSpace(channel.ExternalId)) + if (channel is not null && !string.IsNullOrWhiteSpace(channel.ExternalId)) { tempChannelCache[parent.ChannelId] = channel; } } - if (channel != null || tempChannelCache.TryGetValue(parent.ChannelId, out channel)) + if (channel is not null || tempChannelCache.TryGetValue(parent.ChannelId, out channel)) { channelId = channel.ExternalId; } @@ -2453,13 +2427,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV DtoOptions = new DtoOptions() }).FirstOrDefault() as LiveTvChannel; - if (channel != null && !string.IsNullOrWhiteSpace(channel.ExternalId)) + if (channel is not null && !string.IsNullOrWhiteSpace(channel.ExternalId)) { tempChannelCache[programInfo.ChannelId] = channel; } } - if (channel != null || tempChannelCache.TryGetValue(programInfo.ChannelId, out channel)) + if (channel is not null || tempChannelCache.TryGetValue(programInfo.ChannelId, out channel)) { channelId = channel.ExternalId; } @@ -2653,7 +2627,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { var configuredDevice = configuredDevices.FirstOrDefault(i => string.Equals(i.DeviceId, device.DeviceId, StringComparison.OrdinalIgnoreCase)); - if (configuredDevice != null && !string.Equals(device.Url, configuredDevice.Url, StringComparison.OrdinalIgnoreCase)) + if (configuredDevice is not null && !string.Equals(device.Url, configuredDevice.Url, StringComparison.OrdinalIgnoreCase)) { _logger.LogInformation("Tuner url has changed from {PreviousUrl} to {NewUrl}", configuredDevice.Url, device.Url); diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs index 08534de59..5369c9b3d 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs @@ -208,7 +208,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV // var audioChannels = 2; // var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio); - // if (audioStream != null) + // if (audioStream is not null) // { // audioChannels = audioStream.Channels ?? audioChannels; // } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs index 58b798ce6..d5a6feb47 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs @@ -36,7 +36,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV [MemberNotNull(nameof(_items))] private void EnsureLoaded() { - if (_items != null) + if (_items is not null) { return; } @@ -49,7 +49,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { var bytes = File.ReadAllBytes(_dataPath); _items = JsonSerializer.Deserialize<T[]>(bytes, _jsonOptions); - if (_items == null) + if (_items is null) { Logger.LogError("Error deserializing {Path}, data was null", _dataPath); _items = Array.Empty<T>(); diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs index 40dcca94f..7bbeae866 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs @@ -63,7 +63,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } } } - else if (info.IsMovie && info.ProductionYear != null) + else if (info.IsMovie && info.ProductionYear is not null) { name += " (" + info.ProductionYear + ")"; } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs index b1259de23..bf28f3b67 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs @@ -16,10 +16,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV /// <inheritdoc /> public override void Add(SeriesTimerInfo item) { - if (string.IsNullOrEmpty(item.Id)) - { - throw new ArgumentException("SeriesTimerInfo.Id cannot be null or empty."); - } + ArgumentException.ThrowIfNullOrEmpty(item.Id); base.Add(item); } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs index a861e6ae4..9f8441fa4 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs @@ -74,10 +74,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV public override void Add(TimerInfo item) { - if (string.IsNullOrEmpty(item.Id)) - { - throw new ArgumentException("TimerInfo.Id cannot be null or empty."); - } + ArgumentException.ThrowIfNullOrEmpty(item.Id); base.Add(item); AddOrUpdateSystemTimer(item); @@ -122,11 +119,28 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV if (_timers.TryAdd(item.Id, timer)) { - Logger.LogInformation( - "Creating recording timer for {Id}, {Name}. Timer will fire in {Minutes} minutes", + if (item.IsSeries) + { + Logger.LogInformation( + "Creating recording timer for {Id}, {Name} {SeasonNumber}x{EpisodeNumber:D2} on channel {ChannelId}. Timer will fire in {Minutes} minutes at {StartDate}", + item.Id, + item.Name, + item.SeasonNumber, + item.EpisodeNumber, + item.ChannelId, + dueTime.TotalMinutes.ToString(CultureInfo.InvariantCulture), + item.StartDate); + } + else + { + Logger.LogInformation( + "Creating recording timer for {Id}, {Name} on channel {ChannelId}. Timer will fire in {Minutes} minutes at {StartDate}", item.Id, item.Name, - dueTime.TotalMinutes.ToString(CultureInfo.InvariantCulture)); + item.ChannelId, + dueTime.TotalMinutes.ToString(CultureInfo.InvariantCulture), + item.StartDate); + } } else { @@ -148,7 +162,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV var timerId = (string?)state ?? throw new ArgumentNullException(nameof(state)); var timer = GetAll().FirstOrDefault(i => string.Equals(i.Id, timerId, StringComparison.OrdinalIgnoreCase)); - if (timer != null) + if (timer is not null) { TimerFired?.Invoke(this, new GenericEventArgs<TimerInfo>(timer)); } diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index 4311db28d..ca3e45707 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -74,10 +74,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken) { - if (string.IsNullOrEmpty(channelId)) - { - throw new ArgumentNullException(nameof(channelId)); - } + ArgumentException.ThrowIfNullOrEmpty(channelId); // Normalize incoming input channelId = channelId.Replace(".json.schedulesdirect.org", string.Empty, StringComparison.OrdinalIgnoreCase).TrimStart('I'); @@ -111,7 +108,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings using var response = await Send(options, true, info, cancellationToken).ConfigureAwait(false); await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); var dailySchedules = await JsonSerializer.DeserializeAsync<IReadOnlyList<DayDto>>(responseStream, _jsonOptions, cancellationToken).ConfigureAwait(false); - if (dailySchedules == null) + if (dailySchedules is null) { return Array.Empty<ProgramInfo>(); } @@ -127,7 +124,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings using var innerResponse = await Send(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false); await using var innerResponseStream = await innerResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); var programDetails = await JsonSerializer.DeserializeAsync<IReadOnlyList<ProgramDetailsDto>>(innerResponseStream, _jsonOptions, cancellationToken).ConfigureAwait(false); - if (programDetails == null) + if (programDetails is null) { return Array.Empty<ProgramInfo>(); } @@ -153,7 +150,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings continue; } - if (images != null) + if (images is not null) { var imageIndex = images.FindIndex(i => i.ProgramId == schedule.ProgramId[..10]); if (imageIndex > -1) @@ -166,12 +163,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings const double DesiredAspect = 2.0 / 3; - programEntry.PrimaryImage = GetProgramImage(ApiUrl, imagesWithText, DesiredAspect) ?? - GetProgramImage(ApiUrl, allImages, DesiredAspect); + programEntry.PrimaryImage = GetProgramImage(ApiUrl, imagesWithText, DesiredAspect, token) ?? + GetProgramImage(ApiUrl, allImages, DesiredAspect, token); const double WideAspect = 16.0 / 9; - programEntry.ThumbImage = GetProgramImage(ApiUrl, imagesWithText, WideAspect); + programEntry.ThumbImage = GetProgramImage(ApiUrl, imagesWithText, WideAspect, token); // Don't supply the same image twice if (string.Equals(programEntry.PrimaryImage, programEntry.ThumbImage, StringComparison.Ordinal)) @@ -179,7 +176,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings programEntry.ThumbImage = null; } - programEntry.BackdropImage = GetProgramImage(ApiUrl, imagesWithoutText, WideAspect); + programEntry.BackdropImage = GetProgramImage(ApiUrl, imagesWithoutText, WideAspect, token); // programEntry.bannerImage = GetProgramImage(ApiUrl, data, "Banner", false) ?? // GetProgramImage(ApiUrl, data, "Banner-L1", false) ?? @@ -228,7 +225,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings private ProgramInfo GetProgram(string channelId, ProgramDto programInfo, ProgramDetailsDto details) { - if (programInfo.AirDateTime == null) + if (programInfo.AirDateTime is null) { return null; } @@ -266,7 +263,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings } string episodeTitle = null; - if (details.EpisodeTitle150 != null) + if (details.EpisodeTitle150 is not null) { episodeTitle = details.EpisodeTitle150; } @@ -283,7 +280,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings EpisodeTitle = episodeTitle, Audio = audioType, // IsNew = programInfo.@new ?? false, - IsRepeat = programInfo.New == null, + IsRepeat = programInfo.New is null, IsSeries = string.Equals(details.EntityType, "episode", StringComparison.OrdinalIgnoreCase), ImageUrl = details.PrimaryImage, ThumbImageUrl = details.ThumbImage, @@ -315,13 +312,13 @@ namespace Emby.Server.Implementations.LiveTv.Listings info.ShowId = showId; - if (programInfo.VideoProperties != null) + if (programInfo.VideoProperties is not null) { info.IsHD = programInfo.VideoProperties.Contains("hdtv", StringComparison.OrdinalIgnoreCase); info.Is3D = programInfo.VideoProperties.Contains("3d", StringComparison.OrdinalIgnoreCase); } - if (details.ContentRating != null && details.ContentRating.Count > 0) + if (details.ContentRating is not null && details.ContentRating.Count > 0) { info.OfficialRating = details.ContentRating[0].Code.Replace("TV", "TV-", StringComparison.Ordinal) .Replace("--", "-", StringComparison.Ordinal); @@ -333,13 +330,13 @@ namespace Emby.Server.Implementations.LiveTv.Listings } } - if (details.Descriptions != null) + if (details.Descriptions is not null) { - if (details.Descriptions.Description1000 != null && details.Descriptions.Description1000.Count > 0) + if (details.Descriptions.Description1000 is not null && details.Descriptions.Description1000.Count > 0) { info.Overview = details.Descriptions.Description1000[0].Description; } - else if (details.Descriptions.Description100 != null && details.Descriptions.Description100.Count > 0) + else if (details.Descriptions.Description100 is not null && details.Descriptions.Description100.Count > 0) { info.Overview = details.Descriptions.Description100[0].Description; } @@ -351,12 +348,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings info.SeriesProviderIds[MetadataProvider.Zap2It.ToString()] = info.SeriesId; - if (details.Metadata != null) + if (details.Metadata is not null) { foreach (var metadataProgram in details.Metadata) { var gracenote = metadataProgram.Gracenote; - if (gracenote != null) + if (gracenote is not null) { info.SeasonNumber = gracenote.Season; @@ -371,13 +368,13 @@ namespace Emby.Server.Implementations.LiveTv.Listings } } - if (details.OriginalAirDate != null) + if (details.OriginalAirDate is not null) { info.OriginalAirDate = details.OriginalAirDate; info.ProductionYear = info.OriginalAirDate.Value.Year; } - if (details.Movie != null) + if (details.Movie is not null) { if (!string.IsNullOrEmpty(details.Movie.Year) && int.TryParse(details.Movie.Year, out int year)) @@ -386,7 +383,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings } } - if (details.Genres != null) + if (details.Genres is not null) { info.Genres = details.Genres.Where(g => !string.IsNullOrWhiteSpace(g)).ToList(); info.IsNews = details.Genres.Contains("news", StringComparison.OrdinalIgnoreCase); @@ -400,14 +397,14 @@ namespace Emby.Server.Implementations.LiveTv.Listings return info; } - private static string GetProgramImage(string apiUrl, IEnumerable<ImageDataDto> images, double desiredAspect) + private static string GetProgramImage(string apiUrl, IEnumerable<ImageDataDto> images, double desiredAspect, string token) { var match = images .OrderBy(i => Math.Abs(desiredAspect - GetAspectRatio(i))) .ThenByDescending(i => GetSizeOrder(i)) .FirstOrDefault(); - if (match == null) + if (match is null) { return null; } @@ -418,14 +415,13 @@ namespace Emby.Server.Implementations.LiveTv.Listings { return null; } - else if (uri.IndexOf("http", StringComparison.OrdinalIgnoreCase) != -1) + + if (uri.IndexOf("http", StringComparison.OrdinalIgnoreCase) != -1) { return uri; } - else - { - return apiUrl + "/image/" + uri; - } + + return apiUrl + "/image/" + uri + "?token=" + token; } private static double GetAspectRatio(ImageDataDto i) @@ -458,6 +454,8 @@ namespace Emby.Server.Implementations.LiveTv.Listings IReadOnlyList<string> programIds, CancellationToken cancellationToken) { + var token = await GetToken(info, cancellationToken).ConfigureAwait(false); + if (programIds.Count == 0) { return Array.Empty<ShowImagesDto>(); @@ -479,6 +477,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings { Content = new StringContent(str.ToString(), Encoding.UTF8, MediaTypeNames.Application.Json) }; + message.Headers.TryAddWithoutValidation("token", token); try { @@ -515,7 +514,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings var root = await JsonSerializer.DeserializeAsync<IReadOnlyList<HeadendsDto>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false); - if (root != null) + if (root is not null) { foreach (HeadendsDto headend in root) { @@ -570,15 +569,13 @@ namespace Emby.Server.Implementations.LiveTv.Listings _tokens.TryAdd(username, savedToken); } - if (!string.IsNullOrEmpty(savedToken.Name) && !string.IsNullOrEmpty(savedToken.Value)) + if (!string.IsNullOrEmpty(savedToken.Name) + && long.TryParse(savedToken.Value, CultureInfo.InvariantCulture, out long ticks)) { - if (long.TryParse(savedToken.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out long ticks)) + // If it's under 24 hours old we can still use it + if (DateTime.UtcNow.Ticks - ticks < TimeSpan.FromHours(20).Ticks) { - // If it's under 24 hours old we can still use it - if (DateTime.UtcNow.Ticks - ticks < TimeSpan.FromHours(20).Ticks) - { - return savedToken.Name; - } + return savedToken.Name; } } @@ -667,15 +664,8 @@ namespace Emby.Server.Implementations.LiveTv.Listings { var token = await GetToken(info, cancellationToken).ConfigureAwait(false); - if (string.IsNullOrEmpty(token)) - { - throw new ArgumentException("Authentication required."); - } - - if (string.IsNullOrEmpty(info.ListingsId)) - { - throw new ArgumentException("Listings Id required"); - } + ArgumentException.ThrowIfNullOrEmpty(token); + ArgumentException.ThrowIfNullOrEmpty(info.ListingsId); _logger.LogInformation("Adding new LineUp "); @@ -686,17 +676,11 @@ namespace Emby.Server.Implementations.LiveTv.Listings private async Task<bool> HasLineup(ListingsProviderInfo info, CancellationToken cancellationToken) { - if (string.IsNullOrEmpty(info.ListingsId)) - { - throw new ArgumentException("Listings Id required"); - } + ArgumentException.ThrowIfNullOrEmpty(info.ListingsId); var token = await GetToken(info, cancellationToken).ConfigureAwait(false); - if (string.IsNullOrEmpty(token)) - { - throw new ArgumentException("token required"); - } + ArgumentException.ThrowIfNullOrEmpty(token); _logger.LogInformation("Headends on account "); @@ -729,23 +713,13 @@ namespace Emby.Server.Implementations.LiveTv.Listings { if (validateLogin) { - if (string.IsNullOrEmpty(info.Username)) - { - throw new ArgumentException("Username is required"); - } - - if (string.IsNullOrEmpty(info.Password)) - { - throw new ArgumentException("Password is required"); - } + ArgumentException.ThrowIfNullOrEmpty(info.Username); + ArgumentException.ThrowIfNullOrEmpty(info.Password); } if (validateListings) { - if (string.IsNullOrEmpty(info.ListingsId)) - { - throw new ArgumentException("Listings Id required"); - } + ArgumentException.ThrowIfNullOrEmpty(info.ListingsId); var hasLineup = await HasLineup(info, CancellationToken.None).ConfigureAwait(false); @@ -764,17 +738,11 @@ namespace Emby.Server.Implementations.LiveTv.Listings public async Task<List<ChannelInfo>> GetChannels(ListingsProviderInfo info, CancellationToken cancellationToken) { var listingsId = info.ListingsId; - if (string.IsNullOrEmpty(listingsId)) - { - throw new ArgumentException("ListingsId required"); - } + ArgumentException.ThrowIfNullOrEmpty(listingsId); var token = await GetToken(info, cancellationToken).ConfigureAwait(false); - if (string.IsNullOrEmpty(token)) - { - throw new ArgumentException("token required"); - } + ArgumentException.ThrowIfNullOrEmpty(token); using var options = new HttpRequestMessage(HttpMethod.Get, ApiUrl + "/lineups/" + listingsId); options.Headers.TryAddWithoutValidation("token", token); @@ -782,7 +750,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings using var httpResponse = await Send(options, true, info, cancellationToken).ConfigureAwait(false); await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); var root = await JsonSerializer.DeserializeAsync<ChannelDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false); - if (root == null) + if (root is null) { return new List<ChannelInfo>(); } @@ -811,7 +779,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings Name = string.IsNullOrWhiteSpace(station.Name) ? channelNumber : station.Name }; - if (station.Logo != null) + if (station.Logo is not null) { channelInfo.ImageUrl = station.Logo.Url; } diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs index 7570a2bcf..066afb956 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs @@ -32,18 +32,15 @@ namespace Emby.Server.Implementations.LiveTv.Listings private readonly IServerConfigurationManager _config; private readonly IHttpClientFactory _httpClientFactory; private readonly ILogger<XmlTvListingsProvider> _logger; - private readonly IFileSystem _fileSystem; public XmlTvListingsProvider( IServerConfigurationManager config, IHttpClientFactory httpClientFactory, - ILogger<XmlTvListingsProvider> logger, - IFileSystem fileSystem) + ILogger<XmlTvListingsProvider> logger) { _config = config; _httpClientFactory = httpClientFactory; _logger = logger; - _fileSystem = fileSystem; } public string Name => "XmlTV"; @@ -140,32 +137,33 @@ namespace Emby.Server.Implementations.LiveTv.Listings private static ProgramInfo GetProgramInfo(XmlTvProgram program, ListingsProviderInfo info) { - string episodeTitle = program.Episode?.Title; + string episodeTitle = program.Episode.Title; + var programCategories = program.Categories.Where(c => !string.IsNullOrWhiteSpace(c)).ToList(); var programInfo = new ProgramInfo { ChannelId = program.ChannelId, EndDate = program.EndDate.UtcDateTime, - EpisodeNumber = program.Episode?.Episode, + EpisodeNumber = program.Episode.Episode, EpisodeTitle = episodeTitle, - Genres = program.Categories, + Genres = programCategories, StartDate = program.StartDate.UtcDateTime, Name = program.Title, Overview = program.Description, ProductionYear = program.CopyrightDate?.Year, - SeasonNumber = program.Episode?.Series, - IsSeries = program.Episode != null, + SeasonNumber = program.Episode.Series, + IsSeries = program.Episode.Series is not null, IsRepeat = program.IsPreviouslyShown && !program.IsNew, - IsPremiere = program.Premiere != null, - IsKids = program.Categories.Any(c => info.KidsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), - IsMovie = program.Categories.Any(c => info.MovieCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), - IsNews = program.Categories.Any(c => info.NewsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), - IsSports = program.Categories.Any(c => info.SportsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), + IsPremiere = program.Premiere is not null, + IsKids = programCategories.Any(c => info.KidsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), + IsMovie = programCategories.Any(c => info.MovieCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), + IsNews = programCategories.Any(c => info.NewsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), + IsSports = programCategories.Any(c => info.SportsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), ImageUrl = string.IsNullOrEmpty(program.Icon?.Source) ? null : program.Icon.Source, HasImage = !string.IsNullOrEmpty(program.Icon?.Source), OfficialRating = string.IsNullOrEmpty(program.Rating?.Value) ? null : program.Rating.Value, CommunityRating = program.StarRating, - SeriesId = program.Episode == null ? null : program.Title.GetMD5().ToString("N", CultureInfo.InvariantCulture) + SeriesId = program.Episode.Episode is null ? null : program.Title?.GetMD5().ToString("N", CultureInfo.InvariantCulture) }; if (string.IsNullOrWhiteSpace(program.ProgramId)) @@ -246,7 +244,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings { Id = c.Id, Name = c.DisplayName, - ImageUrl = c.Icon != null && !string.IsNullOrEmpty(c.Icon.Source) ? c.Icon.Source : null, + ImageUrl = string.IsNullOrEmpty(c.Icon?.Source) ? null : c.Icon.Source, Number = string.IsNullOrWhiteSpace(c.Number) ? c.Id : c.Number }).ToList(); } diff --git a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs index c09f9cf8d..9326fbd5c 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs @@ -80,7 +80,7 @@ namespace Emby.Server.Implementations.LiveTv dto.ProgramId = GetInternalProgramId(info.ProgramId).ToString("N", CultureInfo.InvariantCulture); } - if (program != null) + if (program is not null) { dto.ProgramInfo = _dtoService.GetBaseItemDto(program, new DtoOptions()); @@ -98,7 +98,7 @@ namespace Emby.Server.Implementations.LiveTv } } - if (channel != null) + if (channel is not null) { dto.ChannelName = channel.Name; @@ -150,7 +150,7 @@ namespace Emby.Server.Implementations.LiveTv dto.ProgramId = GetInternalProgramId(info.ProgramId).ToString("N", CultureInfo.InvariantCulture); } - dto.DayPattern = info.Days == null ? null : GetDayPattern(info.Days.ToArray()); + dto.DayPattern = info.Days is null ? null : GetDayPattern(info.Days.ToArray()); FillImages(dto, info.Name, info.SeriesId); @@ -168,10 +168,10 @@ namespace Emby.Server.Implementations.LiveTv DtoOptions = new DtoOptions(false) }).FirstOrDefault(); - if (librarySeries != null) + if (librarySeries is not null) { var image = librarySeries.GetImageInfo(ImageType.Thumb, 0); - if (image != null) + if (image is not null) { try { @@ -185,7 +185,7 @@ namespace Emby.Server.Implementations.LiveTv } image = librarySeries.GetImageInfo(ImageType.Backdrop, 0); - if (image != null) + if (image is not null) { try { @@ -212,10 +212,10 @@ namespace Emby.Server.Implementations.LiveTv Name = string.IsNullOrEmpty(programSeriesId) ? seriesName : null }).FirstOrDefault(); - if (program != null) + if (program is not null) { var image = program.GetImageInfo(ImageType.Primary, 0); - if (image != null) + if (image is not null) { try { @@ -228,10 +228,10 @@ namespace Emby.Server.Implementations.LiveTv } } - if (dto.ParentBackdropImageTags == null || dto.ParentBackdropImageTags.Length == 0) + if (dto.ParentBackdropImageTags is null || dto.ParentBackdropImageTags.Length == 0) { image = program.GetImageInfo(ImageType.Backdrop, 0); - if (image != null) + if (image is not null) { try { @@ -262,10 +262,10 @@ namespace Emby.Server.Implementations.LiveTv DtoOptions = new DtoOptions(false) }).FirstOrDefault(); - if (librarySeries != null) + if (librarySeries is not null) { var image = librarySeries.GetImageInfo(ImageType.Thumb, 0); - if (image != null) + if (image is not null) { try { @@ -279,7 +279,7 @@ namespace Emby.Server.Implementations.LiveTv } image = librarySeries.GetImageInfo(ImageType.Backdrop, 0); - if (image != null) + if (image is not null) { try { @@ -305,7 +305,7 @@ namespace Emby.Server.Implementations.LiveTv DtoOptions = new DtoOptions(false) }).FirstOrDefault(); - if (program == null) + if (program is null) { program = _libraryManager.GetItemList(new InternalItemsQuery { @@ -318,10 +318,10 @@ namespace Emby.Server.Implementations.LiveTv }).FirstOrDefault(); } - if (program != null) + if (program is not null) { var image = program.GetImageInfo(ImageType.Primary, 0); - if (image != null) + if (image is not null) { try { @@ -334,10 +334,10 @@ namespace Emby.Server.Implementations.LiveTv } } - if (dto.ParentBackdropImageTags == null || dto.ParentBackdropImageTags.Length == 0) + if (dto.ParentBackdropImageTags is null || dto.ParentBackdropImageTags.Length == 0) { image = program.GetImageInfo(ImageType.Backdrop, 0); - if (image != null) + if (image is not null) { try { @@ -460,7 +460,7 @@ namespace Emby.Server.Implementations.LiveTv { var channel = _libraryManager.GetItemById(dto.ChannelId); - if (channel != null) + if (channel is not null) { info.ChannelId = channel.ExternalId; } @@ -470,7 +470,7 @@ namespace Emby.Server.Implementations.LiveTv { var program = _libraryManager.GetItemById(dto.ProgramId); - if (program != null) + if (program is not null) { info.ProgramId = program.ExternalId; } @@ -480,7 +480,7 @@ namespace Emby.Server.Implementations.LiveTv { var timer = await liveTv.GetSeriesTimer(dto.SeriesTimerId, cancellationToken).ConfigureAwait(false); - if (timer != null) + if (timer is not null) { info.SeriesTimerId = timer.ExternalId; } @@ -526,7 +526,7 @@ namespace Emby.Server.Implementations.LiveTv { var channel = _libraryManager.GetItemById(dto.ChannelId); - if (channel != null) + if (channel is not null) { info.ChannelId = channel.ExternalId; } @@ -536,7 +536,7 @@ namespace Emby.Server.Implementations.LiveTv { var program = _libraryManager.GetItemById(dto.ProgramId); - if (program != null) + if (program is not null) { info.ProgramId = program.ExternalId; } diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs index 97c2e6e30..ee039ff0f 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs @@ -434,7 +434,7 @@ namespace Emby.Server.Implementations.LiveTv var item = _libraryManager.GetItemById(id) as LiveTvChannel; - if (item == null) + if (item is null) { item = new LiveTvChannel { @@ -446,7 +446,7 @@ namespace Emby.Server.Implementations.LiveTv isNew = true; } - if (channelInfo.Tags != null) + if (channelInfo.Tags is not null) { if (!channelInfo.Tags.SequenceEqual(item.Tags, StringComparer.OrdinalIgnoreCase)) { @@ -836,7 +836,7 @@ namespace Emby.Server.Implementations.LiveTv { var seriesTimers = await GetSeriesTimersInternal(new SeriesTimerQuery(), cancellationToken).ConfigureAwait(false); var seriesTimer = seriesTimers.Items.FirstOrDefault(i => string.Equals(_tvDtoService.GetInternalSeriesTimerId(i.Id).ToString("N", CultureInfo.InvariantCulture), query.SeriesTimerId, StringComparison.OrdinalIgnoreCase)); - if (seriesTimer != null) + if (seriesTimer is not null) { internalQuery.ExternalSeriesId = seriesTimer.SeriesId; @@ -948,7 +948,7 @@ namespace Emby.Server.Implementations.LiveTv var channel = _libraryManager.GetItemById(program.ChannelId); - if (channel == null) + if (channel is null) { return score; } @@ -989,7 +989,7 @@ namespace Emby.Server.Implementations.LiveTv var timer = timerList.FirstOrDefault(i => string.Equals(i.ProgramId, externalProgramId, StringComparison.OrdinalIgnoreCase)); var foundSeriesTimer = false; - if (timer != null) + if (timer is not null) { if (timer.Status != RecordingStatus.Cancelled && timer.Status != RecordingStatus.Error) { @@ -1016,7 +1016,7 @@ namespace Emby.Server.Implementations.LiveTv var seriesTimer = seriesTimerList.FirstOrDefault(i => string.Equals(i.SeriesId, externalSeriesId, StringComparison.OrdinalIgnoreCase)); - if (seriesTimer != null) + if (seriesTimer is not null) { program.SeriesTimerId = _tvDtoService.GetInternalSeriesTimerId(seriesTimer.Id) .ToString("N", CultureInfo.InvariantCulture); @@ -1086,7 +1086,7 @@ namespace Emby.Server.Implementations.LiveTv var coreService = _services.OfType<EmbyTV.EmbyTV>().FirstOrDefault(); - if (coreService != null) + if (coreService is not null) { await coreService.RefreshSeriesTimers(cancellationToken).ConfigureAwait(false); await coreService.RefreshTimers(cancellationToken).ConfigureAwait(false); @@ -1280,7 +1280,7 @@ namespace Emby.Server.Implementations.LiveTv { var item = _libraryManager.GetItemById(itemId); - if (item != null) + if (item is not null) { _libraryManager.DeleteItem( item, @@ -1312,20 +1312,19 @@ namespace Emby.Server.Implementations.LiveTv return 7; } - private QueryResult<BaseItem> GetEmbyRecordings(RecordingQuery query, DtoOptions dtoOptions, User user) + private async Task<QueryResult<BaseItem>> GetEmbyRecordingsAsync(RecordingQuery query, DtoOptions dtoOptions, User user) { - if (user == null) + if (user is null) { return new QueryResult<BaseItem>(); } - var folderIds = GetRecordingFolders(user, true) - .Select(i => i.Id) - .ToList(); + var folders = await GetRecordingFoldersAsync(user, true).ConfigureAwait(false); + var folderIds = Array.ConvertAll(folders, x => x.Id); var excludeItemTypes = new List<BaseItemKind>(); - if (folderIds.Count == 0) + if (folderIds.Length == 0) { return new QueryResult<BaseItem>(); } @@ -1377,7 +1376,7 @@ namespace Emby.Server.Implementations.LiveTv limit = null; // var allActivePaths = EmbyTV.EmbyTV.Current.GetAllActiveRecordings().Select(i => i.Path).ToArray(); - // var items = allActivePaths.Select(i => _libraryManager.FindByPath(i, false)).Where(i => i != null).ToArray(); + // var items = allActivePaths.Select(i => _libraryManager.FindByPath(i, false)).Where(i => i is not null).ToArray(); // return new QueryResult<BaseItem> // { @@ -1392,7 +1391,7 @@ namespace Emby.Server.Implementations.LiveTv { MediaTypes = new[] { MediaType.Video }, Recursive = true, - AncestorIds = folderIds.ToArray(), + AncestorIds = folderIds, IsFolder = false, IsVirtualItem = false, Limit = limit, @@ -1517,7 +1516,7 @@ namespace Emby.Server.Implementations.LiveTv dto.CompletionPercentage = pct; } - if (channel != null) + if (channel is not null) { dto.ChannelName = channel.Name; @@ -1528,7 +1527,7 @@ namespace Emby.Server.Implementations.LiveTv } } - public QueryResult<BaseItemDto> GetRecordings(RecordingQuery query, DtoOptions options) + public async Task<QueryResult<BaseItemDto>> GetRecordingsAsync(RecordingQuery query, DtoOptions options) { var user = query.UserId.Equals(default) ? null @@ -1536,7 +1535,7 @@ namespace Emby.Server.Implementations.LiveTv RemoveFields(options); - var internalResult = GetEmbyRecordings(query, options, user); + var internalResult = await GetEmbyRecordingsAsync(query, options, user).ConfigureAwait(false); var returnArray = _dtoService.GetBaseItemDtos(internalResult.Items, options, user); @@ -1702,7 +1701,7 @@ namespace Emby.Server.Implementations.LiveTv { var timer = await GetTimer(id, CancellationToken.None).ConfigureAwait(false); - if (timer == null) + if (timer is null) { throw new ResourceNotFoundException(string.Format(CultureInfo.InvariantCulture, "Timer with Id {0} not found", id)); } @@ -1721,7 +1720,7 @@ namespace Emby.Server.Implementations.LiveTv { var timer = await GetSeriesTimer(id, CancellationToken.None).ConfigureAwait(false); - if (timer == null) + if (timer is null) { throw new ResourceNotFoundException(string.Format(CultureInfo.InvariantCulture, "SeriesTimer with Id {0} not found", id)); } @@ -1834,7 +1833,7 @@ namespace Emby.Server.Implementations.LiveTv { var internalChannelId = _tvDtoService.GetInternalChannelId(i.Item2.Name, i.Item1.ChannelId); var channel = _libraryManager.GetItemById(internalChannelId); - channelName = channel == null ? null : channel.Name; + channelName = channel is null ? null : channel.Name; } return _tvDtoService.GetSeriesTimerInfoDto(i.Item1, i.Item2, channelName); @@ -1887,7 +1886,7 @@ namespace Emby.Server.Implementations.LiveTv { var currentProgram = programs.FirstOrDefault(i => channel.Id.Equals(i.ChannelId)); - if (currentProgram != null) + if (currentProgram is not null) { currentProgramsList.Add(currentProgram); } @@ -1913,7 +1912,7 @@ namespace Emby.Server.Implementations.LiveTv ILiveTvService service = null; ProgramInfo programInfo = null; - if (program != null) + if (program is not null) { service = GetService(program); @@ -2147,7 +2146,7 @@ namespace Emby.Server.Implementations.LiveTv var service = _services.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture), parts[0], StringComparison.OrdinalIgnoreCase)); - if (service == null) + if (service is null) { throw new ArgumentException("Service not found."); } @@ -2178,7 +2177,7 @@ namespace Emby.Server.Implementations.LiveTv var provider = _tunerHosts.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase)); - if (provider == null) + if (provider is null) { throw new ResourceNotFoundException(); } @@ -2222,7 +2221,7 @@ namespace Emby.Server.Implementations.LiveTv var provider = _listingProviders.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase)); - if (provider == null) + if (provider is null) { throw new ResourceNotFoundException( string.Format( @@ -2317,7 +2316,7 @@ namespace Emby.Server.Implementations.LiveTv var providerChannel = EmbyTV.EmbyTV.Current.GetEpgChannelFromTunerChannel(mappings, tunerChannel, providerChannels); - if (providerChannel != null) + if (providerChannel is not null) { result.ProviderChannelName = providerChannel.Name; result.ProviderChannelId = providerChannel.Id; @@ -2334,7 +2333,7 @@ namespace Emby.Server.Implementations.LiveTv { var provider = _listingProviders.FirstOrDefault(i => string.Equals(providerType, i.Type, StringComparison.OrdinalIgnoreCase)); - if (provider == null) + if (provider is null) { throw new ResourceNotFoundException(); } @@ -2347,7 +2346,7 @@ namespace Emby.Server.Implementations.LiveTv var provider = _listingProviders.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase)); - if (provider == null) + if (provider is null) { throw new ResourceNotFoundException(); } @@ -2379,32 +2378,32 @@ namespace Emby.Server.Implementations.LiveTv return _tvDtoService.GetInternalProgramId(externalId); } - public List<BaseItem> GetRecordingFolders(User user) - { - return GetRecordingFolders(user, false); - } + /// <inheritdoc /> + public Task<BaseItem[]> GetRecordingFoldersAsync(User user) + => GetRecordingFoldersAsync(user, false); - private List<BaseItem> GetRecordingFolders(User user, bool refreshChannels) + private async Task<BaseItem[]> GetRecordingFoldersAsync(User user, bool refreshChannels) { var folders = EmbyTV.EmbyTV.Current.GetRecordingFolders() .SelectMany(i => i.Locations) .Distinct(StringComparer.OrdinalIgnoreCase) .Select(i => _libraryManager.FindByPath(i, true)) - .Where(i => i != null && i.IsVisibleStandalone(user)) + .Where(i => i is not null && i.IsVisibleStandalone(user)) .SelectMany(i => _libraryManager.GetCollectionFolders(i)) - .GroupBy(x => x.Id) - .Select(x => x.First()) + .DistinctBy(x => x.Id) .OrderBy(i => i.SortName) .ToList(); - folders.AddRange(_channelManager.GetChannelsInternal(new MediaBrowser.Model.Channels.ChannelQuery + var channels = await _channelManager.GetChannelsInternalAsync(new MediaBrowser.Model.Channels.ChannelQuery { UserId = user.Id, IsRecordingsFolder = true, RefreshLatestChannelItems = refreshChannels - }).Items); + }).ConfigureAwait(false); + + folders.AddRange(channels.Items); - return folders.Cast<BaseItem>().ToList(); + return folders.Cast<BaseItem>().ToArray(); } } } diff --git a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs index 4b7584af3..6a92fc599 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs @@ -42,7 +42,7 @@ namespace Emby.Server.Implementations.LiveTv { var activeRecordingInfo = _liveTvManager.GetActiveRecordingInfo(item.Path); - if (string.IsNullOrEmpty(item.Path) || activeRecordingInfo != null) + if (string.IsNullOrEmpty(item.Path) || activeRecordingInfo is not null) { return GetMediaSourcesInternal(item, activeRecordingInfo, cancellationToken); } @@ -59,7 +59,7 @@ namespace Emby.Server.Implementations.LiveTv try { - if (activeRecordingInfo != null) + if (activeRecordingInfo is not null) { sources = await EmbyTV.EmbyTV.Current.GetRecordingStreamMediaSources(activeRecordingInfo, cancellationToken) .ConfigureAwait(false); diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs index 2b82f2462..7b6c8b80a 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs @@ -131,10 +131,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts public async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken) { - if (string.IsNullOrEmpty(channelId)) - { - throw new ArgumentNullException(nameof(channelId)); - } + ArgumentException.ThrowIfNullOrEmpty(channelId); if (IsValidChannelId(channelId)) { @@ -147,7 +144,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts var channels = await GetChannels(host, true, cancellationToken).ConfigureAwait(false); var channelInfo = channels.FirstOrDefault(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase)); - if (channelInfo != null) + if (channelInfo is not null) { return await GetChannelStreamMediaSources(host, channelInfo, cancellationToken).ConfigureAwait(false); } @@ -166,10 +163,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts public async Task<ILiveStream> GetChannelStream(string channelId, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken) { - if (string.IsNullOrEmpty(channelId)) - { - throw new ArgumentNullException(nameof(channelId)); - } + ArgumentException.ThrowIfNullOrEmpty(channelId); if (!IsValidChannelId(channelId)) { @@ -187,7 +181,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts var channels = await GetChannels(host, true, cancellationToken).ConfigureAwait(false); var channelInfo = channels.FirstOrDefault(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase)); - if (channelInfo != null) + if (channelInfo is not null) { hostsWithChannel.Add(new Tuple<TunerHostInfo, ChannelInfo>(host, channelInfo)); } @@ -223,10 +217,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts protected virtual bool IsValidChannelId(string channelId) { - if (string.IsNullOrEmpty(channelId)) - { - throw new ArgumentNullException(nameof(channelId)); - } + ArgumentException.ThrowIfNullOrEmpty(channelId); return channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase); } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index e0eaa8e58..98bbc1540 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -14,6 +14,7 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Extensions; using Jellyfin.Extensions.Json; +using Jellyfin.Extensions.Json.Converters; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller; @@ -58,7 +59,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun _socketFactory = socketFactory; _streamHelper = streamHelper; - _jsonOptions = JsonDefaults.Options; + _jsonOptions = new JsonSerializerOptions(JsonDefaults.Options); + _jsonOptions.Converters.Add(new JsonBoolNumberConverter()); } public string Name => "HD Homerun"; @@ -302,7 +304,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun var hdHomerunChannelInfo = channels.FirstOrDefault() as HdHomerunChannelInfo; - if (hdHomerunChannelInfo == null || hdHomerunChannelInfo.IsLegacyTuner) + if (hdHomerunChannelInfo is null || hdHomerunChannelInfo.IsLegacyTuner) { return await GetTunerInfosUdp(info, cancellationToken).ConfigureAwait(false); } @@ -503,7 +505,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { var modelInfo = await GetModelInfo(tuner, false, cancellationToken).ConfigureAwait(false); - if (modelInfo != null && modelInfo.SupportsTranscoding) + if (modelInfo is not null && modelInfo.SupportsTranscoding) { if (tuner.AllowHWTranscoding) { @@ -560,7 +562,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun var mediaSource = GetMediaSource(tunerHost, hdhrId, channel, profile); - if (hdhomerunChannel != null && hdhomerunChannel.IsLegacyTuner) + if (hdhomerunChannel is not null && hdhomerunChannel.IsLegacyTuner) { return new HdHomerunUdpStream( mediaSource, @@ -674,7 +676,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun var info = await TryGetTunerHostInfo(deviceAddress, cancellationToken).ConfigureAwait(false); - if (info != null) + if (info is not null) { list.Add(info); } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs index 48d9e316d..7bc209d6b 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs @@ -37,7 +37,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { using (var socket = _tcpClient) { - if (socket != null) + if (socket is not null) { _tcpClient = null; @@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun public async Task<bool> CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken) { using var client = new TcpClient(); - await client.ConnectAsync(remoteIp, HdHomeRunPort).ConfigureAwait(false); + await client.ConnectAsync(remoteIp, HdHomeRunPort, cancellationToken).ConfigureAwait(false); using var stream = client.GetStream(); return await CheckTunerAvailability(stream, tuner, cancellationToken).ConfigureAwait(false); @@ -67,7 +67,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun int receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); - return VerifyReturnValueOfGetSet(buffer.AsSpan(receivedBytes), "none"); + return VerifyReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), "none"); } finally { diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs index 80d9d0724..3450f971f 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs @@ -13,8 +13,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun public LegacyHdHomerunChannelCommands(string url) { // parse url for channel and program - var regExp = new Regex(@"\/ch([0-9]+)-?([0-9]*)"); - var match = regExp.Match(url); + var match = Regex.Match(url, @"\/ch([0-9]+)-?([0-9]*)"); if (match.Success) { _channel = match.Groups[1].Value; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs index 2748794b3..3ae9e256b 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs @@ -35,7 +35,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts EnableStreamSharing = true; UniqueId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); - if (tuner != null) + if (tuner is not null) { TunerHostId = tuner.Id; } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs index a423ec8f4..b41816230 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs @@ -122,9 +122,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts var attributes = ParseExtInf(extInf, out string remaining); extInf = remaining; - if (attributes.TryGetValue("tvg-logo", out string value)) + if (attributes.TryGetValue("tvg-logo", out string tvgLogo)) { - channel.ImageUrl = value; + channel.ImageUrl = tvgLogo; + } + else if (attributes.TryGetValue("logo", out string logo)) + { + channel.ImageUrl = logo; } if (attributes.TryGetValue("group-title", out string groupTitle)) @@ -166,30 +170,25 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts var nameInExtInf = nameParts.Length > 1 ? nameParts[^1].AsSpan().Trim() : ReadOnlySpan<char>.Empty; string numberString = null; - string attributeValue; - if (attributes.TryGetValue("tvg-chno", out attributeValue)) + if (attributes.TryGetValue("tvg-chno", out var attributeValue) + && double.TryParse(attributeValue, CultureInfo.InvariantCulture, out _)) { - if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _)) - { - numberString = attributeValue; - } + numberString = attributeValue; } if (!IsValidChannelNumber(numberString)) { if (attributes.TryGetValue("tvg-id", out attributeValue)) { - if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _)) + if (double.TryParse(attributeValue, CultureInfo.InvariantCulture, out _)) { numberString = attributeValue; } - else if (attributes.TryGetValue("channel-id", out attributeValue)) + else if (attributes.TryGetValue("channel-id", out attributeValue) + && double.TryParse(attributeValue, CultureInfo.InvariantCulture, out _)) { - if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _)) - { - numberString = attributeValue; - } + numberString = attributeValue; } } @@ -207,7 +206,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { var numberPart = nameInExtInf.Slice(0, numberIndex).Trim(new[] { ' ', '.' }); - if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out _)) + if (double.TryParse(numberPart, CultureInfo.InvariantCulture, out _)) { numberString = numberPart.ToString(); } @@ -255,19 +254,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts private static bool IsValidChannelNumber(string numberString) { - if (string.IsNullOrWhiteSpace(numberString) || - string.Equals(numberString, "-1", StringComparison.OrdinalIgnoreCase) || - string.Equals(numberString, "0", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - - if (!double.TryParse(numberString, NumberStyles.Any, CultureInfo.InvariantCulture, out _)) + if (string.IsNullOrWhiteSpace(numberString) + || string.Equals(numberString, "-1", StringComparison.Ordinal) + || string.Equals(numberString, "0", StringComparison.Ordinal)) { return false; } - return true; + return double.TryParse(numberString, CultureInfo.InvariantCulture, out _); } private static string GetChannelName(string extInf, Dictionary<string, string> attributes) @@ -285,7 +279,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { var numberPart = nameInExtInf.Substring(0, numberIndex).Trim(new[] { ' ', '.' }); - if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out _)) + if (double.TryParse(numberPart, CultureInfo.InvariantCulture, out _)) { // channel.Number = number.ToString(); nameInExtInf = nameInExtInf.Substring(numberIndex + 1).Trim(new[] { ' ', '-' }); @@ -317,8 +311,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - var reg = new Regex(@"([a-z0-9\-_]+)=\""([^""]+)\""", RegexOptions.IgnoreCase); - var matches = reg.Matches(line); + var matches = Regex.Matches(line, @"([a-z0-9\-_]+)=\""([^""]+)\""", RegexOptions.IgnoreCase); remaining = line; diff --git a/Emby.Server.Implementations/Localization/Core/af.json b/Emby.Server.Implementations/Localization/Core/af.json index f356c98a9..9fbf364ef 100644 --- a/Emby.Server.Implementations/Localization/Core/af.json +++ b/Emby.Server.Implementations/Localization/Core/af.json @@ -122,5 +122,6 @@ "TaskOptimizeDatabase": "Optimaliseer databasis", "TaskKeyframeExtractorDescription": "Haal keyframes vanuit video lêers om meer presiese HLS afspeellyste te maak. Dit kan lank duur.", "TaskKeyframeExtractor": "Keyframe Ekstraktor", - "External": "Ekstern" + "External": "Ekstern", + "HearingImpaired": "gehoorgestremd" } diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json index 9dc2fe799..93d50e6e3 100644 --- a/Emby.Server.Implementations/Localization/Core/ar.json +++ b/Emby.Server.Implementations/Localization/Core/ar.json @@ -3,20 +3,20 @@ "AppDeviceValues": "تطبيق: {0}, جهاز: {1}", "Application": "تطبيق", "Artists": "الفنانين", - "AuthenticationSucceededWithUserName": "تمت مصادقة {0} بنجاح", + "AuthenticationSucceededWithUserName": "نجحت عملية التوثيق بـ {0}", "Books": "الكتب", - "CameraImageUploadedFrom": "صورة كاميرا جديدة تم رفعها من {0}", + "CameraImageUploadedFrom": "رُفعت صورة الكاميرا الجديدة من {0}", "Channels": "القنوات", "ChapterNameValue": "الفصل {0}", "Collections": "التجميعات", "DeviceOfflineWithName": "قُطِع الاتصال ب{0}", "DeviceOnlineWithName": "{0} متصل", "FailedLoginAttemptWithUserName": "محاولة تسجيل الدخول فشلت من {0}", - "Favorites": "مفضلات", + "Favorites": "المفضلة", "Folders": "المجلدات", "Genres": "التصنيفات", "HeaderAlbumArtists": "فناني الألبوم", - "HeaderContinueWatching": "استمر بالمشاهدة", + "HeaderContinueWatching": "استئناف المشاهدة", "HeaderFavoriteAlbums": "الألبومات المفضلة", "HeaderFavoriteArtists": "الفنانون المفضلون", "HeaderFavoriteEpisodes": "الحلقات المفضلة", @@ -27,15 +27,15 @@ "HeaderRecordingGroups": "مجموعات التسجيل", "HomeVideos": "الفيديوهات الشخصية", "Inherit": "توريث", - "ItemAddedWithName": "تم إضافة {0} للمكتبة", - "ItemRemovedWithName": "تم إزالة {0} من المكتبة", + "ItemAddedWithName": "أُضيف {0} للمكتبة", + "ItemRemovedWithName": "أُزيل {0} من المكتبة", "LabelIpAddressValue": "عنوان الآي بي: {0}", "LabelRunningTimeValue": "مدة التشغيل: {0}", "Latest": "أحدث", - "MessageApplicationUpdated": "لقد تم تحديث خادم Jellyfin", - "MessageApplicationUpdatedTo": "تم تحديث خادم Jellyfin الى {0}", - "MessageNamedServerConfigurationUpdatedWithValue": "تم تحديث إعدادات الخادم في قسم {0}", - "MessageServerConfigurationUpdated": "تم تحديث إعدادات الخادم", + "MessageApplicationUpdated": "حُدث خادم Jellyfin", + "MessageApplicationUpdatedTo": "حُدث خادم Jellyfin إلى {0}", + "MessageNamedServerConfigurationUpdatedWithValue": "حُدثت إعدادات الخادم في قسم {0}", + "MessageServerConfigurationUpdated": "حُدثت إعدادات الخادم", "MixedContent": "محتوى مختلط", "Movies": "الأفلام", "Music": "الموسيقى", @@ -45,14 +45,14 @@ "NameSeasonUnknown": "الموسم غير معروف", "NewVersionIsAvailable": "نسخة جديدة من خادم Jellyfin متوفرة للتحميل.", "NotificationOptionApplicationUpdateAvailable": "يوجد تحديث للتطبيق", - "NotificationOptionApplicationUpdateInstalled": "تم تحديث التطبيق", + "NotificationOptionApplicationUpdateInstalled": "نُصب تحديث التطبيق", "NotificationOptionAudioPlayback": "بدأ تشغيل المقطع الصوتي", - "NotificationOptionAudioPlaybackStopped": "تم إيقاف تشغيل المقطع الصوتي", - "NotificationOptionCameraImageUploaded": "تم رفع صورة الكاميرا", + "NotificationOptionAudioPlaybackStopped": "أُوقف تشغيل المقطع الصوتي", + "NotificationOptionCameraImageUploaded": "رُفعت صورة الكاميرا", "NotificationOptionInstallationFailed": "فشل في التثبيت", - "NotificationOptionNewLibraryContent": "تم إضافة محتوى جديد", + "NotificationOptionNewLibraryContent": "أُضيف محتوى جديدا", "NotificationOptionPluginError": "فشل في الملحق", - "NotificationOptionPluginInstalled": "تم تثبيت الملحق", + "NotificationOptionPluginInstalled": "ثُبتت المكونات الإضافية", "NotificationOptionPluginUninstalled": "تمت إزالة الملحق", "NotificationOptionPluginUpdateInstalled": "تم تثبيت تحديثات الملحق", "NotificationOptionServerRestartRequired": "يجب إعادة تشغيل الخادم", @@ -91,13 +91,13 @@ "UserStoppedPlayingItemWithValues": "قام {0} بإيقاف تشغيل {1} على {2}", "ValueHasBeenAddedToLibrary": "تمت اضافت {0} إلى مكتبة الوسائط", "ValueSpecialEpisodeName": "حلقه خاصه - {0}", - "VersionNumber": "النسخة {0}", + "VersionNumber": "الإصدار {0}", "TaskCleanCacheDescription": "يحذف الملفات المؤقتة التي لم يعد النظام بحاجة إليها.", "TaskCleanCache": "احذف ما بمجلد الملفات المؤقتة", "TasksChannelsCategory": "قنوات الإنترنت", "TasksLibraryCategory": "مكتبة", "TasksMaintenanceCategory": "صيانة", - "TaskRefreshLibraryDescription": "يفصح مكتبة الوسائط الخاصة بك بحثًا عن ملفات جديدة، ومن ثم يتحدث البيانات الوصفية.", + "TaskRefreshLibraryDescription": "يفحص مكتبة الوسائط الخاصة بك باحثا عن ملفات جديدة، ومن ثم يُحدث البيانات الوصفية.", "TaskRefreshLibrary": "افحص مكتبة الوسائط", "TaskRefreshChapterImagesDescription": "يُنشئ صور مصغرة لمقاطع الفيديو التي تحتوي على فصول.", "TaskRefreshChapterImages": "استخراج صور الفصل", @@ -123,5 +123,6 @@ "TaskOptimizeDatabase": "تحسين قاعدة البيانات", "TaskKeyframeExtractorDescription": "يستخرج الإطارات الرئيسية من ملفات الفيديو لكي ينشئ قوائم تشغيل بث HTTP المباشر. قد تستمر هذه العملية لوقت طويل.", "TaskKeyframeExtractor": "مستخرج الإطار الرئيسي", - "External": "خارجي" + "External": "خارجي", + "HearingImpaired": "ضعاف السمع" } diff --git a/Emby.Server.Implementations/Localization/Core/be.json b/Emby.Server.Implementations/Localization/Core/be.json index 56c4e7d39..3af124678 100644 --- a/Emby.Server.Implementations/Localization/Core/be.json +++ b/Emby.Server.Implementations/Localization/Core/be.json @@ -1,4 +1,127 @@ { - "Sync": "Сінхранізацыя", - "Playlists": "Плэйліст" + "Sync": "Сінхранізаваць", + "Playlists": "Плэйлісты", + "Latest": "Апошні", + "LabelIpAddressValue": "IP-адрас: {0}", + "ItemAddedWithName": "{0} быў дададзены ў бібліятэку", + "MessageApplicationUpdated": "Сервер Jellyfin абноўлены", + "NotificationOptionApplicationUpdateInstalled": "Абнаўленне прыкладання ўсталявана", + "PluginInstalledWithName": "{0} быў усталяваны", + "UserCreatedWithName": "Карыстальнік {0} быў створаны", + "Albums": "Альбомы", + "Application": "Прыкладанне", + "AuthenticationSucceededWithUserName": "{0} паспяхова аўтэнтыфікаваны", + "Channels": "Каналы", + "ChapterNameValue": "Раздзел {0}", + "Collections": "Калекцыі", + "Default": "Па змаўчанні", + "FailedLoginAttemptWithUserName": "Няўдалая спроба ўваходу з {0}", + "Folders": "Папкі", + "Favorites": "Абранае", + "External": "Знешні", + "Genres": "Жанры", + "HeaderContinueWatching": "Працягнуць прагляд", + "HeaderFavoriteAlbums": "Абраныя альбомы", + "HeaderFavoriteEpisodes": "Абраныя серыі", + "HeaderFavoriteShows": "Абраныя шоу", + "HeaderFavoriteSongs": "Абраныя песні", + "HeaderLiveTV": "Прамы эфір", + "HeaderAlbumArtists": "Выканаўцы альбома", + "LabelRunningTimeValue": "Працягласць: {0}", + "HomeVideos": "Хатнія відэа", + "ItemRemovedWithName": "{0} быў выдалены з бібліятэкі", + "MessageApplicationUpdatedTo": "Сервер Jellyfin абноўлены да {0}", + "Movies": "Фільмы", + "Music": "Музыка", + "MusicVideos": "Музычныя кліпы", + "NameInstallFailed": "Устаноўка {0} не атрымалася", + "NameSeasonNumber": "Сезон {0}", + "NotificationOptionApplicationUpdateAvailable": "Даступна абнаўленне прыкладання", + "NotificationOptionPluginInstalled": "Плагін усталяваны", + "NotificationOptionPluginUpdateInstalled": "Абнаўленне плагіна усталявана", + "NotificationOptionServerRestartRequired": "Патрабуецца перазапуск сервера", + "Photos": "Фатаграфіі", + "Plugin": "Плагін", + "PluginUninstalledWithName": "{0} быў выдалены", + "PluginUpdatedWithName": "{0} быў абноўлены", + "ProviderValue": "Пастаўшчык: {0}", + "Songs": "Песні", + "System": "Сістэма", + "User": "Карыстальнік", + "UserDeletedWithName": "Карыстальнік {0} быў выдалены", + "UserDownloadingItemWithValues": "{0} спампоўваецца {1}", + "TaskOptimizeDatabase": "Аптымізаваць базу дадзеных", + "Artists": "Выканаўцы", + "UserOfflineFromDevice": "{0} адключыўся ад {1}", + "UserPolicyUpdatedWithName": "Палітыка карыстальніка абноўлена для {0}", + "TaskCleanActivityLogDescription": "Выдаляе старэйшыя за зададзены ўзрост запісы ў журнале актыўнасці.", + "TaskRefreshChapterImagesDescription": "Стварае мініяцюры для відэа, якія маюць раздзелы.", + "TaskCleanLogsDescription": "Выдаляе файлы журналу, якім больш за {0} дзён.", + "TaskUpdatePluginsDescription": "Спампоўвае і ўсталёўвае абнаўленні для плагінаў, якія настроены на аўтаматычнае абнаўленне.", + "TaskRefreshChannelsDescription": "Абнаўляе інфармацыю аб інтэрнэт-канале.", + "TaskDownloadMissingSubtitlesDescription": "Шукае ў інтэрнэце адсутныя субтытры на аснове канфігурацыі метададзеных.", + "TaskOptimizeDatabaseDescription": "Ушчыльняе базу дадзеных і скарачае вольную прастору. Выкананне гэтай задачы пасля сканавання бібліятэкі або ўнясення іншых змяненняў, якія прадугледжваюць мадыфікацыю базы дадзеных, можа палепшыць прадукцыйнасць.", + "TaskKeyframeExtractor": "Экстрактар ключавых кадраў", + "TasksApplicationCategory": "Прыкладанне", + "AppDeviceValues": "Прыкладанне: {0}, Прылада: {1}", + "Books": "Кнігі", + "CameraImageUploadedFrom": "Новая выява камеры была загружана з {0}", + "DeviceOfflineWithName": "{0} адключыўся", + "DeviceOnlineWithName": "{0} падлучаны", + "Forced": "Прымусова", + "HeaderRecordingGroups": "Групы запісаў", + "HeaderNextUp": "Наступнае", + "HeaderFavoriteArtists": "Абраныя выканаўцы", + "HearingImpaired": "Са слабым слыхам", + "Inherit": "Атрымаць у спадчыну", + "MessageNamedServerConfigurationUpdatedWithValue": "Канфігурацыя сервера {0} абноўлена", + "MessageServerConfigurationUpdated": "Канфігурацыя сервера абноўлена", + "MixedContent": "Змешаны змест", + "NameSeasonUnknown": "Невядомы сезон", + "NotificationOptionInstallationFailed": "Збой усталёўкі", + "NewVersionIsAvailable": "Новая версія сервера Jellyfin даступная для cпампоўкі.", + "NotificationOptionCameraImageUploaded": "Выява камеры запампавана", + "NotificationOptionAudioPlaybackStopped": "Прайграванне аўдыё спынена", + "NotificationOptionAudioPlayback": "Прайграванне аўдыё пачалося", + "NotificationOptionNewLibraryContent": "Дададзены новы кантэнт", + "NotificationOptionPluginError": "Збой плагіна", + "NotificationOptionPluginUninstalled": "Плагін выдалены", + "NotificationOptionTaskFailed": "Збой запланаванага задання", + "NotificationOptionUserLockedOut": "Карыстальнік заблакіраваны", + "NotificationOptionVideoPlayback": "Пачалося прайграванне відэа", + "NotificationOptionVideoPlaybackStopped": "Прайграванне відэа спынена", + "ScheduledTaskFailedWithName": "{0} не атрымалася", + "ScheduledTaskStartedWithName": "{0} пачалося", + "ServerNameNeedsToBeRestarted": "{0} трэба перазапусціць", + "Shows": "Шоу", + "StartupEmbyServerIsLoading": "Jellyfin Server загружаецца. Калі ласка, паўтарыце спробу крыху пазней.", + "SubtitleDownloadFailureFromForItem": "Не атрымалася спампаваць субтытры з {0} для {1}", + "TvShows": "ТБ-шоу", + "Undefined": "Нявызначана", + "UserLockedOutWithName": "Карыстальнік {0} быў заблакіраваны", + "UserOnlineFromDevice": "{0} падключаны з {1}", + "UserPasswordChangedWithName": "Пароль быў зменены для карыстальніка {0}", + "UserStartedPlayingItemWithValues": "{0} грае {1} на {2}", + "UserStoppedPlayingItemWithValues": "{0} скончыў прайграванне {1} на {2}", + "ValueHasBeenAddedToLibrary": "{0} быў дададзены ў вашу медыятэку", + "ValueSpecialEpisodeName": "Спецэпізод - {0}", + "VersionNumber": "Версія {0}", + "TasksMaintenanceCategory": "Абслугоўванне", + "TasksLibraryCategory": "Медыятэка", + "TasksChannelsCategory": "Інтэрнэт-каналы", + "TaskCleanActivityLog": "Ачысціць журнал актыўнасці", + "TaskCleanCache": "Ачысціць кэш", + "TaskCleanCacheDescription": "Выдаляе файлы кэша, якія больш не патрэбныя сістэме.", + "TaskRefreshChapterImages": "Выняць выявы раздзелаў", + "TaskRefreshLibrary": "Сканіраваць медыятэку", + "TaskRefreshLibraryDescription": "Сканіруе вашу медыятэку на наяўнасць новых файлаў і абнаўляе метададзеныя.", + "TaskCleanLogs": "Ачысціць часопіс", + "TaskRefreshPeople": "Абнавіць людзей", + "TaskRefreshPeopleDescription": "Абнаўленне метаданых для акцёраў і рэжысёраў у вашай медыятэцы.", + "TaskUpdatePlugins": "Абнавіць плагіны", + "TaskCleanTranscode": "Ачысціць каталог перакадзіравання", + "TaskCleanTranscodeDescription": "Выдаляе перакадзіраваныя файлы, старэйшыя за адзін дзень.", + "TaskRefreshChannels": "Абнавіць каналы", + "TaskDownloadMissingSubtitles": "Спампаваць адсутныя субтытры", + "TaskKeyframeExtractorDescription": "Выдае ключавыя кадры з відэафайлаў для стварэння больш дакладных спісаў прайгравання HLS. Гэта задача можа працаваць у працягу доўгага часу." } diff --git a/Emby.Server.Implementations/Localization/Core/bg-BG.json b/Emby.Server.Implementations/Localization/Core/bg-BG.json index 64cb36fd8..13b99cc99 100644 --- a/Emby.Server.Implementations/Localization/Core/bg-BG.json +++ b/Emby.Server.Implementations/Localization/Core/bg-BG.json @@ -123,5 +123,6 @@ "TaskOptimizeDatabase": "Оптимизирай базата данни", "TaskKeyframeExtractorDescription": "Извличат се ключови кадри от видеофайловете ,за да се създаде по точен ХЛС списък . Задачата може да отнеме много време.", "TaskKeyframeExtractor": "Извличане на ключови кадри", - "External": "Външен" + "External": "Външен", + "HearingImpaired": "Увреден слух" } diff --git a/Emby.Server.Implementations/Localization/Core/bn.json b/Emby.Server.Implementations/Localization/Core/bn.json index c3fbe2408..355fb3b21 100644 --- a/Emby.Server.Implementations/Localization/Core/bn.json +++ b/Emby.Server.Implementations/Localization/Core/bn.json @@ -1,27 +1,27 @@ { "DeviceOnlineWithName": "{0}-এর সাথে সংযুক্ত হয়েছে", "DeviceOfflineWithName": "{0}-এর সাথে সংযোগ বিচ্ছিন্ন হয়েছে", - "Collections": "সংগ্রহ", + "Collections": "সংগ্রহশালা", "ChapterNameValue": "অধ্যায় {0}", - "Channels": "চ্যানেল", + "Channels": "চ্যানেলসমূহ", "CameraImageUploadedFrom": "{0} থেকে একটি নতুন ক্যামেরার চিত্র আপলোড করা হয়েছে", - "Books": "বই", + "Books": "পুস্তকসমূহ", "AuthenticationSucceededWithUserName": "{0} অনুমোদন সফল", - "Artists": "শিল্পীরা", + "Artists": "শিল্পীগণ", "Application": "অ্যাপ্লিকেশন", - "Albums": "অ্যালবামগুলো", + "Albums": "অ্যালবামসমূহ", "HeaderFavoriteEpisodes": "প্রিব পর্বগুলো", "HeaderFavoriteArtists": "প্রিয় শিল্পীরা", "HeaderFavoriteAlbums": "প্রিয় এলবামগুলো", "HeaderContinueWatching": "দেখতে থাকুন", - "HeaderAlbumArtists": "এলবাম শিল্পীবৃন্দ", - "Genres": "শৈলী", - "Folders": "ফোল্ডারগুলো", + "HeaderAlbumArtists": "অ্যালবাম শিল্পীবৃন্দ", + "Genres": "শৈলীধারাসমূহ", + "Folders": "ফোল্ডারসমূহ", "Favorites": "পছন্দসমূহ", "FailedLoginAttemptWithUserName": "{0} লগিন করতে ব্যর্থ হয়েছে", "AppDeviceValues": "অ্যাপ: {0}, ডিভাইস: {0}", "VersionNumber": "সংস্করণ {0}", - "ValueSpecialEpisodeName": "বিশেষ - {0}", + "ValueSpecialEpisodeName": "বিশেষ পর্ব - {0}", "ValueHasBeenAddedToLibrary": "আপনার লাইব্রেরিতে {0} যোগ করা হয়েছে", "UserStoppedPlayingItemWithValues": "{2}তে {1} বাজানো শেষ করেছেন {0}", "UserStartedPlayingItemWithValues": "{2}তে {1} বাজাচ্ছেন {0}", @@ -36,10 +36,10 @@ "User": "ব্যবহারকারী", "TvShows": "টিভি শোগুলো", "System": "সিস্টেম", - "Sync": "সিংক", + "Sync": "সমলয় স্থাপন", "SubtitleDownloadFailureFromForItem": "{2} থেকে {1} এর জন্য সাবটাইটেল ডাউনলোড ব্যর্থ", "StartupEmbyServerIsLoading": "জেলিফিন সার্ভার লোড হচ্ছে। দয়া করে একটু পরে আবার চেষ্টা করুন।", - "Songs": "গানগুলো", + "Songs": "সঙ্গীতসমূহ", "Shows": "টিভি পর্ব", "ServerNameNeedsToBeRestarted": "{0} রিস্টার্ট করা প্রয়োজন", "ScheduledTaskStartedWithName": "{0} শুরু হয়েছে", @@ -49,8 +49,8 @@ "PluginUninstalledWithName": "{0} বাদ দেয়া হয়েছে", "PluginInstalledWithName": "{0} ইন্সটল করা হয়েছে", "Plugin": "প্লাগিন", - "Playlists": "প্লেলিস্ট", - "Photos": "ছবিগুলো", + "Playlists": "প্লে লিস্ট সমূহ", + "Photos": "চিত্রসমূহ", "NotificationOptionVideoPlaybackStopped": "ভিডিও চলা বন্ধ", "NotificationOptionVideoPlayback": "ভিডিও চলা শুরু হয়েছে", "NotificationOptionUserLockedOut": "ব্যবহারকারী ঢুকতে পারছে না", @@ -71,9 +71,9 @@ "NameSeasonUnknown": "সিজন অজানা", "NameSeasonNumber": "সিজন {0}", "NameInstallFailed": "{0} ইন্সটল ব্যর্থ", - "MusicVideos": "গানের ভিডিও", + "MusicVideos": "সঙ্গীত ভিডিয়ো সমূহ", "Music": "গান", - "Movies": "চলচ্চিত্র", + "Movies": "চলচ্চিত্রসমূহ", "MixedContent": "মিশ্র কন্টেন্ট", "MessageServerConfigurationUpdated": "সার্ভারের কনফিগারেশন আপডেট করা হয়েছে", "HeaderRecordingGroups": "রেকর্ডিং দল", @@ -117,5 +117,11 @@ "Forced": "জোরকরে", "TaskCleanActivityLogDescription": "নির্ধারিত সময়ের আগের কাজের হিসাব মুছে দিন খালি করুন.", "TaskCleanActivityLog": "কাজের ফাইল খালি করুন", - "Default": "প্রাথমিক" + "Default": "প্রাথমিক", + "HearingImpaired": "দুর্বল শ্রবণক্ষমতাধরদের জন্য", + "TaskOptimizeDatabaseDescription": "তথ্যভাণ্ডার সুবিন্যস্ত করে ও অব্যবহৃত জায়গা ছেড়ে দেয়। লাইব্রেরী স্ক্যান অথবা যেকোনো তথ্যভাণ্ডার পরিবর্তনের পর এই প্রক্রিয়া চালালে তথ্যভাণ্ডারের তথ্য প্রদান দ্রুততর হতে পারে।", + "External": "বাহ্যিক", + "TaskOptimizeDatabase": "তথ্যভাণ্ডার সুবিন্যাস", + "TaskKeyframeExtractor": "কি-ফ্রেম নিষ্কাশক", + "TaskKeyframeExtractorDescription": "ভিডিয়ো থেকে কি-ফ্রেম নিষ্কাশনের মাধ্যমে অধিকতর সঠিক HLS প্লে লিস্ট তৈরী করে। এই প্রক্রিয়া দীর্ঘ সময় ধরে চলতে পারে।" } diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json index 644d2676e..26290df4d 100644 --- a/Emby.Server.Implementations/Localization/Core/ca.json +++ b/Emby.Server.Implementations/Localization/Core/ca.json @@ -5,7 +5,7 @@ "Artists": "Artistes", "AuthenticationSucceededWithUserName": "{0} s'ha autenticat correctament", "Books": "Llibres", - "CameraImageUploadedFrom": "S'ha pujat una nova imatge des de la camera desde {0}", + "CameraImageUploadedFrom": "S'ha pujat una nova imatge de càmera des de {0}", "Channels": "Canals", "ChapterNameValue": "Capítol {0}", "Collections": "Col·leccions", @@ -16,92 +16,92 @@ "Folders": "Carpetes", "Genres": "Gèneres", "HeaderAlbumArtists": "Artistes de l'àlbum", - "HeaderContinueWatching": "Continua Veient", - "HeaderFavoriteAlbums": "Àlbums Preferits", - "HeaderFavoriteArtists": "Artistes Predilectes", - "HeaderFavoriteEpisodes": "Episodis Predilectes", - "HeaderFavoriteShows": "Sèries Predilectes", - "HeaderFavoriteSongs": "Cançons Predilectes", - "HeaderLiveTV": "TV en Directe", + "HeaderContinueWatching": "Continuar veient", + "HeaderFavoriteAlbums": "Àlbums preferits", + "HeaderFavoriteArtists": "Artistes preferits", + "HeaderFavoriteEpisodes": "Episodis preferits", + "HeaderFavoriteShows": "Sèries preferides", + "HeaderFavoriteSongs": "Cançons preferides", + "HeaderLiveTV": "TV en directe", "HeaderNextUp": "A continuació", - "HeaderRecordingGroups": "Grups d'Enregistrament", - "HomeVideos": "Vídeos Domèstics", + "HeaderRecordingGroups": "Grups d'enregistrament", + "HomeVideos": "Vídeos domèstics", "Inherit": "Hereta", - "ItemAddedWithName": "{0} ha estat afegit a la biblioteca", - "ItemRemovedWithName": "{0} ha estat eliminat de la biblioteca", + "ItemAddedWithName": "{0} ha sigut afegit a la biblioteca", + "ItemRemovedWithName": "{0} ha sigut eliminat de la biblioteca", "LabelIpAddressValue": "Adreça IP: {0}", "LabelRunningTimeValue": "Temps en funcionament: {0}", - "Latest": "Darreres", - "MessageApplicationUpdated": "El Servidor de Jellyfin ha estat actualitzat", - "MessageApplicationUpdatedTo": "El Servidor de Jellyfin ha estat actualitzat a {0}", + "Latest": "Darrers", + "MessageApplicationUpdated": "El servidor de Jellyfin ha estat actualitzat", + "MessageApplicationUpdatedTo": "El servidor de Jellyfin ha estat actualitzat a {0}", "MessageNamedServerConfigurationUpdatedWithValue": "La secció {0} de la configuració del servidor ha estat actualitzada", "MessageServerConfigurationUpdated": "S'ha actualitzat la configuració del servidor", "MixedContent": "Contingut barrejat", "Movies": "Pel·lícules", "Music": "Música", - "MusicVideos": "Vídeos Musicals", - "NameInstallFailed": "Instalació de {0} fallida", + "MusicVideos": "Videoclips", + "NameInstallFailed": "{0} instal·lació fallida", "NameSeasonNumber": "Temporada {0}", - "NameSeasonUnknown": "Temporada Desconeguda", - "NewVersionIsAvailable": "Una nova versió del Servidor Jellyfin està disponible per descarregar.", - "NotificationOptionApplicationUpdateAvailable": "Actualització d'aplicació disponible", - "NotificationOptionApplicationUpdateInstalled": "Actualització d'aplicació instal·lada", - "NotificationOptionAudioPlayback": "Reproducció d'audio iniciada", - "NotificationOptionAudioPlaybackStopped": "Reproducció d'audio aturada", + "NameSeasonUnknown": "Temporada desconeguda", + "NewVersionIsAvailable": "Una nova versió del servidor de Jellyfin està disponible per a descarregar.", + "NotificationOptionApplicationUpdateAvailable": "Actualització de l'aplicació disponible", + "NotificationOptionApplicationUpdateInstalled": "Actualització de l'aplicació instal·lada", + "NotificationOptionAudioPlayback": "Reproducció d'àudio iniciada", + "NotificationOptionAudioPlaybackStopped": "Reproducció d'àudio aturada", "NotificationOptionCameraImageUploaded": "Imatge de càmera pujada", - "NotificationOptionInstallationFailed": "Instalació fallida", + "NotificationOptionInstallationFailed": "Instal·lació fallida", "NotificationOptionNewLibraryContent": "Nou contingut afegit", - "NotificationOptionPluginError": "Un connector ha fallat", - "NotificationOptionPluginInstalled": "Connector instal·lat", - "NotificationOptionPluginUninstalled": "Connector desinstal·lat", - "NotificationOptionPluginUpdateInstalled": "Actualització de connector instal·lada", + "NotificationOptionPluginError": "Un complement ha fallat", + "NotificationOptionPluginInstalled": "Complement instal·lat", + "NotificationOptionPluginUninstalled": "Complement desinstal·lat", + "NotificationOptionPluginUpdateInstalled": "Actualització de complement instal·lada", "NotificationOptionServerRestartRequired": "Reinici del servidor requerit", "NotificationOptionTaskFailed": "Tasca programada fallida", - "NotificationOptionUserLockedOut": "Usuari tancat", - "NotificationOptionVideoPlayback": "Reproducció de video iniciada", - "NotificationOptionVideoPlaybackStopped": "Reproducció de video aturada", + "NotificationOptionUserLockedOut": "Usuari expulsat", + "NotificationOptionVideoPlayback": "Reproducció de vídeo iniciada", + "NotificationOptionVideoPlaybackStopped": "Reproducció de vídeo aturada", "Photos": "Fotos", "Playlists": "Llistes de reproducció", - "Plugin": "Connector", + "Plugin": "Complement", "PluginInstalledWithName": "{0} ha estat instal·lat", "PluginUninstalledWithName": "{0} ha estat desinstal·lat", "PluginUpdatedWithName": "{0} ha estat actualitzat", "ProviderValue": "Proveïdor: {0}", "ScheduledTaskFailedWithName": "{0} ha fallat", - "ScheduledTaskStartedWithName": "{0} iniciat", + "ScheduledTaskStartedWithName": "{0} s'ha iniciat", "ServerNameNeedsToBeRestarted": "{0} necessita ser reiniciat", "Shows": "Sèries", "Songs": "Cançons", - "StartupEmbyServerIsLoading": "El Servidor d'Jellyfin està carregant. Si et plau, prova de nou en breus.", + "StartupEmbyServerIsLoading": "El servidor de Jellyfin s'està carregant. Proveu-ho altre cop aviat.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", - "SubtitleDownloadFailureFromForItem": "Els subtítols no s'han pogut baixar de {0} per {1}", + "SubtitleDownloadFailureFromForItem": "Els subtítols per a {1} no s'han pogut baixar de {0}", "Sync": "Sincronitzar", "System": "Sistema", - "TvShows": "Espectacles de TV", - "User": "User", + "TvShows": "Sèries de TV", + "User": "Usuari", "UserCreatedWithName": "S'ha creat l'usuari {0}", "UserDeletedWithName": "L'usuari {0} ha estat eliminat", "UserDownloadingItemWithValues": "{0} està descarregant {1}", - "UserLockedOutWithName": "L'usuari {0} ha sigut tancat", + "UserLockedOutWithName": "L'usuari {0} ha sigut expulsat", "UserOfflineFromDevice": "{0} s'ha desconnectat de {1}", "UserOnlineFromDevice": "{0} està connectat des de {1}", "UserPasswordChangedWithName": "La contrasenya ha estat canviada per a l'usuari {0}", - "UserPolicyUpdatedWithName": "La política d'usuari s'ha actualitzat per {0}", + "UserPolicyUpdatedWithName": "La política d'usuari s'ha actualitzat per a {0}", "UserStartedPlayingItemWithValues": "{0} ha començat a reproduir {1}", "UserStoppedPlayingItemWithValues": "{0} ha parat de reproduir {1}", - "ValueHasBeenAddedToLibrary": "{0} ha sigut afegit a la teva llibreria", + "ValueHasBeenAddedToLibrary": "{0} ha sigut afegit a la teva biblioteca", "ValueSpecialEpisodeName": "Especial - {0}", "VersionNumber": "Versió {0}", "TaskDownloadMissingSubtitlesDescription": "Cerca a internet els subtítols que faltin a partir de la configuració de metadades.", "TaskDownloadMissingSubtitles": "Descarrega els subtítols que faltin", "TaskRefreshChannelsDescription": "Actualitza la informació dels canals d'internet.", - "TaskRefreshChannels": "Actualitza Canals", - "TaskCleanTranscodeDescription": "Elimina els arxius temporals de transcodificacions que tinguin més d'un dia.", + "TaskRefreshChannels": "Actualitza els canals", + "TaskCleanTranscodeDescription": "Elimina els arxius de transcodificacions que tinguin més d'un dia.", "TaskCleanTranscode": "Neteja les transcodificacions", - "TaskUpdatePluginsDescription": "Actualitza les extensions que estan configurades per actualitzar-se automàticament.", - "TaskUpdatePlugins": "Actualitza les extensions", + "TaskUpdatePluginsDescription": "Actualitza els connectors que estan configurats per a actualitzar-se automàticament.", + "TaskUpdatePlugins": "Actualitza els connectors", "TaskRefreshPeopleDescription": "Actualitza les metadades dels actors i directors de la teva mediateca.", - "TaskRefreshPeople": "Actualitza Persones", + "TaskRefreshPeople": "Actualitza les persones", "TaskCleanLogsDescription": "Esborra els logs que tinguin més de {0} dies.", "TaskCleanLogs": "Neteja els registres", "TaskRefreshLibraryDescription": "Escaneja la mediateca buscant fitxers nous i refresca les metadades.", @@ -115,13 +115,14 @@ "TasksLibraryCategory": "Biblioteca", "TasksMaintenanceCategory": "Manteniment", "TaskCleanActivityLogDescription": "Eliminat entrades del registre d'activitats mes antigues que l'antiguitat configurada.", - "TaskCleanActivityLog": "Buidar Registre d'Activitat", + "TaskCleanActivityLog": "Buidar el registre d'activitat", "Undefined": "Indefinit", "Forced": "Forçat", - "Default": "Defecto", + "Default": "Per defecte", "TaskOptimizeDatabaseDescription": "Compacta la base de dades i trunca l'espai lliure. Executar aquesta tasca després d’escanejar la biblioteca o fer altres canvis que impliquin modificacions a la base de dades pot millorar el rendiment.", "TaskOptimizeDatabase": "Optimitzar la base de dades", "TaskKeyframeExtractorDescription": "Extreu fotogrames clau dels fitxers de vídeo per crear llistes de reproducció HLS més precises. Aquesta tasca pot durar molt de temps.", "TaskKeyframeExtractor": "Extractor de fotogrames clau", - "External": "Extern" + "External": "Extern", + "HearingImpaired": "Discapacitat auditiva" } diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json index 943fc651f..08db5a30e 100644 --- a/Emby.Server.Implementations/Localization/Core/cs.json +++ b/Emby.Server.Implementations/Localization/Core/cs.json @@ -123,5 +123,6 @@ "TaskOptimizeDatabase": "Optimalizovat databázi", "TaskKeyframeExtractorDescription": "Vytahuje klíčové snímky ze souborů videa za účelem vytváření přesnějších seznamů přehrávání HLS. Tento úkol může trvat velmi dlouho.", "TaskKeyframeExtractor": "Vytahovač klíčových snímků", - "External": "Externí" + "External": "Externí", + "HearingImpaired": "Sluchově postižení" } diff --git a/Emby.Server.Implementations/Localization/Core/cy.json b/Emby.Server.Implementations/Localization/Core/cy.json index 331c3d678..794a8e4ce 100644 --- a/Emby.Server.Implementations/Localization/Core/cy.json +++ b/Emby.Server.Implementations/Localization/Core/cy.json @@ -28,7 +28,7 @@ "NameSeasonNumber": "Tymor {0}", "MusicVideos": "Fideos Cerddoriaeth", "MixedContent": "Cynnwys amrywiol", - "HomeVideos": "Fideos Cartref", + "HomeVideos": "Genres", "HeaderNextUp": "Nesaf i Fyny", "HeaderFavoriteArtists": "Ffefryn Artistiaid", "HeaderFavoriteAlbums": "Ffefryn Albwmau", @@ -122,5 +122,6 @@ "TaskRefreshChapterImagesDescription": "Creu mân-luniau ar gyfer fideos sydd â phenodau.", "TaskRefreshChapterImages": "Echdynnu Lluniau Pennod", "TaskCleanCacheDescription": "Dileu ffeiliau cache nad oes eu hangen ar y system mwyach.", - "TaskCleanCache": "Gwaghau Ffolder Cache" + "TaskCleanCache": "Gwaghau Ffolder Cache", + "HearingImpaired": "Nam ar y clyw" } diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json index 57455587d..1b6eecdcf 100644 --- a/Emby.Server.Implementations/Localization/Core/da.json +++ b/Emby.Server.Implementations/Localization/Core/da.json @@ -1,9 +1,9 @@ { - "Albums": "Albummer", + "Albums": "Album", "AppDeviceValues": "App: {0}, Enhed: {1}", "Application": "Applikation", "Artists": "Kunstnere", - "AuthenticationSucceededWithUserName": "{0} succesfuldt autentificeret", + "AuthenticationSucceededWithUserName": "{0} er logget ind", "Books": "Bøger", "CameraImageUploadedFrom": "Et nyt kamerabillede er blevet uploadet fra {0}", "Channels": "Kanaler", @@ -11,17 +11,17 @@ "Collections": "Samlinger", "DeviceOfflineWithName": "{0} har afbrudt forbindelsen", "DeviceOnlineWithName": "{0} er forbundet", - "FailedLoginAttemptWithUserName": "Fejlet loginforsøg fra {0}", + "FailedLoginAttemptWithUserName": "Mislykket loginforsøg fra {0}", "Favorites": "Favoritter", "Folders": "Mapper", "Genres": "Genrer", - "HeaderAlbumArtists": "Kunstnerens album", - "HeaderContinueWatching": "Fortsæt Afspilning", - "HeaderFavoriteAlbums": "Favoritalbummer", - "HeaderFavoriteArtists": "Favoritkunstnere", - "HeaderFavoriteEpisodes": "Favoritepisoder", - "HeaderFavoriteShows": "Favoritserier", - "HeaderFavoriteSongs": "Favoritsange", + "HeaderAlbumArtists": "Albums kunstnere", + "HeaderContinueWatching": "Fortsæt afspilning", + "HeaderFavoriteAlbums": "Favorit albummer", + "HeaderFavoriteArtists": "Favorit kunstnere", + "HeaderFavoriteEpisodes": "Favorit afsnit", + "HeaderFavoriteShows": "Favorit serier", + "HeaderFavoriteSongs": "Favorit sange", "HeaderLiveTV": "Live-TV", "HeaderNextUp": "Næste", "HeaderRecordingGroups": "Optagelsesgrupper", @@ -39,89 +39,90 @@ "MixedContent": "Blandet indhold", "Movies": "Film", "Music": "Musik", - "MusicVideos": "Musik videoer", + "MusicVideos": "Musikvideoer", "NameInstallFailed": "{0} installationen mislykkedes", "NameSeasonNumber": "Sæson {0}", - "NameSeasonUnknown": "Ukendt Sæson", - "NewVersionIsAvailable": "En ny version af Jellyfin Server er tilgængelig til download.", - "NotificationOptionApplicationUpdateAvailable": "Opdatering til applikation tilgængelig", - "NotificationOptionApplicationUpdateInstalled": "Opdatering til applikation installeret", + "NameSeasonUnknown": "Ukendt sæson", + "NewVersionIsAvailable": "En ny version af Jellyfin Server er tilgængelig.", + "NotificationOptionApplicationUpdateAvailable": "Opdatering til applikationen er tilgængelig", + "NotificationOptionApplicationUpdateInstalled": "Opdatering til applikationen blev installeret", "NotificationOptionAudioPlayback": "Lydafspilning påbegyndt", "NotificationOptionAudioPlaybackStopped": "Lydafspilning stoppet", "NotificationOptionCameraImageUploaded": "Kamerabillede uploadet", - "NotificationOptionInstallationFailed": "Installationen fejlede", + "NotificationOptionInstallationFailed": "Installationen mislykkedes", "NotificationOptionNewLibraryContent": "Nyt indhold tilføjet", - "NotificationOptionPluginError": "Pluginfejl", - "NotificationOptionPluginInstalled": "Plugin installeret", - "NotificationOptionPluginUninstalled": "Plugin afinstalleret", - "NotificationOptionPluginUpdateInstalled": "Opdatering til plugin installeret", - "NotificationOptionServerRestartRequired": "Genstart af server påkrævet", - "NotificationOptionTaskFailed": "Planlagt opgave fejlet", - "NotificationOptionUserLockedOut": "Bruger låst ude", + "NotificationOptionPluginError": "Plugin fejl", + "NotificationOptionPluginInstalled": "Plugin blev installeret", + "NotificationOptionPluginUninstalled": "Plugin blev afinstalleret", + "NotificationOptionPluginUpdateInstalled": "Opdatering til plugin blev installeret", + "NotificationOptionServerRestartRequired": "Genstart af serveren er påkrævet", + "NotificationOptionTaskFailed": "Planlagt opgave er fejlet", + "NotificationOptionUserLockedOut": "Bruger er låst ude", "NotificationOptionVideoPlayback": "Videoafspilning påbegyndt", - "NotificationOptionVideoPlaybackStopped": "Videoafspilning stoppet", - "Photos": "Fotoer", + "NotificationOptionVideoPlaybackStopped": "Videoafspilning blev stoppet", + "Photos": "Fotos", "Playlists": "Afspilningslister", "Plugin": "Plugin", "PluginInstalledWithName": "{0} blev installeret", "PluginUninstalledWithName": "{0} blev afinstalleret", "PluginUpdatedWithName": "{0} blev opdateret", "ProviderValue": "Udbyder: {0}", - "ScheduledTaskFailedWithName": "{0} fejlet", - "ScheduledTaskStartedWithName": "{0} påbegyndt", + "ScheduledTaskFailedWithName": "{0} mislykkedes", + "ScheduledTaskStartedWithName": "{0} påbegyndte", "ServerNameNeedsToBeRestarted": "{0} skal genstartes", "Shows": "Serier", "Songs": "Sange", - "StartupEmbyServerIsLoading": "Jellyfin Server er i gang med at starte op. Prøv venligst igen om lidt.", + "StartupEmbyServerIsLoading": "Jellyfin Server er i gang med at starte. Forsøg igen om et øjeblik.", "SubtitleDownloadFailureForItem": "Fejlet i download af undertekster for {0}", - "SubtitleDownloadFailureFromForItem": "Undertekster kunne ikke downloades fra {0} til {1}", - "Sync": "Synk", + "SubtitleDownloadFailureFromForItem": "Undertekster kunne ikke hentes fra {0} til {1}", + "Sync": "Synkroniser", "System": "System", - "TvShows": "TV serier", + "TvShows": "TV-serier", "User": "Bruger", "UserCreatedWithName": "Bruger {0} er blevet oprettet", - "UserDeletedWithName": "Brugeren {0} er blevet slettet", - "UserDownloadingItemWithValues": "{0} downloader {1}", + "UserDeletedWithName": "Brugeren {0} er nu slettet", + "UserDownloadingItemWithValues": "{0} henter {1}", "UserLockedOutWithName": "Brugeren {0} er blevet låst ude", "UserOfflineFromDevice": "{0} har afbrudt fra {1}", "UserOnlineFromDevice": "{0} er online fra {1}", - "UserPasswordChangedWithName": "Adgangskode er ændret for bruger {0}", - "UserPolicyUpdatedWithName": "Brugerpolitik er blevet opdateret for {0}", + "UserPasswordChangedWithName": "Adgangskode er ændret for brugeren {0}", + "UserPolicyUpdatedWithName": "Brugerpolitikken er blevet opdateret for {0}", "UserStartedPlayingItemWithValues": "{0} har påbegyndt afspilning af {1}", "UserStoppedPlayingItemWithValues": "{0} har afsluttet afspilning af {1} på {2}", "ValueHasBeenAddedToLibrary": "{0} er blevet tilføjet til dit mediebibliotek", "ValueSpecialEpisodeName": "Special - {0}", "VersionNumber": "Version {0}", - "TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata konfiguration.", - "TaskDownloadMissingSubtitles": "Download manglende undertekster", - "TaskUpdatePluginsDescription": "Downloader og installere opdateringer for plugins som er konfigureret til at opdatere automatisk.", + "TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata konfigurationen.", + "TaskDownloadMissingSubtitles": "Hent manglende undertekster", + "TaskUpdatePluginsDescription": "Henter og installerer opdateringer for plugins, som er indstillet til at blive opdateret automatisk.", "TaskUpdatePlugins": "Opdater Plugins", - "TaskCleanLogsDescription": "Sletter log filer som er mere end {0} dage gammle.", - "TaskCleanLogs": "Ryd Log Mappe", - "TaskRefreshLibraryDescription": "Scanner dit medie bibliotek for nye filer og opdaterer metadata.", + "TaskCleanLogsDescription": "Sletter log filer som er mere end {0} dage gamle.", + "TaskCleanLogs": "Ryd Log mappe", + "TaskRefreshLibraryDescription": "Scanner dit medie bibliotek for nye filer og opdateret metadata.", "TaskRefreshLibrary": "Scan Medie Bibliotek", - "TaskCleanCacheDescription": "Sletter cache filer som systemet ikke har brug for længere.", - "TaskCleanCache": "Ryd Cache Mappe", + "TaskCleanCacheDescription": "Sletter cache filer som systemet ikke længere bruger.", + "TaskCleanCache": "Ryd Cache mappe", "TasksChannelsCategory": "Internet Kanaler", "TasksApplicationCategory": "Applikation", "TasksLibraryCategory": "Bibliotek", "TasksMaintenanceCategory": "Vedligeholdelse", - "TaskRefreshChapterImages": "Udtræk Kapitel billeder", + "TaskRefreshChapterImages": "Udtræk kapitel billeder", "TaskRefreshChapterImagesDescription": "Lav miniaturebilleder for videoer der har kapitler.", - "TaskRefreshChannelsDescription": "Genopfrisker internet kanal information.", - "TaskRefreshChannels": "Genopfrisk Kanaler", - "TaskCleanTranscodeDescription": "Fjern transcode filer som er mere end en dag gammel.", - "TaskCleanTranscode": "Rengør Transcode Mappen", - "TaskRefreshPeople": "Genopfrisk Personer", - "TaskRefreshPeopleDescription": "Opdatere metadata for skuespillere og instruktører i dit bibliotek.", - "TaskCleanActivityLogDescription": "Sletter linjer i aktivitetsloggen ældre end den konfigureret alder.", + "TaskRefreshChannelsDescription": "Opdater internet kanal information.", + "TaskRefreshChannels": "Opdater Kanaler", + "TaskCleanTranscodeDescription": "Fjern transcode filer som er mere end 1 dag gammel.", + "TaskCleanTranscode": "Tøm Transcode mappen", + "TaskRefreshPeople": "Opdater Personer", + "TaskRefreshPeopleDescription": "Opdaterer metadata for skuespillere og instruktører i dit mediebibliotek.", + "TaskCleanActivityLogDescription": "Sletter linjer i aktivitetsloggen ældre end den konfigurerede alder.", "TaskCleanActivityLog": "Ryd Aktivitetslog", "Undefined": "Udefineret", "Forced": "Tvunget", "Default": "Standard", - "TaskOptimizeDatabaseDescription": "Kompakter database og forkorter fri plads. Ved at køre denne proces efter at scanne biblioteket eller efter at ændre noget som kunne have indflydelse på databasen, kan forbedre ydeevne.", + "TaskOptimizeDatabaseDescription": "Komprimerer databasen og frigør plads. Denne handling køres efter at have scannet mediebiblioteket, eller efter at have lavet ændringer til databasen, for at højne ydeevnen.", "TaskOptimizeDatabase": "Optimér database", - "TaskKeyframeExtractorDescription": "Udtrækker billeder fra videofiler for at lave mere præcise HLS playlister. Denne opgave kan godt tage lang tid.", - "TaskKeyframeExtractor": "Billedramme udtrækker", - "External": "Ekstern" + "TaskKeyframeExtractorDescription": "Udtrækker billeder fra videofiler for at lave mere præcise HLS playlister. Denne opgave kan tage lang tid.", + "TaskKeyframeExtractor": "Nøglebillede udtræk", + "External": "Ekstern", + "HearingImpaired": "Hørehæmmet" } diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json index 9c278db4d..e1c3e9de1 100644 --- a/Emby.Server.Implementations/Localization/Core/de.json +++ b/Emby.Server.Implementations/Localization/Core/de.json @@ -123,5 +123,6 @@ "TaskOptimizeDatabase": "Datenbank optimieren", "TaskKeyframeExtractorDescription": "Extrahiere Keyframes aus Videodateien, um präzisere HLS-Playlisten zu erzeugen. Dieser Vorgang kann sehr lange dauern.", "TaskKeyframeExtractor": "Keyframe Extraktor", - "External": "Extern" + "External": "Extern", + "HearingImpaired": "Hörgeschädigt" } diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json index 9e216a166..c6e2244ca 100644 --- a/Emby.Server.Implementations/Localization/Core/el.json +++ b/Emby.Server.Implementations/Localization/Core/el.json @@ -15,7 +15,7 @@ "Favorites": "Αγαπημένα", "Folders": "Φάκελοι", "Genres": "Είδη", - "HeaderAlbumArtists": "Δισκογραφικοί καλλιτέχνες", + "HeaderAlbumArtists": "Καλλιτέχνες άλμπουμ", "HeaderContinueWatching": "Συνεχίστε την παρακολούθηση", "HeaderFavoriteAlbums": "Αγαπημένα Άλμπουμ", "HeaderFavoriteArtists": "Αγαπημένοι Καλλιτέχνες", @@ -24,8 +24,8 @@ "HeaderFavoriteSongs": "Αγαπημένα Τραγούδια", "HeaderLiveTV": "Ζωντανή Τηλεόραση", "HeaderNextUp": "Επόμενο", - "HeaderRecordingGroups": "Μουσικά Συγκροτήματα", - "HomeVideos": "Προσωπικά βίντεο", + "HeaderRecordingGroups": "Ομάδες Ηχογράφησης", + "HomeVideos": "Προσωπικά Βίντεο", "Inherit": "Κληρονόμηση", "ItemAddedWithName": "{0} προστέθηκε στη βιβλιοθήκη", "ItemRemovedWithName": "{0} διαγράφηκε από τη βιβλιοθήκη", @@ -51,10 +51,10 @@ "NotificationOptionCameraImageUploaded": "Μεταφορτώθηκε φωτογραφία απο κάμερα", "NotificationOptionInstallationFailed": "Αποτυχία εγκατάστασης", "NotificationOptionNewLibraryContent": "Προστέθηκε νέο περιεχόμενο", - "NotificationOptionPluginError": "Αποτυχία του plugin", - "NotificationOptionPluginInstalled": "Το plugin εγκαταστάθηκε", - "NotificationOptionPluginUninstalled": "Το plugin απεγκαταστάθηκε", - "NotificationOptionPluginUpdateInstalled": "Η αναβάθμιση του plugin εγκαταστάθηκε", + "NotificationOptionPluginError": "Αποτυχία του πρόσθετου", + "NotificationOptionPluginInstalled": "Το πρόσθετο εγκαταστάθηκε", + "NotificationOptionPluginUninstalled": "Το πρόσθετο απεγκαταστάθηκε", + "NotificationOptionPluginUpdateInstalled": "Η αναβάθμιση του πρόσθετου εγκαταστάθηκε", "NotificationOptionServerRestartRequired": "Ο διακομιστής χρειάζεται επανεκκίνηση", "NotificationOptionTaskFailed": "Αποτυχία προγραμματισμένης εργασίας", "NotificationOptionUserLockedOut": "Ο χρήστης αποκλείστηκε", @@ -66,7 +66,7 @@ "PluginInstalledWithName": "{0} εγκαταστήθηκε", "PluginUninstalledWithName": "{0} έχει απεγκατασταθεί", "PluginUpdatedWithName": "{0} έχει αναβαθμιστεί", - "ProviderValue": "Provider: {0}", + "ProviderValue": "Πάροχος: {0}", "ScheduledTaskFailedWithName": "{0} αποτυχία", "ScheduledTaskStartedWithName": "{0} ξεκίνησε", "ServerNameNeedsToBeRestarted": "{0} χρειάζεται επανεκκίνηση", @@ -79,7 +79,7 @@ "System": "Σύστημα", "TvShows": "Τηλεοπτικές Σειρές", "User": "Χρήστης", - "UserCreatedWithName": "Δημιουργήθηκε ο χρήστης {0}", + "UserCreatedWithName": "Ο χρήστης {0} δημιουργήθηκε", "UserDeletedWithName": "Ο χρήστης {0} έχει διαγραφεί", "UserDownloadingItemWithValues": "{0} κατεβάζει {1}", "UserLockedOutWithName": "Ο χρήστης {0} αποκλείστηκε", @@ -93,29 +93,29 @@ "ValueSpecialEpisodeName": "Σπέσιαλ - {0}", "VersionNumber": "Έκδοση {0}", "TaskRefreshPeople": "Ανανέωση Ατόμων", - "TaskCleanLogsDescription": "Διαγράφει τα αρχεία καταγραφής που είναι άνω των {0} ημερών.", - "TaskCleanLogs": "Καθαρισμός Καταλόγου Καταγραφής", - "TaskRefreshLibraryDescription": "Σαρώνει την βιβλιοθήκη πολυμέσων σας για νέα αρχεία και αναζωογονεί τα μεταδεδομένα.", + "TaskCleanLogsDescription": "Διαγράφει αρχεία καταγραφής που είναι πάνω από {0} ημέρες.", + "TaskCleanLogs": "Εκκαθάριση Καταλόγου Καταγραφής", + "TaskRefreshLibraryDescription": "Σαρώνει την βιβλιοθήκη πολυμέσων σας για νέα αρχεία και ανανεώνει τα μεταδεδομένα.", "TaskRefreshLibrary": "Βιβλιοθήκη Σάρωσης Πολυμέσων", - "TaskRefreshChapterImagesDescription": "Δημιουργεί μικρογραφίες για βίντεο με κεφάλαια.", + "TaskRefreshChapterImagesDescription": "Δημιουργεί μικρογραφίες για βίντεο που έχουν κεφάλαια.", "TaskRefreshChapterImages": "Εξαγωγή Εικόνων Κεφαλαίου", - "TaskCleanCacheDescription": "Τα διαγραμμένα αρχεία προσωρινής μνήμης που δεν χρειάζονται πλέον από το σύστημα.", + "TaskCleanCacheDescription": "Διαγράφει αρχεία προσωρινής μνήμης που δεν χρειάζονται πλέον το σύστημα.", "TaskCleanCache": "Καθαρισμός Καταλόγου Προσωρινής Μνήμης", "TasksChannelsCategory": "Κανάλια Διαδικτύου", "TasksApplicationCategory": "Εφαρμογή", "TasksLibraryCategory": "Βιβλιοθήκη", "TasksMaintenanceCategory": "Συντήρηση", - "TaskDownloadMissingSubtitlesDescription": "Αναζητήσεις στο διαδίκτυο όπου λείπουν υπότιτλους με βάση τη διαμόρφωση μεταδεδομένων.", + "TaskDownloadMissingSubtitlesDescription": "Ψάχνει στο διαδίκτυο για υπότιτλους που λείπουν με βάση τη διαμόρφωση μεταδεδομένων.", "TaskDownloadMissingSubtitles": "Λήψη υπότιτλων που λείπουν", "TaskRefreshChannelsDescription": "Ανανεώνει τις πληροφορίες καναλιού στο διαδικτύου.", "TaskRefreshChannels": "Ανανέωση Καναλιών", - "TaskCleanTranscodeDescription": "Διαγράφει αρχείου διακωδικοποιητή περισσότερο από μία ημέρα.", - "TaskCleanTranscode": "Καθαρισμός Kαταλόγου Διακωδικοποιητή", - "TaskUpdatePluginsDescription": "Κατεβάζει και εγκαθιστά ενημερώσεις για τις προσθήκες που έχουν ρυθμιστεί για αυτόματη ενημέρωση.", - "TaskUpdatePlugins": "Ενημέρωση Προσθηκών", - "TaskRefreshPeopleDescription": "Ενημερώνει μεταδεδομένα για ηθοποιούς και σκηνοθέτες στην βιβλιοθήκη των πολυμέσων σας.", + "TaskCleanTranscodeDescription": "Διαγράφει αρχεία διακωδικοποίησης άνω της μίας ημέρας.", + "TaskCleanTranscode": "Εκκαθάριση Kαταλόγου Διακωδικοποίησης", + "TaskUpdatePluginsDescription": "Κατεβάζει και εγκαθιστά ενημερώσεις για τα πρόσθετα που έχουν ρυθμιστεί για αυτόματη ενημέρωση.", + "TaskUpdatePlugins": "Ενημέρωση Πρόσθετων", + "TaskRefreshPeopleDescription": "Ενημερώνει τα μεταδεδομένα για ηθοποιούς και σκηνοθέτες στη βιβλιοθήκη πολυμέσων σας.", "TaskCleanActivityLogDescription": "Διαγράφει καταχωρήσεις απο το αρχείο καταγραφής παλαιότερες από την επιλεγμένη ηλικία.", - "TaskCleanActivityLog": "Καθαρό Αρχείο Καταγραφής Δραστηριοτήτων", + "TaskCleanActivityLog": "Εκκαθάριση Αρχείου Καταγραφής Δραστηριοτήτων", "Undefined": "Απροσδιόριστο", "Forced": "Εξαναγκασμένο", "Default": "Προεπιλογή", @@ -123,5 +123,6 @@ "TaskOptimizeDatabase": "Βελτιστοποίηση βάσης δεδομένων", "TaskKeyframeExtractorDescription": "Εξάγει καρέ από αρχεία βίντεο για να δημιουργήσει πιο ακριβείς λίστες αναπαραγωγής HLS. Αυτή η διεργασία μπορεί να πάρει χρόνο.", "TaskKeyframeExtractor": "Εξαγωγέας βασικών καρέ βίντεο", - "External": "Εξωτερικό" + "External": "Εξωτερικό", + "HearingImpaired": "Με προβλήματα ακοής" } diff --git a/Emby.Server.Implementations/Localization/Core/en-GB.json b/Emby.Server.Implementations/Localization/Core/en-GB.json index 862410c54..243688388 100644 --- a/Emby.Server.Implementations/Localization/Core/en-GB.json +++ b/Emby.Server.Implementations/Localization/Core/en-GB.json @@ -123,5 +123,6 @@ "TaskOptimizeDatabase": "Optimise database", "TaskKeyframeExtractorDescription": "Extracts keyframes from video files to create more precise HLS playlists. This task may run for a long time.", "TaskKeyframeExtractor": "Keyframe Extractor", - "External": "External" + "External": "External", + "HearingImpaired": "Hearing Impaired" } diff --git a/Emby.Server.Implementations/Localization/Core/es-AR.json b/Emby.Server.Implementations/Localization/Core/es-AR.json index 1289172ba..8bd3c5def 100644 --- a/Emby.Server.Implementations/Localization/Core/es-AR.json +++ b/Emby.Server.Implementations/Localization/Core/es-AR.json @@ -118,10 +118,11 @@ "TaskCleanActivityLog": "Borrar log de actividades", "Undefined": "Indefinido", "Forced": "Forzado", - "Default": "Por Defecto", + "Default": "Predeterminado", "TaskOptimizeDatabaseDescription": "Compacta la base de datos y restaura el espacio libre. Ejecutar esta tarea después de actualizar las librerías o realizar otros cambios que impliquen modificar las bases de datos puede mejorar la performance.", "TaskOptimizeDatabase": "Optimización de base de datos", "External": "Externo", "TaskKeyframeExtractorDescription": "Extrae Fotogramas Clave de los archivos de vídeo para crear Listas de Reprodución HLS más precisas. Esta tarea puede durar mucho tiempo.", - "TaskKeyframeExtractor": "Extractor de Fotogramas Clave" + "TaskKeyframeExtractor": "Extractor de Fotogramas Clave", + "HearingImpaired": "Discapacidad Auditiva" } diff --git a/Emby.Server.Implementations/Localization/Core/es-MX.json b/Emby.Server.Implementations/Localization/Core/es-MX.json index a7391cc88..d677cc46c 100644 --- a/Emby.Server.Implementations/Localization/Core/es-MX.json +++ b/Emby.Server.Implementations/Localization/Core/es-MX.json @@ -123,5 +123,6 @@ "TaskOptimizeDatabaseDescription": "Compacta la base de datos y trunca el espacio libre. Puede mejorar el rendimiento si se realiza esta tarea después de escanear la biblioteca o después de realizar otros cambios que impliquen modificar la base de datos.", "TaskKeyframeExtractorDescription": "Extrae los cuadros clave de los archivos de vídeo para crear listas HLS más precisas. Esta tarea puede tardar un buen rato.", "TaskKeyframeExtractor": "Extractor de Cuadros Clave", - "External": "Externo" + "External": "Externo", + "HearingImpaired": "Discapacidad Auditiva" } diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json index db65a0c6d..f5636a0af 100644 --- a/Emby.Server.Implementations/Localization/Core/es.json +++ b/Emby.Server.Implementations/Localization/Core/es.json @@ -31,7 +31,7 @@ "ItemRemovedWithName": "{0} ha sido eliminado de la biblioteca", "LabelIpAddressValue": "Dirección IP: {0}", "LabelRunningTimeValue": "Tiempo de funcionamiento: {0}", - "Latest": "Últimos", + "Latest": "Últimas", "MessageApplicationUpdated": "Se ha actualizado el servidor Jellyfin", "MessageApplicationUpdatedTo": "Se ha actualizado el servidor Jellyfin a la versión {0}", "MessageNamedServerConfigurationUpdatedWithValue": "La sección {0} de configuración del servidor ha sido actualizada", @@ -123,5 +123,6 @@ "TaskOptimizeDatabaseDescription": "Optimiza y libera el espacio libre en la base de datos. Ejecutar esta tarea tras escanear la biblioteca o hacer cambios que impliquen modificaciones en la base de datos puede mejorar el rendimiento.", "TaskKeyframeExtractorDescription": "Extrae los fotogramas clave de los archivos de vídeo para crear listas HLS más precisas. Esta tarea puede tardar mucho tiempo.", "TaskKeyframeExtractor": "Extractor de Fotogramas Clave", - "External": "Externo" + "External": "Externo", + "HearingImpaired": "Discapacidad Auditiva" } diff --git a/Emby.Server.Implementations/Localization/Core/es_419.json b/Emby.Server.Implementations/Localization/Core/es_419.json index d6078c9c6..3d5c04633 100644 --- a/Emby.Server.Implementations/Localization/Core/es_419.json +++ b/Emby.Server.Implementations/Localization/Core/es_419.json @@ -122,5 +122,6 @@ "TaskOptimizeDatabase": "Optimizar base de datos", "External": "Externo", "TaskKeyframeExtractorDescription": "Extrae Fotogramas Clave de los archivos de vídeo para crear Listas de Reproducción HLS más precisas. Esta tarea puede durar mucho tiempo.", - "TaskKeyframeExtractor": "Extractor de Fotogramas Clave" + "TaskKeyframeExtractor": "Extractor de Fotogramas Clave", + "HearingImpaired": "Discapacidad auditiva" } diff --git a/Emby.Server.Implementations/Localization/Core/et.json b/Emby.Server.Implementations/Localization/Core/et.json index da44e53d0..081462407 100644 --- a/Emby.Server.Implementations/Localization/Core/et.json +++ b/Emby.Server.Implementations/Localization/Core/et.json @@ -120,5 +120,8 @@ "UserPolicyUpdatedWithName": "Kasutaja {0} õigusi värskendati", "UserStoppedPlayingItemWithValues": "{0} lõpetas {1} taasesituse seadmes {2}", "UserOnlineFromDevice": "{0} on ühendatud seadmest {1}", - "External": "Väline" + "External": "Väline", + "HearingImpaired": "Kuulmispuudega", + "TaskKeyframeExtractorDescription": "Eraldab videofailidest võtmekaadreid, et luua täpsemaid HLS-i esitusloendeid. See ülesanne võib kesta pikka aega.", + "TaskKeyframeExtractor": "Võtmekaadri ekstraktor" } diff --git a/Emby.Server.Implementations/Localization/Core/eu.json b/Emby.Server.Implementations/Localization/Core/eu.json index dfedce7b3..e91084f92 100644 --- a/Emby.Server.Implementations/Localization/Core/eu.json +++ b/Emby.Server.Implementations/Localization/Core/eu.json @@ -100,7 +100,7 @@ "ItemRemovedWithName": "{0} liburutegitik ezabatu da", "ItemAddedWithName": "{0} liburutegira gehitu da", "HomeVideos": "Etxeko bideoak", - "HeaderNextUp": "Hurrengoa", + "HeaderNextUp": "Nobedadeak", "HeaderLiveTV": "Zuzeneko TB", "HeaderFavoriteSongs": "Gogoko abestiak", "HeaderFavoriteShows": "Gogoko showak", @@ -116,5 +116,12 @@ "CameraImageUploadedFrom": "{0}-tik kamera irudi berri bat igo da", "AuthenticationSucceededWithUserName": "{0} ongi autentifikatu da", "Application": "Aplikazioa", - "AppDeviceValues": "App: {0}, Gailua: {1}" + "AppDeviceValues": "App: {0}, Gailua: {1}", + "HearingImpaired": "Entzunaldia aldatua", + "ProviderValue": "Hornitzailea: {0}", + "TaskKeyframeExtractorDescription": "Bideo fitxategietako fotograma gakoak ateratzen ditu HLS erreprodukzio-zerrenda zehatzagoak sortzeko. Zeregin honek denbora asko iraun dezake.", + "HeaderRecordingGroups": "Grabaketa taldeak", + "Inherit": "Oinordetu", + "TaskOptimizeDatabaseDescription": "Datu-basea trinkotu eta bertatik espazioa askatzen du. Liburutegia eskaneatu ondoren edo datu-basean aldaketak egin ondoren ataza hau exekutatzeak errendimendua hobetu lezake.", + "TaskKeyframeExtractor": "Fotograma gakoen erauzgailua" } diff --git a/Emby.Server.Implementations/Localization/Core/fa.json b/Emby.Server.Implementations/Localization/Core/fa.json index 026648af4..8e4bba25b 100644 --- a/Emby.Server.Implementations/Localization/Core/fa.json +++ b/Emby.Server.Implementations/Localization/Core/fa.json @@ -123,5 +123,6 @@ "TaskOptimizeDatabaseDescription": "فشرده سازی پایگاه داده و باز کردن فضای آزاد.اجرای این گزینه بعد از اسکن کردن کتابخانه یا تغییرات دیگر که روی پایگاه داده تأثیر میگذارند میتواند کارایی را بهبود ببخشد.", "TaskKeyframeExtractorDescription": "فریم های کلیدی را از فایل های ویدئویی استخراج می کند تا لیست های پخش HLS دقیق تری ایجاد کند. این کار ممکن است برای مدت طولانی اجرا شود.", "TaskKeyframeExtractor": "استخراج کننده فریم کلیدی", - "External": "خارجی" + "External": "خارجی", + "HearingImpaired": "مشکل شنوایی" } diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json index f0cafd1c0..8672cfb9f 100644 --- a/Emby.Server.Implementations/Localization/Core/fi.json +++ b/Emby.Server.Implementations/Localization/Core/fi.json @@ -118,9 +118,10 @@ "TaskCleanActivityLogDescription": "Poistaa määritettyä ikää vanhemmat tapahtumat toimintahistoriasta.", "TaskCleanActivityLog": "Tyhjennä toimintahistoria", "Undefined": "Määrittelemätön", - "TaskOptimizeDatabaseDescription": "Tiivistää ja puhdistaa tietokannan. Tämän toiminnon suorittaminen kirjastojen skannauksen tai muiden tietokantaan liittyvien muutoksien jälkeen voi parantaa suorituskykyä.", + "TaskOptimizeDatabaseDescription": "Tiivistää ja puhdistaa tietokannan. Tämän toiminnon suorittaminen kirjastopäivityksen tai muiden mahdollisten tietokantamuutosten jälkeen voi parantaa suorituskykyä.", "TaskOptimizeDatabase": "Optimoi tietokanta", "TaskKeyframeExtractorDescription": "Purkaa videotiedostojen avainkuvat tarkempien HLS-toistolistojen luomiseksi. Tehtävä saattaa kestää huomattavan pitkään.", "TaskKeyframeExtractor": "Avainkuvien purkain", - "External": "Ulkoinen" + "External": "Ulkoinen", + "HearingImpaired": "Kuulorajoitteinen" } diff --git a/Emby.Server.Implementations/Localization/Core/fil.json b/Emby.Server.Implementations/Localization/Core/fil.json index 99839ae6e..01b3e95fc 100644 --- a/Emby.Server.Implementations/Localization/Core/fil.json +++ b/Emby.Server.Implementations/Localization/Core/fil.json @@ -119,5 +119,9 @@ "Undefined": "Hindi tiyak", "Forced": "Sapilitan", "TaskOptimizeDatabaseDescription": "Iko-compact ang database at ita-truncate ang free space. Ang pagpapatakbo ng gawaing ito pagkatapos ng pag-scan sa library o paggawa ng iba pang mga pagbabago na nagpapahiwatig ng mga pagbabago sa database ay maaaring magpa-improve ng performance.", - "TaskOptimizeDatabase": "I-optimize ang database" + "TaskOptimizeDatabase": "I-optimize ang database", + "HearingImpaired": "Bingi", + "TaskKeyframeExtractor": "Tagabunot ng Keyframe", + "TaskKeyframeExtractorDescription": "Nagbubunot ng keyframe mula sa mga bidyo upang makabuo ng mas tumpak na HLS playlist. Maaaring matagal ito gawin.", + "External": "External" } diff --git a/Emby.Server.Implementations/Localization/Core/fr-CA.json b/Emby.Server.Implementations/Localization/Core/fr-CA.json index 24ca8f861..3ee045d89 100644 --- a/Emby.Server.Implementations/Localization/Core/fr-CA.json +++ b/Emby.Server.Implementations/Localization/Core/fr-CA.json @@ -5,7 +5,7 @@ "Artists": "Artistes", "AuthenticationSucceededWithUserName": "{0} authentifié avec succès", "Books": "Livres", - "CameraImageUploadedFrom": "Une nouvelle image de caméra a été téléchargée depuis {0}", + "CameraImageUploadedFrom": "Une nouvelle photo a été téléversée depuis {0}", "Channels": "Chaînes", "ChapterNameValue": "Chapitre {0}", "Collections": "Collections", @@ -123,5 +123,6 @@ "TaskOptimizeDatabase": "Optimiser la base de données", "TaskKeyframeExtractorDescription": "Extrait les images clés des fichiers vidéo pour créer des listes de lecture HLS plus précises. Cette tâche peut durer très longtemps.", "TaskKeyframeExtractor": "Extracteur d'image clé", - "External": "Externe" + "External": "Externe", + "HearingImpaired": "Malentendants" } diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json index 648c878e9..4877bcd7a 100644 --- a/Emby.Server.Implementations/Localization/Core/fr.json +++ b/Emby.Server.Implementations/Localization/Core/fr.json @@ -15,7 +15,7 @@ "Favorites": "Favoris", "Folders": "Dossiers", "Genres": "Genres", - "HeaderAlbumArtists": "Artistes d'album", + "HeaderAlbumArtists": "Artistes de l'album", "HeaderContinueWatching": "Reprendre le visionnage", "HeaderFavoriteAlbums": "Albums favoris", "HeaderFavoriteArtists": "Artistes préférés", @@ -123,5 +123,6 @@ "TaskOptimizeDatabase": "Optimiser la base de données", "TaskKeyframeExtractorDescription": "Extrait les images clés des fichiers vidéo pour créer des listes de lecture HLS plus précises. Cette tâche peut durer très longtemps.", "TaskKeyframeExtractor": "Extracteur d'image clé", - "External": "Externe" + "External": "Externe", + "HearingImpaired": "Malentendants" } diff --git a/Emby.Server.Implementations/Localization/Core/gl.json b/Emby.Server.Implementations/Localization/Core/gl.json index b433c6f68..76a98aa54 100644 --- a/Emby.Server.Implementations/Localization/Core/gl.json +++ b/Emby.Server.Implementations/Localization/Core/gl.json @@ -47,7 +47,7 @@ "HeaderFavoriteEpisodes": "Episodios Favoritos", "HeaderFavoriteArtists": "Artistas Favoritos", "HeaderFavoriteAlbums": "Álbunes Favoritos", - "HeaderContinueWatching": "Seguir mirando", + "HeaderContinueWatching": "Seguir vendo", "HeaderAlbumArtists": "Artistas do Album", "Genres": "Xéneros", "Forced": "Forzado", @@ -119,5 +119,9 @@ "UserOnlineFromDevice": "{0} está en liña desde {1}", "UserOfflineFromDevice": "{0} desconectouse desde {1}", "TaskOptimizeDatabaseDescription": "Compacta e libera o espazo libre da base de datos. Executar esta tarefa logo de realizar mudanzas que impliquen modificacións da base de datos ou despois de escanear a biblioteca pode traer mellorías de desempeño.", - "TaskOptimizeDatabase": "Optimizar base de datos" + "TaskOptimizeDatabase": "Optimizar base de datos", + "TaskKeyframeExtractorDescription": "Extrae fragmentos do vídeo para crear listas de reprodución HLS máis precisas. Podería levarlle bastante tempo.", + "External": "Externo", + "HearingImpaired": "Problemas de audición", + "TaskKeyframeExtractor": "Extractor de fragmentos" } diff --git a/Emby.Server.Implementations/Localization/Core/gsw.json b/Emby.Server.Implementations/Localization/Core/gsw.json index bd8cec710..ac9da1dd1 100644 --- a/Emby.Server.Implementations/Localization/Core/gsw.json +++ b/Emby.Server.Implementations/Localization/Core/gsw.json @@ -1,7 +1,7 @@ { "Albums": "Alben", "AppDeviceValues": "App: {0}, Gerät: {1}", - "Application": "Anwendung", + "Application": "Applikation", "Artists": "Künstler", "AuthenticationSucceededWithUserName": "{0} hat sich angemeldet", "Books": "Bücher", @@ -14,7 +14,7 @@ "FailedLoginAttemptWithUserName": "Fehlgeschlagener Anmeldeversuch von {0}", "Favorites": "Favoriten", "Folders": "Ordner", - "Genres": "Genres", + "Genres": "Genre", "HeaderAlbumArtists": "Album-Künstler", "HeaderContinueWatching": "weiter schauen", "HeaderFavoriteAlbums": "Lieblingsalben", @@ -49,7 +49,7 @@ "NotificationOptionAudioPlayback": "Audiowedergab gstartet", "NotificationOptionAudioPlaybackStopped": "Audiwedergab gstoppt", "NotificationOptionCameraImageUploaded": "Foti ueglade", - "NotificationOptionInstallationFailed": "Installationsfehler", + "NotificationOptionInstallationFailed": "Installationsfähler", "NotificationOptionNewLibraryContent": "Nöie Inhaut hinzuegfüegt", "NotificationOptionPluginError": "Plugin-Fäuer", "NotificationOptionPluginInstalled": "Plugin installiert", @@ -120,5 +120,9 @@ "Forced": "Erzwungen", "Default": "Standard", "TaskOptimizeDatabase": "Datenbank optimieren", - "External": "Extern" + "External": "Extern", + "TaskOptimizeDatabaseDescription": "Kompromiert d Datenbank und trennt freie Speicherplatz. Durch die Ufagb cha d Leistig nach em ne Scan vor Bibliothek oder andere Ufgabe verbesseret werde.", + "HearingImpaired": "Hörgschädigti", + "TaskKeyframeExtractor": "Keyframe-Extraktor", + "TaskKeyframeExtractorDescription": "Extrahiert Keyframes us Videodateien zum erstelle vo genauere HLS Playliste. Die Ufgab cha für e langi Zyt laufe." } diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json index c635dab23..694a3d688 100644 --- a/Emby.Server.Implementations/Localization/Core/he.json +++ b/Emby.Server.Implementations/Localization/Core/he.json @@ -123,5 +123,6 @@ "TaskOptimizeDatabaseDescription": "דוחס את מסד הנתונים ומוריד את שטח האחסון שבשימוש. הרצה של פעולה זו לאחר סריקת הספרייה או שינויים אחרים שמשפיעים על מסד הנתונים יכולה לשפר ביצועים.", "TaskKeyframeExtractorDescription": "חלץ תמונות מפתח מקבצי וידאו בכדי ליצור רשימות השמעה מדויקות יותר של HLS. משימה זו עלולה להימשך זמן רב.", "TaskKeyframeExtractor": "מחלץ תמונות מפתח", - "External": "חיצוני" + "External": "חיצוני", + "HearingImpaired": "לקוי שמיעה" } diff --git a/Emby.Server.Implementations/Localization/Core/hi.json b/Emby.Server.Implementations/Localization/Core/hi.json index 182b43ffc..4bbb1dd35 100644 --- a/Emby.Server.Implementations/Localization/Core/hi.json +++ b/Emby.Server.Implementations/Localization/Core/hi.json @@ -67,5 +67,42 @@ "Plugin": "प्लग-इन", "Playlists": "प्लेलिस्ट", "Photos": "तस्वीरें", - "External": "बाहरी" + "External": "बाहरी", + "PluginUpdatedWithName": "{0} अपडेट हुए", + "ScheduledTaskStartedWithName": "{0} शुरू हुए", + "Songs": "गाने", + "UserStartedPlayingItemWithValues": "{0} {2} पर {1} खेल रहे हैं", + "UserStoppedPlayingItemWithValues": "{0} ने {2} पर {1} खेलना खत्म किया", + "StartupEmbyServerIsLoading": "जेलीफ़िन सर्वर लोड हो रहा है। कृपया शीघ्र ही पुन: प्रयास करें।", + "ServerNameNeedsToBeRestarted": "{0} रीस्टार्ट करने की आवश्यकता है", + "UserCreatedWithName": "उपयोगकर्ता {0} बनाया गया", + "UserDownloadingItemWithValues": "{0} डाउनलोड हो रहा है", + "UserOfflineFromDevice": "{0} {1} से डिस्कनेक्ट हो गया है", + "Undefined": "अनिर्धारित", + "UserOnlineFromDevice": "{0} {1} से ऑनलाइन है", + "Shows": "शो", + "UserPasswordChangedWithName": "उपयोगकर्ता {0} के लिए पासवर्ड बदल दिया गया है", + "UserDeletedWithName": "उपयोगकर्ता {0} हटा दिया गया", + "UserPolicyUpdatedWithName": "{0} के लिए उपयोगकर्ता नीति अपडेट कर दी गई है", + "User": "उपयोगकर्ता", + "SubtitleDownloadFailureFromForItem": "{1} के लिए {0} से उपशीर्षक डाउनलोड करने में विफल", + "ProviderValue": "प्रदाता: {0}", + "ScheduledTaskFailedWithName": "{0}असफल", + "UserLockedOutWithName": "उपयोगकर्ता {0} को लॉक आउट कर दिया गया है", + "System": "प्रणाली", + "TvShows": "टीवी शो", + "HearingImpaired": "मूक बधिर", + "ValueSpecialEpisodeName": "विशेष - {0}", + "TasksMaintenanceCategory": "रखरखाव", + "Sync": "समाकलयति", + "VersionNumber": "{0} पाठान्तर", + "ValueHasBeenAddedToLibrary": "{0} आपके माध्यम ग्रन्थालय में उपजात हो गया हैं", + "TasksLibraryCategory": "संग्रहालय", + "TaskOptimizeDatabase": "जानकारी प्रवृद्धि", + "TaskDownloadMissingSubtitles": "असमेत अनुलेख को अवाहरति करें", + "TaskRefreshLibrary": "माध्यम संग्राहत को छाने", + "TaskCleanActivityLog": "क्रियाकलाप लॉग साफ करें", + "TasksChannelsCategory": "इंटरनेट प्रणाली", + "TasksApplicationCategory": "अनुप्रयोग", + "TaskRefreshPeople": "लोगोकी जानकारी ताज़ी करें" } diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json index c63cd2b94..d01295419 100644 --- a/Emby.Server.Implementations/Localization/Core/hr.json +++ b/Emby.Server.Implementations/Localization/Core/hr.json @@ -123,5 +123,6 @@ "External": "Vanjski", "TaskKeyframeExtractorDescription": "Izvlačenje ključnih okvira iz videozapisa za stvaranje objektivnije HLS liste za reprodukciju. Pokretanje ovog zadatka može potrajati.", "TaskKeyframeExtractor": "Izvoditelj ključnog okvira", - "TaskOptimizeDatabaseDescription": "Sažima bazu podataka i uklanja prazan prostor. Pokretanje ovog zadatka, može poboljšati performanse nakon provođenja indeksiranja biblioteke ili provođenja drugih promjena koje utječu na bazu podataka." + "TaskOptimizeDatabaseDescription": "Sažima bazu podataka i uklanja prazan prostor. Pokretanje ovog zadatka, može poboljšati performanse nakon provođenja indeksiranja biblioteke ili provođenja drugih promjena koje utječu na bazu podataka.", + "HearingImpaired": "Oštećen sluh" } diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json index c7f2f9c85..62d48cebd 100644 --- a/Emby.Server.Implementations/Localization/Core/hu.json +++ b/Emby.Server.Implementations/Localization/Core/hu.json @@ -123,5 +123,6 @@ "TaskOptimizeDatabase": "Adatbázis optimalizálása", "TaskKeyframeExtractor": "Kulcskockák kibontása", "TaskKeyframeExtractorDescription": "Kulcskockákat bont ki a videofájlokból, hogy pontosabb HLS lejátszási listákat hozzon létre. Ez a feladat hosszú ideig tarthat.", - "External": "Külső" + "External": "Külső", + "HearingImpaired": "Hallássérült" } diff --git a/Emby.Server.Implementations/Localization/Core/id.json b/Emby.Server.Implementations/Localization/Core/id.json index 3e05525c8..87ce07da3 100644 --- a/Emby.Server.Implementations/Localization/Core/id.json +++ b/Emby.Server.Implementations/Localization/Core/id.json @@ -82,7 +82,7 @@ "MessageServerConfigurationUpdated": "Konfigurasi server telah diperbarui", "MessageNamedServerConfigurationUpdatedWithValue": "Bagian konfigurasi server {0} telah diperbarui", "FailedLoginAttemptWithUserName": "Gagal melakukan login dari {0}", - "CameraImageUploadedFrom": "Gambar kamera baru telah diunggah dari {0}", + "CameraImageUploadedFrom": "Sebuah gambar kamera baru telah diunggah dari {0}", "DeviceOfflineWithName": "{0} telah terputus", "DeviceOnlineWithName": "{0} telah terhubung", "NotificationOptionVideoPlaybackStopped": "Pemutaran video berhenti", @@ -122,5 +122,6 @@ "TaskOptimizeDatabase": "Optimalkan basis data", "TaskKeyframeExtractorDescription": "Ekstrak bingkai utama dari file video untuk membuat daftar putar HLS yang lebih tepat. Tugas ini dapat berjalan untuk waktu yang lama.", "TaskKeyframeExtractor": "Ekstraktor Bingkai Utama", - "External": "Luar" + "External": "Luar", + "HearingImpaired": "Gangguan Pendengaran" } diff --git a/Emby.Server.Implementations/Localization/Core/is.json b/Emby.Server.Implementations/Localization/Core/is.json index b262a8b42..a40f49506 100644 --- a/Emby.Server.Implementations/Localization/Core/is.json +++ b/Emby.Server.Implementations/Localization/Core/is.json @@ -107,5 +107,14 @@ "TasksApplicationCategory": "Forrit", "TasksLibraryCategory": "Miðlasafn", "TasksMaintenanceCategory": "Viðhald", - "Default": "Sjálfgefið" + "Default": "Sjálfgefið", + "TaskCleanActivityLog": "Hreinsa athafnaskrá", + "TaskRefreshPeople": "Endurnýja fólk", + "TaskDownloadMissingSubtitles": "Sækja texta sem vantar", + "TaskOptimizeDatabase": "Fínstilla gagnagrunn", + "Undefined": "Óskilgreint", + "TaskCleanLogsDescription": "Eyðir færslu skrám sem eru meira en {0} gömul.", + "TaskCleanLogs": "Hreinsa færslu skrá", + "TaskDownloadMissingSubtitlesDescription": "Leitar á netinu að texta sem vantar miðað við uppsetningu lýsigagna.", + "HearingImpaired": "Heyrnarskertur" } diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json index 2aa84c536..3710f03e0 100644 --- a/Emby.Server.Implementations/Localization/Core/it.json +++ b/Emby.Server.Implementations/Localization/Core/it.json @@ -123,5 +123,6 @@ "TaskOptimizeDatabase": "Ottimizza Database", "TaskKeyframeExtractor": "Estrattore di Keyframe", "TaskKeyframeExtractorDescription": "Estrae i keyframe dai video per creare migliori playlist HLS. Questa procedura potrebbe richiedere molto tempo.", - "External": "Esterno" + "External": "Esterno", + "HearingImpaired": "con problemi di udito" } diff --git a/Emby.Server.Implementations/Localization/Core/ja.json b/Emby.Server.Implementations/Localization/Core/ja.json index d90d705b2..7b059c68e 100644 --- a/Emby.Server.Implementations/Localization/Core/ja.json +++ b/Emby.Server.Implementations/Localization/Core/ja.json @@ -37,8 +37,8 @@ "MessageNamedServerConfigurationUpdatedWithValue": "サーバー設定項目の {0} が更新されました", "MessageServerConfigurationUpdated": "サーバー設定が更新されました", "MixedContent": "ミックスコンテンツ", - "Movies": "ムービー", - "Music": "ミュージック", + "Movies": "映画", + "Music": "音楽", "MusicVideos": "ミュージックビデオ", "NameInstallFailed": "{0}のインストールに失敗しました", "NameSeasonNumber": "シーズン {0}", @@ -122,5 +122,6 @@ "TaskOptimizeDatabase": "データベースの最適化", "TaskKeyframeExtractorDescription": "より正確なHLSプレイリストを作成するため、動画ファイルからキーフレームを抽出する。この処理には時間がかかる場合があります。", "TaskKeyframeExtractor": "キーフレーム抽出", - "External": "外部" + "External": "外部", + "HearingImpaired": "聴覚障害の方" } diff --git a/Emby.Server.Implementations/Localization/Core/jbo.json b/Emby.Server.Implementations/Localization/Core/jbo.json index 0967ef424..1b47bb2f2 100644 --- a/Emby.Server.Implementations/Localization/Core/jbo.json +++ b/Emby.Server.Implementations/Localization/Core/jbo.json @@ -1 +1,7 @@ -{} +{ + "Albums": "lo albuma", + "Artists": "lo larpra", + "Books": "lo cukta", + "HeaderAlbumArtists": "lo albuma larpra", + "Playlists": "lo zgipor" +} diff --git a/Emby.Server.Implementations/Localization/Core/ka.json b/Emby.Server.Implementations/Localization/Core/ka.json new file mode 100644 index 000000000..dbbc81eeb --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/ka.json @@ -0,0 +1,127 @@ +{ + "Genres": "ჟანრები", + "HeaderAlbumArtists": "ალბომის შემსრულებლები", + "HeaderFavoriteAlbums": "რჩეული ალბომები", + "TasksApplicationCategory": "აპლიკაცია", + "Albums": "ალბომები", + "AppDeviceValues": "აპი: {0}, მოწყობილობა: {1}", + "Application": "აპლიკაცია", + "Artists": "შემსრულებლები", + "AuthenticationSucceededWithUserName": "{0} -ის ავთენტიკაცია წარმატებულია", + "Books": "წიგნები", + "Forced": "ძალით", + "Inherit": "მემკვიდრეობით", + "Latest": "უახლესი", + "Movies": "ფილმები", + "Music": "მუსიკა", + "Photos": "ფოტოები", + "Playlists": "დასაკრავი სიები", + "Plugin": "დამატება", + "Shows": "სერიალები", + "Songs": "სიმღერები", + "Sync": "სინქრონიზაცია", + "System": "სისტემა", + "Undefined": "აღუწერელი", + "User": "მომხმარებელი", + "TasksMaintenanceCategory": "რემონტი", + "TasksLibraryCategory": "ბიბლიოთეკა", + "ChapterNameValue": "თავი {0}", + "HeaderContinueWatching": "ყურების გაგრძელება", + "HeaderFavoriteArtists": "რჩეული შემსრულებლები", + "DeviceOfflineWithName": "{0} გაითიშა", + "External": "გარე", + "HeaderFavoriteEpisodes": "რჩეული ეპიზოდები", + "HeaderFavoriteSongs": "რჩეული სიმღერები", + "HeaderRecordingGroups": "ჩამწერი ჯგუფები", + "HearingImpaired": "სმენადაქვეითებული", + "LabelRunningTimeValue": "გაშვებულობის დრო: {0}", + "MessageApplicationUpdatedTo": "Jellyfin-ის სერვერი განახლდა {0}-ზე", + "MessageNamedServerConfigurationUpdatedWithValue": "სერვერის კონფიგურაციის სექცია {0} განახლდა", + "MixedContent": "შერეული შემცველობა", + "MusicVideos": "მუსიკის ვიდეოები", + "NotificationOptionInstallationFailed": "დაყენების შეცდომა", + "NotificationOptionApplicationUpdateInstalled": "აპლიკაციის განახლება დაყენებულია", + "NotificationOptionAudioPlayback": "აუდიოს დაკვრა დაწყებულია", + "NotificationOptionCameraImageUploaded": "კამერის გამოსახულება ატვირთულია", + "NotificationOptionVideoPlaybackStopped": "ვიდეოს დაკვრა გაჩერებულია", + "PluginUninstalledWithName": "{0} წაიშალა", + "ScheduledTaskStartedWithName": "{0} გაეშვა", + "VersionNumber": "ვერსია {0}", + "TasksChannelsCategory": "ინტერნეტ-არხები", + "ValueSpecialEpisodeName": "სპეციალური - {0}", + "TaskRefreshChannelsDescription": "ინტერნეტ-არხის ინფორმაციის განახლება.", + "Channels": "არხები", + "Collections": "კოლექციები", + "Default": "ნაგულისხმები", + "Favorites": "რჩეულები", + "Folders": "საქაღალდეები", + "HeaderFavoriteShows": "რჩეული სერიალები", + "HeaderLiveTV": "ცოცხალი TV", + "HeaderNextUp": "შემდეგი ზემოთ", + "HomeVideos": "სახლის ვიდეოები", + "NameSeasonNumber": "სეზონი {0}", + "NameSeasonUnknown": "სეზონი უცნობია", + "NotificationOptionPluginError": "დამატების შეცდომა", + "NotificationOptionPluginInstalled": "დამატება დაყენებულია", + "NotificationOptionPluginUninstalled": "დამატება წაიშალა", + "ProviderValue": "მომწოდებელი: {0}", + "ScheduledTaskFailedWithName": "{0} ავარიულია", + "TvShows": "TV სერიალები", + "TaskRefreshPeople": "ხალხის განახლება", + "TaskUpdatePlugins": "დამატებების განახლება", + "TaskRefreshChannels": "არხების განახლება", + "TaskOptimizeDatabase": "ბაზების ოპტიმიზაცია", + "TaskKeyframeExtractor": "საკვანძო კადრის გამომღები", + "DeviceOnlineWithName": "{0} შეერთებულია", + "LabelIpAddressValue": "IP მისამართი: {0}", + "NameInstallFailed": "{0}-ის დაყენების შეცდომა", + "NotificationOptionApplicationUpdateAvailable": "ხელმისაწვდომია აპლიკაციის განახლება", + "NotificationOptionAudioPlaybackStopped": "აუდიოს დაკვრა გაჩერებულია", + "NotificationOptionNewLibraryContent": "ახალი შემცველობა დამატებულია", + "NotificationOptionPluginUpdateInstalled": "დამატების განახლება დაყენებულია", + "NotificationOptionServerRestartRequired": "სერვერის გადატვირთვა აუცილებელია", + "NotificationOptionTaskFailed": "დაგეგმილი ამოცანის შეცდომა", + "NotificationOptionUserLockedOut": "მომხმარებელი დაიბლოკა", + "NotificationOptionVideoPlayback": "ვიდეოს დაკვრა დაწყებულია", + "PluginInstalledWithName": "{0} დაყენებულია", + "PluginUpdatedWithName": "{0} განახლდა", + "TaskCleanActivityLog": "აქტივობების ჟურნალის გასუფთავება", + "TaskCleanCache": "ქეშის საქაღალდის გასუფთავება", + "TaskRefreshChapterImages": "თავის სურათების გაშლა", + "TaskRefreshLibrary": "მედიის ბიბლიოთეკის სკანირება", + "TaskCleanLogs": "ჟურნალის საქაღალდის გასუფთავება", + "TaskCleanTranscode": "ტრანსკოდირების საქაღალდის გასუფთავება", + "TaskDownloadMissingSubtitles": "ნაკლული სუბტიტრების გადმოწერა", + "UserDownloadingItemWithValues": "{0} -ი {0}-ს იწერს", + "FailedLoginAttemptWithUserName": "{0}-დან შემოსვლის შეცდომა", + "MessageApplicationUpdated": "Jellyfin-ის სერვერი განახლდა", + "MessageServerConfigurationUpdated": "სერვერის კონფიგურაცია განახლდა", + "ServerNameNeedsToBeRestarted": "საჭიროა {0}-ის გადატვირთვა", + "UserCreatedWithName": "მომხმარებელი {0} შეიქმნა", + "UserDeletedWithName": "მომხმარებელი {0} წაშლილია", + "UserOnlineFromDevice": "{0}-ი ხაზზეა {1}-დან", + "UserOfflineFromDevice": "{0}-ი {1}-დან გაითიშა", + "ItemAddedWithName": "{0} ჩამატებულია ბიბლიოთეკაში", + "ItemRemovedWithName": "{0} წაშლილია ბიბლიოთეკიდან", + "UserLockedOutWithName": "მომხმარებელი {0} დაბლოკილია", + "UserStartedPlayingItemWithValues": "{0} თამაშობს {1}-ს {2}-ზე", + "UserPasswordChangedWithName": "მომხმარებლისთვის {0} პაროლი შეცვლილია", + "UserPolicyUpdatedWithName": "{0}-ის მომხმარებლის პოლიტიკა განახლდა", + "UserStoppedPlayingItemWithValues": "{0}-მა დაამთავრა {1}-ის დაკვრა {2}-ზე", + "TaskRefreshChapterImagesDescription": "თავების მქონე ვიდეოებისთვის მინიატურების შექმნა.", + "TaskKeyframeExtractorDescription": "უფრო ზუსტი HLS დასაკრავი სიებისითვის ვიდეოდან საკვანძო გადრების ამოღება. შეიძლება საკმაო დრო დასჭირდეს.", + "NewVersionIsAvailable": "გადმოსაწერად ხელმისაწვდომია Jellyfin -ის ახალი ვერსია.", + "CameraImageUploadedFrom": "ახალი კამერის გამოსახულება ატვირთულია {0}-დან", + "StartupEmbyServerIsLoading": "Jellyfin სერვერი იტვირთება. მოგვიანებით სცადეთ.", + "SubtitleDownloadFailureFromForItem": "{0}-დან {1}-სთვის სუბტიტრების გადმოწერის შეცდომა", + "ValueHasBeenAddedToLibrary": "{0} დაემატა თქვენს მედიის ბიბლიოთეკას", + "TaskCleanActivityLogDescription": "მითითებულ ასაკზე ძველი ჟურნალის ჩანაწერების წაშლა.", + "TaskCleanCacheDescription": "სისტემისთვის არასაჭირო ქეშის ფაილების წაშლა.", + "TaskRefreshLibraryDescription": "თქვენი მედია ბიბლიოთეკაში ახალი ფაილების ძებნა და მეტამონაცემების განახლება.", + "TaskCleanLogsDescription": "{0} დღეზე ძველი ჟურნალის ფაილების წაშლა.", + "TaskRefreshPeopleDescription": "თქვენს მედიის ბიბლიოთეკაში მსახიობების და რეჟისორების მეტამონაცემების განახლება.", + "TaskUpdatePluginsDescription": "ავტომატურად განახლებადად მონიშნული დამატებების განახლებების გადმოწერა და დაყენება.", + "TaskCleanTranscodeDescription": "ერთ დღეზე უფრო ძველი ტრანსკოდირების ფაილების წაშლა.", + "TaskDownloadMissingSubtitlesDescription": "მეტამონაცემებზე დაყრდნობით ინტერნეტში ნაკლული სუბტიტრების ძებნა.", + "TaskOptimizeDatabaseDescription": "ბაზს შეკუშვა და ადგილის გათავისუფლება. ამ ამოცანის ბიბლიოთეკის სკანირების ან ნებისმიერი ცვლილების, რომელიც ბაზაში რამეს აკეთებს, გაშვებას შეუძლია ბაზის წარმადობა გაზარდოს." +} diff --git a/Emby.Server.Implementations/Localization/Core/km.json b/Emby.Server.Implementations/Localization/Core/km.json new file mode 100644 index 000000000..02f9d4443 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/km.json @@ -0,0 +1,3 @@ +{ + "Albums": "Albums" +} diff --git a/Emby.Server.Implementations/Localization/Core/ko.json b/Emby.Server.Implementations/Localization/Core/ko.json index a4b2e75b3..67dcf5b04 100644 --- a/Emby.Server.Implementations/Localization/Core/ko.json +++ b/Emby.Server.Implementations/Localization/Core/ko.json @@ -123,5 +123,6 @@ "TaskOptimizeDatabase": "데이터베이스 최적화", "TaskKeyframeExtractorDescription": "비디오 파일에서 키프레임을 추출하여 더 정확한 HLS 재생 목록을 만듭니다. 이 작업은 오랫동안 진행될 수 있습니다.", "TaskKeyframeExtractor": "키프레임 추출", - "External": "외부" + "External": "외부", + "HearingImpaired": "청각 장애" } diff --git a/Emby.Server.Implementations/Localization/Core/lt-LT.json b/Emby.Server.Implementations/Localization/Core/lt-LT.json index 232b3ec93..e1c937b6c 100644 --- a/Emby.Server.Implementations/Localization/Core/lt-LT.json +++ b/Emby.Server.Implementations/Localization/Core/lt-LT.json @@ -123,5 +123,6 @@ "TaskKeyframeExtractorDescription": "Iš vaizdo įrašo paruošia reikšminius kadrus, kad būtų sukuriamas tikslenis HLS grojaraštis. Šios užduoties vykdymas gali ilgai užtrukti.", "TaskKeyframeExtractor": "Pagrindinių kadrų ištraukėjas", "TaskOptimizeDatabaseDescription": "Suspaudžia duomenų bazę ir atlaisvina vietą. Paleidžiant šią užduotį, po bibliotekos skenavimo arba kitų veiksmų kurie galimai modifikuoja duomenų bazė, gali pagerinti greitaveiką.", - "External": "Išorinis" + "External": "Išorinis", + "HearingImpaired": "Su klausos sutrikimais" } diff --git a/Emby.Server.Implementations/Localization/Core/lv.json b/Emby.Server.Implementations/Localization/Core/lv.json index e460fd719..f753a369a 100644 --- a/Emby.Server.Implementations/Localization/Core/lv.json +++ b/Emby.Server.Implementations/Localization/Core/lv.json @@ -120,5 +120,6 @@ "Default": "Noklusējuma", "TaskOptimizeDatabaseDescription": "Saspiež datubāzi un atbrīvo atmiņu. Uzdevum palaišana pēc bibliotēku skenēšanas vai citām, ar datubāzi saistītām, izmaiņām iespējams uzlabos ātrdarbību.", "TaskOptimizeDatabase": "Optimizēt datubāzi", - "External": "Ārējais" + "External": "Ārējais", + "HearingImpaired": "Ar dzirdes traucējumiem" } diff --git a/Emby.Server.Implementations/Localization/Core/lzh.json b/Emby.Server.Implementations/Localization/Core/lzh.json new file mode 100644 index 000000000..031a4dac7 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/lzh.json @@ -0,0 +1,6 @@ +{ + "Albums": "辑册", + "Artists": "艺人", + "AuthenticationSucceededWithUserName": "{0} 授之权矣", + "Books": "册" +} diff --git a/Emby.Server.Implementations/Localization/Core/mr.json b/Emby.Server.Implementations/Localization/Core/mr.json index b2227e454..a8fb26b91 100644 --- a/Emby.Server.Implementations/Localization/Core/mr.json +++ b/Emby.Server.Implementations/Localization/Core/mr.json @@ -122,5 +122,6 @@ "External": "बाहेरचा", "DeviceOnlineWithName": "{0} कनेक्ट झाले", "DeviceOfflineWithName": "{0} डिस्कनेक्ट झाला आहे", - "AuthenticationSucceededWithUserName": "{0} यशस्वीरित्या प्रमाणीकृत" + "AuthenticationSucceededWithUserName": "{0} यशस्वीरित्या प्रमाणीकृत", + "HearingImpaired": "कर्णबधीर" } diff --git a/Emby.Server.Implementations/Localization/Core/ms.json b/Emby.Server.Implementations/Localization/Core/ms.json index 3d54a5a95..b2293e4b6 100644 --- a/Emby.Server.Implementations/Localization/Core/ms.json +++ b/Emby.Server.Implementations/Localization/Core/ms.json @@ -39,7 +39,7 @@ "MixedContent": "Kandungan campuran", "Movies": "Filem-filem", "Music": "Muzik", - "MusicVideos": "Video muzik", + "MusicVideos": "Video Muzik", "NameInstallFailed": "{0} pemasangan gagal", "NameSeasonNumber": "Musim {0}", "NameSeasonUnknown": "Musim Tidak Diketahui", @@ -55,7 +55,7 @@ "NotificationOptionPluginInstalled": "Plugin telah dipasang", "NotificationOptionPluginUninstalled": "Plugin telah dinyahpasang", "NotificationOptionPluginUpdateInstalled": "Kemaskini plugin telah dipasang", - "NotificationOptionServerRestartRequired": "", + "NotificationOptionServerRestartRequired": "Perlu mulakan semula server", "NotificationOptionTaskFailed": "Kegagalan tugas berjadual", "NotificationOptionUserLockedOut": "Pengguna telah dikunci", "NotificationOptionVideoPlayback": "Ulangmain video bermula", @@ -109,5 +109,20 @@ "TaskRefreshLibrary": "Imbas Perpustakaan Media", "TaskRefreshChapterImagesDescription": "Membuat gambaran kecil untuk video yang mempunyai bab.", "TaskRefreshChapterImages": "Ekstrak Gambar-gambar Bab", - "TaskCleanCacheDescription": "Menghapuskan fail cache yang tidak lagi diperlukan oleh sistem." + "TaskCleanCacheDescription": "Menghapuskan fail cache yang tidak lagi diperlukan oleh sistem.", + "HearingImpaired": "Lemah Pendengaran", + "TaskRefreshPeopleDescription": "Kemas kini metadata untuk pelakon dan pengarah di dalam perpustakaan media.", + "TaskUpdatePluginsDescription": "Muat turun dan kemas kini plugin yang dikonfigurasi secara automatik.", + "TaskDownloadMissingSubtitlesDescription": "Cari sari kata yang hilang di internet, berdasarkan konfigurasi metadata.", + "TaskOptimizeDatabaseDescription": "Mampatkan pangkalan data dan potong ruang kosong. Pelaksanaan tugas ini selepas pengimbasan perpustakaan boleh membantu membaiki prestasi.", + "TaskRefreshChannels": "Segarkan Saluran-saluran", + "TaskUpdatePlugins": "Kemas kini plugin", + "TaskDownloadMissingSubtitles": "Muat turn sari kata yang tiada", + "TaskCleanTranscodeDescription": "Padam fail transkod yang lebih lama dari satu hari.", + "TaskRefreshChannelsDescription": "Segarkan maklumat saluran internet.", + "TaskCleanTranscode": "Bersihkan direktori transkod", + "External": "Luaran", + "TaskOptimizeDatabase": "Optimumkan pangkalan data", + "TaskKeyframeExtractor": "Ekstrak bingkai kunci", + "TaskKeyframeExtractorDescription": "Ekstrak bingkai kunci dari fail video untuk membina HLS playlist yang lebih tepat. Tugas ini mungkin perlukan masa yang panjang." } diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json index 77ee46a4f..5c7dec7ef 100644 --- a/Emby.Server.Implementations/Localization/Core/nb.json +++ b/Emby.Server.Implementations/Localization/Core/nb.json @@ -123,5 +123,6 @@ "TaskOptimizeDatabaseDescription": "Komprimerer database og frigjør plass. Denne prosessen kan forbedre ytelsen etter skanning av bibliotek eller andre handlinger som fører til databaseendringer.", "TaskKeyframeExtractorDescription": "Trekker ut nøkkelbilder fra videofiler for å skape mere nøyaktige HLS-spillelister. Denne oppgaven kan ta lang tid.", "TaskKeyframeExtractor": "Nøkkelbilde-uttrekker", - "External": "Ekstern" + "External": "Ekstern", + "HearingImpaired": "Hørselshemmet" } diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index 3f22355d6..4eb00d289 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -5,17 +5,17 @@ "Artists": "Artiesten", "AuthenticationSucceededWithUserName": "{0} is succesvol geauthenticeerd", "Books": "Boeken", - "CameraImageUploadedFrom": "Nieuwe camera afbeelding toegevoegd vanaf {0}", + "CameraImageUploadedFrom": "Nieuwe camera-afbeelding toegevoegd vanaf {0}", "Channels": "Kanalen", "ChapterNameValue": "Hoofdstuk {0}", - "Collections": "Verzamelingen", + "Collections": "Collecties", "DeviceOfflineWithName": "Verbinding met {0} is verbroken", "DeviceOnlineWithName": "{0} is verbonden", "FailedLoginAttemptWithUserName": "Mislukte inlogpoging van {0}", "Favorites": "Favorieten", "Folders": "Mappen", "Genres": "Genres", - "HeaderAlbumArtists": "Album Artiesten", + "HeaderAlbumArtists": "Albumartiesten", "HeaderContinueWatching": "Kijken hervatten", "HeaderFavoriteAlbums": "Favoriete albums", "HeaderFavoriteArtists": "Favoriete artiesten", @@ -58,8 +58,8 @@ "NotificationOptionServerRestartRequired": "Server herstart nodig", "NotificationOptionTaskFailed": "Geplande taak mislukt", "NotificationOptionUserLockedOut": "Gebruiker is vergrendeld", - "NotificationOptionVideoPlayback": "Video gestart", - "NotificationOptionVideoPlaybackStopped": "Video gestopt", + "NotificationOptionVideoPlayback": "Afspelen van video gestart", + "NotificationOptionVideoPlaybackStopped": "Afspelen van video gestopt", "Photos": "Foto's", "Playlists": "Afspeellijsten", "Plugin": "Plug-in", @@ -92,36 +92,37 @@ "ValueHasBeenAddedToLibrary": "{0} is toegevoegd aan je mediabibliotheek", "ValueSpecialEpisodeName": "Speciaal - {0}", "VersionNumber": "Versie {0}", - "TaskDownloadMissingSubtitlesDescription": "Zoekt op het internet naar missende ondertiteling gebaseerd op metadata configuratie.", - "TaskDownloadMissingSubtitles": "Download missende ondertiteling", + "TaskDownloadMissingSubtitlesDescription": "Zoekt op het internet naar ontbrekende ondertiteling gebaseerd op metadataconfiguratie.", + "TaskDownloadMissingSubtitles": "Ontbrekende ondertiteling downloaden", "TaskRefreshChannelsDescription": "Vernieuwt informatie van internet kanalen.", - "TaskRefreshChannels": "Vernieuw Kanalen", + "TaskRefreshChannels": "Kanalen vernieuwen", "TaskCleanTranscodeDescription": "Verwijdert transcode bestanden ouder dan 1 dag.", - "TaskCleanLogs": "Log Folder Opschonen", - "TaskCleanTranscode": "Transcode Folder Opschonen", - "TaskUpdatePluginsDescription": "Download en installeert updates voor plugins waar automatisch updaten aan staat.", - "TaskUpdatePlugins": "Update Plugins", - "TaskRefreshPeopleDescription": "Update metadata for acteurs en regisseurs in de media bibliotheek.", - "TaskRefreshPeople": "Vernieuw Personen", + "TaskCleanLogs": "Logboekmap opschonen", + "TaskCleanTranscode": "Transcoderingsmap opschonen", + "TaskUpdatePluginsDescription": "Downloadt en installeert updates van plug-ins waarvoor automatisch bijwerken is ingeschakeld.", + "TaskUpdatePlugins": "Plug-ins bijwerken", + "TaskRefreshPeopleDescription": "Updatet metadata voor acteurs en regisseurs in je mediabibliotheek.", + "TaskRefreshPeople": "Personen vernieuwen", "TaskCleanLogsDescription": "Verwijdert log bestanden ouder dan {0} dagen.", - "TaskRefreshLibraryDescription": "Scant de media bibliotheek voor nieuwe bestanden en vernieuwt de metadata.", - "TaskRefreshLibrary": "Scan Media Bibliotheek", - "TaskRefreshChapterImagesDescription": "Maakt thumbnails aan voor videos met hoofdstukken.", - "TaskRefreshChapterImages": "Hoofdstukafbeeldingen Uitpakken", + "TaskRefreshLibraryDescription": "Scant de mediabibliotheek op nieuwe bestanden en vernieuwt de metadata.", + "TaskRefreshLibrary": "Mediabibliotheek scannen", + "TaskRefreshChapterImagesDescription": "Maakt voorbeeldafbeedingen aan voor video's met hoofdstukken.", + "TaskRefreshChapterImages": "Hoofdstukafbeeldingen extraheren", "TaskCleanCacheDescription": "Verwijdert gecachte bestanden die het systeem niet langer nodig heeft.", - "TaskCleanCache": "Cache Folder Opschonen", - "TasksChannelsCategory": "Internet Kanalen", - "TasksApplicationCategory": "Applicatie", + "TaskCleanCache": "Cache-map opschonen", + "TasksChannelsCategory": "Internetkanalen", + "TasksApplicationCategory": "Toepassing", "TasksLibraryCategory": "Bibliotheek", "TasksMaintenanceCategory": "Onderhoud", - "TaskCleanActivityLogDescription": "Verwijdert activiteiten logs ouder dan de ingestelde tijd.", - "TaskCleanActivityLog": "Leeg activiteiten logboek", + "TaskCleanActivityLogDescription": "Verwijdert activiteitenlogs ouder dan de ingestelde leeftijd.", + "TaskCleanActivityLog": "Activiteitenlogboek legen", "Undefined": "Niet gedefinieerd", "Forced": "Geforceerd", "Default": "Standaard", "TaskOptimizeDatabaseDescription": "Comprimeert de database en trimt vrije ruimte. Het uitvoeren van deze taak kan de prestaties verbeteren, na het scannen van de bibliotheek of andere aanpassingen die invloed hebben op de database.", "TaskOptimizeDatabase": "Database optimaliseren", - "TaskKeyframeExtractorDescription": "Haalt keyframes uit videobestanden om preciezere HLS afspeellijsten te maken. Dit kan lang duren.", - "TaskKeyframeExtractor": "Keyframe Extractor", - "External": "Extern" + "TaskKeyframeExtractorDescription": "Haalt keyframes uit videobestanden om preciezere HLS-afspeellijsten te maken. Deze taak kan lang duren.", + "TaskKeyframeExtractor": "Keyframe-uitpakker", + "External": "Extern", + "HearingImpaired": "Slechthorend" } diff --git a/Emby.Server.Implementations/Localization/Core/or.json b/Emby.Server.Implementations/Localization/Core/or.json new file mode 100644 index 000000000..0e9d81ee8 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/or.json @@ -0,0 +1,4 @@ +{ + "External": "ବହିଃସ୍ଥ", + "Genres": "ଧରଣ" +} diff --git a/Emby.Server.Implementations/Localization/Core/pa.json b/Emby.Server.Implementations/Localization/Core/pa.json index 4ac57b630..1f982feaf 100644 --- a/Emby.Server.Implementations/Localization/Core/pa.json +++ b/Emby.Server.Implementations/Localization/Core/pa.json @@ -28,22 +28,22 @@ "ValueHasBeenAddedToLibrary": "{0} ਤੁਹਾਡੀ ਮੀਡੀਆ ਲਾਇਬ੍ਰੇਰੀ ਵਿੱਚ ਸ਼ਾਮਲ ਕੀਤਾ ਗਿਆ ਹੈ", "UserStoppedPlayingItemWithValues": "{0} ਨੇ {2} 'ਤੇ {1} ਖੇਡਣਾ ਪੂਰਾ ਕਰ ਲਿਆ ਹੈ", "UserStartedPlayingItemWithValues": "{0} {2} 'ਤੇ {1} ਖੇਡ ਰਿਹਾ ਹੈ", - "UserPolicyUpdatedWithName": "ਉਪਭੋਗਤਾ ਨੀਤੀ ਨੂੰ {0} ਲਈ ਅਪਡੇਟ ਕੀਤਾ ਗਿਆ ਹੈ", - "UserPasswordChangedWithName": "ਪਾਸਵਰਡ ਯੂਜ਼ਰ ਲਈ ਬਦਲਿਆ ਗਿਆ ਹੈ {0}", - "UserOnlineFromDevice": "{0} ਤੋਂ isਨਲਾਈਨ ਹੈ {1}", + "UserPolicyUpdatedWithName": "ਵਰਤੋਂਕਾਰ ਨੀਤੀ ਨੂੰ {0} ਲਈ ਅਪਡੇਟ ਕੀਤਾ ਗਿਆ ਹੈ", + "UserPasswordChangedWithName": "{0} ਵਰਤੋਂਕਾਰ ਲਈ ਪਾਸਵਰਡ ਬਦਲਿਆ ਗਿਆ ਸੀ", + "UserOnlineFromDevice": "{0} ਨੂੰ {1} ਤੋਂ ਆਨਲਾਈਨ ਹੈ", "UserOfflineFromDevice": "{0} ਤੋਂ ਡਿਸਕਨੈਕਟ ਹੋ ਗਿਆ ਹੈ {1}", - "UserLockedOutWithName": "ਯੂਜ਼ਰ {0} ਨੂੰ ਲਾਕ ਆਉਟ ਕਰ ਦਿੱਤਾ ਗਿਆ ਹੈ", - "UserDownloadingItemWithValues": "{0} ਡਾ{ਨਲੋਡ ਕਰ ਰਿਹਾ ਹੈ {1}", - "UserDeletedWithName": "ਯੂਜ਼ਰ {0} ਨੂੰ ਮਿਟਾ ਦਿੱਤਾ ਗਿਆ ਹੈ", - "UserCreatedWithName": "ਯੂਜ਼ਰ {0} ਬਣਾਇਆ ਗਿਆ ਹੈ", - "User": "ਯੂਜ਼ਰ", + "UserLockedOutWithName": "ਵਰਤੋਂਕਾਰ {0} ਨੂੰ ਲਾਕ ਕੀਤਾ ਗਿਆ ਹੈ", + "UserDownloadingItemWithValues": "{0} {1} ਨੂੰ ਡਾਊਨਲੋਡ ਕਰ ਰਿਹਾ ਹੈ", + "UserDeletedWithName": "ਵਰਤੋਂਕਾਰ {0} ਨੂੰ ਹਟਾਇਆ ਗਿਆ", + "UserCreatedWithName": "ਵਰਤੋਂਕਾਰ {0} ਬਣਾਇਆ ਗਿਆ ਹੈ", + "User": "ਵਰਤੋਂਕਾਰ", "Undefined": "ਪਰਿਭਾਸ਼ਤ", - "TvShows": "ਟੀਵੀ ਸ਼ੋਅਜ਼", + "TvShows": "ਟੀਵੀ ਸ਼ੋਅ", "System": "ਸਿਸਟਮ", "Sync": "ਸਿੰਕ", - "SubtitleDownloadFailureFromForItem": "ਉਪਸਿਰਲੇਖ {1} ਲਈ {0} ਤੋਂ ਡਾ toਨਲੋਡ ਕਰਨ ਵਿੱਚ ਅਸਫਲ ਰਹੇ", - "StartupEmbyServerIsLoading": "ਜੈਲੀਫਿਨ ਸਰਵਰ ਲੋਡ ਹੋ ਰਿਹਾ ਹੈ. ਕਿਰਪਾ ਕਰਕੇ ਜਲਦੀ ਹੀ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ.", - "Songs": "ਗਾਣੇਂ", + "SubtitleDownloadFailureFromForItem": "ਉਪਸਿਰਲੇਖ {1} ਲਈ {0} ਤੋਂ ਡਾਊਨਲੋਡ ਕਰਨ ਵਿੱਚ ਅਸਫਲ ਰਹੇ", + "StartupEmbyServerIsLoading": "Jellyfin ਸਰਵਰ ਲੋਡ ਹੋ ਰਿਹਾ ਹੈ। ਛੇਤੀ ਹੀ ਫ਼ੇਰ ਕੋਸ਼ਿਸ਼ ਕਰੋ।", + "Songs": "ਗਾਣੇ", "Shows": "ਸ਼ੋਅ", "ServerNameNeedsToBeRestarted": "{0} ਮੁੜ ਚਾਲੂ ਕਰਨ ਦੀ ਲੋੜ ਹੈ", "ScheduledTaskStartedWithName": "{0} ਸ਼ੁਰੂ ਹੋਇਆ", @@ -57,12 +57,12 @@ "Photos": "ਫੋਟੋਆਂ", "NotificationOptionVideoPlaybackStopped": "ਵੀਡੀਓ ਪਲੇਬੈਕ ਰੋਕਿਆ ਗਿਆ", "NotificationOptionVideoPlayback": "ਵੀਡੀਓ ਪਲੇਬੈਕ ਸ਼ੁਰੂ ਹੋਇਆ", - "NotificationOptionUserLockedOut": "ਉਪਭੋਗਤਾ ਨੂੰ ਲਾਕ ਆਉਟ ਕੀਤਾ ਗਿਆ", + "NotificationOptionUserLockedOut": "ਵਰਤੋਂਕਾਰ ਨੂੰ ਲਾਕ ਕੀਤਾ", "NotificationOptionTaskFailed": "ਨਿਰਧਾਰਤ ਕਾਰਜ ਅਸਫਲਤਾ", "NotificationOptionServerRestartRequired": "ਸਰਵਰ ਨੂੰ ਮੁੜ ਚਾਲੂ ਕਰਨ ਦੀ ਲੋੜ ਹੈ", "NotificationOptionPluginUpdateInstalled": "ਪਲੱਗਇਨ ਅਪਡੇਟ ਇੰਸਟੌਲ ਕੀਤਾ ਗਿਆ", "NotificationOptionPluginUninstalled": "ਪਲੱਗਇਨ ਅਣਇੰਸਟੌਲ ਕੀਤਾ", - "NotificationOptionPluginInstalled": "ਪਲੱਗਇਨ ਸਥਾਪਿਤ ਕੀਤਾ", + "NotificationOptionPluginInstalled": "ਪਲੱਗਇਨ ਇੰਸਟਾਲ ਕੀਤੀ", "NotificationOptionPluginError": "ਪਲੱਗਇਨ ਅਸਫਲ", "NotificationOptionNewLibraryContent": "ਨਵੀਂ ਸਮੱਗਰੀ ਸ਼ਾਮਲ ਕੀਤੀ ਗਈ", "NotificationOptionInstallationFailed": "ਇੰਸਟਾਲੇਸ਼ਨ ਅਸਫਲ", @@ -92,7 +92,7 @@ "HomeVideos": "ਘਰੇਲੂ ਵੀਡੀਓ", "HeaderRecordingGroups": "ਰਿਕਾਰਡਿੰਗ ਸਮੂਹ", "HeaderNextUp": "ਅੱਗੇ", - "HeaderLiveTV": "ਲਾਈਵ ਟੀ", + "HeaderLiveTV": "ਲਾਈਵ ਟੀਵੀ", "HeaderFavoriteSongs": "ਮਨਪਸੰਦ ਗਾਣੇ", "HeaderFavoriteShows": "ਮਨਪਸੰਦ ਸ਼ੋਅ", "HeaderFavoriteEpisodes": "ਮਨਪਸੰਦ ਐਪੀਸੋਡ", @@ -102,20 +102,22 @@ "HeaderAlbumArtists": "ਐਲਬਮ ਕਲਾਕਾਰ", "Genres": "ਸ਼ੈਲੀਆਂ", "Forced": "ਮਜਬੂਰ", - "Folders": "ਫੋਲਡਰਸ", + "Folders": "ਫੋਲਡਰ", "Favorites": "ਮਨਪਸੰਦ", - "FailedLoginAttemptWithUserName": "ਤੋਂ ਲਾਗਇਨ ਕੋਸ਼ਿਸ਼ ਫੇਲ ਹੋਈ {0}", + "FailedLoginAttemptWithUserName": "{0} ਤੋਂ ਲਾਗਇਨ ਕੋਸ਼ਿਸ਼ ਫੇਲ ਹੋਈ", "DeviceOnlineWithName": "{0} ਜੁੜਿਆ ਹੋਇਆ ਹੈ", "DeviceOfflineWithName": "{0} ਡਿਸਕਨੈਕਟ ਹੋ ਗਿਆ ਹੈ", "Default": "ਡਿਫੌਲਟ", "Collections": "ਸੰਗ੍ਰਹਿਣ", - "ChapterNameValue": "ਅਧਿਆਇ {0}", + "ChapterNameValue": "ਚੈਪਟਰ {0}", "Channels": "ਚੈਨਲ", - "CameraImageUploadedFrom": "ਤੋਂ ਇੱਕ ਨਵਾਂ ਕੈਮਰਾ ਚਿੱਤਰ ਅਪਲੋਡ ਕੀਤਾ ਗਿਆ ਹੈ {0}", + "CameraImageUploadedFrom": "{0} ਤੋਂ ਇੱਕ ਨਵਾਂ ਕੈਮਰਾ ਚਿੱਤਰ ਅਪਲੋਡ ਕੀਤਾ ਗਿਆ ਹੈ", "Books": "ਕਿਤਾਬਾਂ", "AuthenticationSucceededWithUserName": "{0} ਸਫਲਤਾਪੂਰਕ ਪ੍ਰਮਾਣਿਤ", "Artists": "ਕਲਾਕਾਰ", "Application": "ਐਪਲੀਕੇਸ਼ਨ", "AppDeviceValues": "ਐਪ: {0}, ਜੰਤਰ: {1}", - "Albums": "ਐਲਬਮਾਂ" + "Albums": "ਐਲਬਮਾਂ", + "TaskOptimizeDatabase": "ਡਾਟਾਬੇਸ ਅਨੁਕੂਲ ਬਣਾਓ", + "External": "ਬਾਹਰੀ" } diff --git a/Emby.Server.Implementations/Localization/Core/pl.json b/Emby.Server.Implementations/Localization/Core/pl.json index d0b458a8f..d4c15ac87 100644 --- a/Emby.Server.Implementations/Localization/Core/pl.json +++ b/Emby.Server.Implementations/Localization/Core/pl.json @@ -123,5 +123,6 @@ "TaskOptimizeDatabaseDescription": "Kompaktuje bazę danych i obcina wolne miejsce. Uruchomienie tego zadania po przeskanowaniu biblioteki lub dokonaniu innych zmian, które pociągają za sobą modyfikacje bazy danych, może poprawić wydajność.", "External": "Zewnętrzny", "TaskKeyframeExtractorDescription": "Wyodrębnia klatki kluczowe z plików wideo w celu utworzenia bardziej precyzyjnych list odtwarzania HLS. To zadanie może trwać przez długi czas.", - "TaskKeyframeExtractor": "Ekstraktor klatek kluczowych" + "TaskKeyframeExtractor": "Ekstraktor klatek kluczowych", + "HearingImpaired": "Niedosłyszący" } diff --git a/Emby.Server.Implementations/Localization/Core/pr.json b/Emby.Server.Implementations/Localization/Core/pr.json index 506c14fdc..87800a2fe 100644 --- a/Emby.Server.Implementations/Localization/Core/pr.json +++ b/Emby.Server.Implementations/Localization/Core/pr.json @@ -13,5 +13,16 @@ "DeviceOfflineWithName": "{0} abandoned ship", "AppDeviceValues": "Captain: {0}, Ship: {1}", "CameraImageUploadedFrom": "Yer looking glass has glimpsed another painting from {0}", - "Collections": "Barrels" + "Collections": "Barrels", + "ItemAddedWithName": "{0} is now with yer treasure", + "Default": "Normal-like", + "FailedLoginAttemptWithUserName": "Ye failed to get in, try from {0}", + "Favorites": "Finest Loot", + "ItemRemovedWithName": "{0} was taken from yer treasure", + "LabelIpAddressValue": "Ship's coordinates: {0}", + "Genres": "types o' booty", + "TaskDownloadMissingSubtitlesDescription": "Scours the seven seas o' the internet for subtitles that be missin' based on the captain's map o' metadata.", + "HeaderAlbumArtists": "Buccaneers o' the musical arts", + "HeaderFavoriteAlbums": "Beloved booty o' musical adventures", + "HeaderFavoriteArtists": "Treasured scallywags o' the creative seas" } diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json index 38a36a7e0..b9b93b7b6 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-BR.json +++ b/Emby.Server.Implementations/Localization/Core/pt-BR.json @@ -123,5 +123,6 @@ "TaskOptimizeDatabase": "Otimizar base de dados", "TaskKeyframeExtractor": "Extrator de quadro-chave", "TaskKeyframeExtractorDescription": "Extrai quadros-chave de arquivos de vídeo para criar listas de reprodução HLS mais precisas. Esta tarefa pode ser executada por um longo tempo.", - "External": "Externo" + "External": "Externo", + "HearingImpaired": "Deficiência Auditiva" } diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json index 7047f1c28..a75182f22 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-PT.json +++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json @@ -123,5 +123,6 @@ "TaskOptimizeDatabase": "Otimizar base de dados", "TaskKeyframeExtractorDescription": "Extrai quadros-chave de ficheiros de video para criar listas de reprodução HLS mais precisas. Esta tarefa pode demorar algum tempo.", "TaskKeyframeExtractor": "Extrator de Quadros-chave", - "External": "Externo" + "External": "Externo", + "HearingImpaired": "Surdo" } diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json index c2c77ccab..2281e80c8 100644 --- a/Emby.Server.Implementations/Localization/Core/pt.json +++ b/Emby.Server.Implementations/Localization/Core/pt.json @@ -120,5 +120,8 @@ "TaskCleanActivityLogDescription": "Apaga itens no registro com idade acima do que é configurado.", "TaskOptimizeDatabase": "Otimizar base de dados", "TaskOptimizeDatabaseDescription": "Base de dados compacta e corta espaço livre. A execução desta tarefa depois de digitalizar a biblioteca ou de fazer outras alterações que impliquem modificações na base de dados pode melhorar o desempenho.", - "External": "Externo" + "External": "Externo", + "HearingImpaired": "Problemas auditivos", + "TaskKeyframeExtractor": "Extrator de quadro-chave", + "TaskKeyframeExtractorDescription": "Retira frames chave do video para criar listas HLS precisas. Esta tarefa pode correr durante algum tempo." } diff --git a/Emby.Server.Implementations/Localization/Core/ro.json b/Emby.Server.Implementations/Localization/Core/ro.json index 53456269a..2c10bb477 100644 --- a/Emby.Server.Implementations/Localization/Core/ro.json +++ b/Emby.Server.Implementations/Localization/Core/ro.json @@ -11,7 +11,7 @@ "UserOfflineFromDevice": "{0} s-a deconectat de la {1}", "UserLockedOutWithName": "Utilizatorul {0} a fost blocat", "UserDownloadingItemWithValues": "{0} descarcă {1}", - "UserDeletedWithName": "Utilizatorul {0} a fost eliminat", + "UserDeletedWithName": "Utilizatorul {0} a fost șters", "UserCreatedWithName": "Utilizatorul {0} a fost creat", "User": "Utilizator", "TvShows": "Seriale TV", @@ -20,33 +20,33 @@ "SubtitleDownloadFailureFromForItem": "Subtitrările nu au putut fi descărcate de la {0} pentru {1}", "StartupEmbyServerIsLoading": "Se încarcă serverul Jellyfin. Încercați din nou în scurt timp.", "Songs": "Melodii", - "Shows": "Spectacole", - "ServerNameNeedsToBeRestarted": "{0} trebuie repornit", + "Shows": "Seriale", + "ServerNameNeedsToBeRestarted": "{0} trebuie să fie repornit", "ScheduledTaskStartedWithName": "{0} pornit/ă", "ScheduledTaskFailedWithName": "{0} eșuat/ă", "ProviderValue": "Furnizor: {0}", "PluginUpdatedWithName": "{0} a fost actualizat/ă", "PluginUninstalledWithName": "{0} a fost dezinstalat", "PluginInstalledWithName": "{0} a fost instalat", - "Plugin": "Plugin", - "Playlists": "Liste redare", + "Plugin": "Extensie", + "Playlists": "Liste de redare", "Photos": "Fotografii", "NotificationOptionVideoPlaybackStopped": "Redarea video oprită", "NotificationOptionVideoPlayback": "Redare video începută", "NotificationOptionUserLockedOut": "Utilizatorul a fost blocat", - "NotificationOptionTaskFailed": "Activitate programata eșuată", + "NotificationOptionTaskFailed": "Activitate programată eșuată", "NotificationOptionServerRestartRequired": "Este necesară repornirea serverului", - "NotificationOptionPluginUpdateInstalled": "Actualizare plugin instalată", - "NotificationOptionPluginUninstalled": "Plugin dezinstalat", - "NotificationOptionPluginInstalled": "Plugin instalat", - "NotificationOptionPluginError": "Plugin-ul a eșuat", - "NotificationOptionNewLibraryContent": "Adăugat conținut nou", - "NotificationOptionInstallationFailed": "Eșec la instalare", - "NotificationOptionCameraImageUploaded": "Încarcată imagine cameră", + "NotificationOptionPluginUpdateInstalled": "Actualizarea extensiei este instalată", + "NotificationOptionPluginUninstalled": "Extensie dezinstalată", + "NotificationOptionPluginInstalled": "Extensie instalată", + "NotificationOptionPluginError": "Eroare de extensie", + "NotificationOptionNewLibraryContent": "A fost adăugat conținut nou", + "NotificationOptionInstallationFailed": "Instalare eșuată", + "NotificationOptionCameraImageUploaded": "Imagine încarcată", "NotificationOptionAudioPlaybackStopped": "Redare audio oprită", "NotificationOptionAudioPlayback": "A început redarea audio", "NotificationOptionApplicationUpdateInstalled": "Actualizarea aplicației a fost instalată", - "NotificationOptionApplicationUpdateAvailable": "Disponibilă o actualizare a aplicației", + "NotificationOptionApplicationUpdateAvailable": "Este disponibilă o actualizare a aplicației", "NewVersionIsAvailable": "O nouă versiune a Jellyfin Server este disponibilă pentru descărcare.", "NameSeasonUnknown": "Sezon Necunoscut", "NameSeasonNumber": "Sezonul {0}", @@ -54,8 +54,8 @@ "MusicVideos": "Videoclipuri muzicale", "Music": "Muzică", "Movies": "Filme", - "MixedContent": "Conținut mixt", - "MessageServerConfigurationUpdated": "Configurația serverului a fost actualizată", + "MixedContent": "Conținut amestecat", + "MessageServerConfigurationUpdated": "Configurarea serverului a fost actualizată", "MessageNamedServerConfigurationUpdatedWithValue": "Secțiunea de configurare a serverului {0} a fost acualizata", "MessageApplicationUpdatedTo": "Jellyfin Server a fost actualizat la {0}", "MessageApplicationUpdated": "Jellyfin Server a fost actualizat", @@ -69,7 +69,7 @@ "HeaderRecordingGroups": "Grupuri de înregistrare", "HeaderLiveTV": "TV în Direct", "HeaderFavoriteSongs": "Melodii Favorite", - "HeaderFavoriteShows": "Spectacole Favorite", + "HeaderFavoriteShows": "Seriale TV Favorite", "HeaderFavoriteEpisodes": "Episoade Favorite", "HeaderFavoriteArtists": "Artiști Favoriți", "HeaderFavoriteAlbums": "Albume Favorite", @@ -97,10 +97,10 @@ "TaskRefreshChannels": "Actualizează canale", "TaskCleanTranscodeDescription": "Șterge fișierele de transcodare mai vechi de o zi.", "TaskCleanTranscode": "Curățați directorul de transcodare", - "TaskUpdatePluginsDescription": "Descarcă și instalează actualizări pentru pluginuri care sunt configurate să se actualizeze automat.", - "TaskUpdatePlugins": "Actualizați plugin-uri", + "TaskUpdatePluginsDescription": "Descarcă și instalează actualizări pentru extensiile care sunt configurate să se actualizeze automat.", + "TaskUpdatePlugins": "Actualizați Extensile", "TaskRefreshPeopleDescription": "Actualizează metadatele pentru actori și regizori din biblioteca media.", - "TaskRefreshPeople": "Actualizează oamenii", + "TaskRefreshPeople": "Actualizează Persoanele", "TaskCleanLogsDescription": "Șterge fișierele jurnal care au mai mult de {0} zile.", "TaskCleanLogs": "Curățare director jurnal", "TaskRefreshLibraryDescription": "Scanează biblioteca media pentru fișiere noi și reîmprospătează metadatele.", @@ -114,13 +114,14 @@ "TasksLibraryCategory": "Librărie", "TasksMaintenanceCategory": "Mentenanță", "TaskCleanActivityLogDescription": "Șterge intrările din jurnalul de activitate mai vechi de data configurată.", - "TaskCleanActivityLog": "Curăță Jurnalul de Activitate", + "TaskCleanActivityLog": "Curăță Jurnalul de Activități", "Undefined": "Nedefinit", "Forced": "Forțat", "Default": "Implicit", - "TaskOptimizeDatabaseDescription": "Compactează baza de date și trunchiază spațiul liber. Rularea acestei sarcini după scanarea bibliotecii sau după efectuarea altor modificări care implică modificări ale bazei de date poate îmbunătăți performanța.", + "TaskOptimizeDatabaseDescription": "Comprimă baza de date și trunchiază spațiul liber. Rularea acestei sarcini după scanarea bibliotecii sau după efectuarea altor modificări care implică modificări ale bazei de date poate îmbunătăți performanța.", "TaskOptimizeDatabase": "Optimizează baza de date", "TaskKeyframeExtractorDescription": "Extrage cadrele cheie din fișierele video pentru a crea liste de redare HLS mai precise. Această sarcină poate rula o perioadă lungă de timp.", "External": "Extern", - "TaskKeyframeExtractor": "Extractor de cadre cheie" + "TaskKeyframeExtractor": "Extractor de cadre cheie", + "HearingImpaired": "Ascultare Impară" } diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json index ea9a82d2b..421513341 100644 --- a/Emby.Server.Implementations/Localization/Core/ru.json +++ b/Emby.Server.Implementations/Localization/Core/ru.json @@ -1,6 +1,6 @@ { "Albums": "Альбомы", - "AppDeviceValues": "Приложение.: {0}, Устройство.: {1}", + "AppDeviceValues": "Приложение: {0}, Устройство: {1}", "Application": "Приложение", "Artists": "Исполнители", "AuthenticationSucceededWithUserName": "{0} - авторизация успешна", @@ -16,14 +16,14 @@ "Folders": "Папки", "Genres": "Жанры", "HeaderAlbumArtists": "Исполнители альбома", - "HeaderContinueWatching": "Продолжение просмотра", + "HeaderContinueWatching": "Продолжить просмотр", "HeaderFavoriteAlbums": "Избранные альбомы", "HeaderFavoriteArtists": "Избранные исполнители", "HeaderFavoriteEpisodes": "Избранные эпизоды", "HeaderFavoriteShows": "Избранные сериалы", "HeaderFavoriteSongs": "Избранные композиции", "HeaderLiveTV": "Эфир", - "HeaderNextUp": "Очередное", + "HeaderNextUp": "Следующий", "HeaderRecordingGroups": "Группы записей", "HomeVideos": "Домашние видео", "Inherit": "Наследуемое", @@ -42,7 +42,7 @@ "MusicVideos": "Муз. видео", "NameInstallFailed": "Установка {0} неудачна", "NameSeasonNumber": "Сезон {0}", - "NameSeasonUnknown": "Сезон неопознан", + "NameSeasonUnknown": "Сезон не опознан", "NewVersionIsAvailable": "Новая версия Jellyfin Server доступна для загрузки.", "NotificationOptionApplicationUpdateAvailable": "Имеется обновление приложения", "NotificationOptionApplicationUpdateInstalled": "Обновление приложения установлено", @@ -50,7 +50,7 @@ "NotificationOptionAudioPlaybackStopped": "Воспроизведение аудио остановлено", "NotificationOptionCameraImageUploaded": "Изображения с камеры загружены", "NotificationOptionInstallationFailed": "Сбой установки", - "NotificationOptionNewLibraryContent": "Новое содержание добавлено", + "NotificationOptionNewLibraryContent": "Новое содержимое добавлено", "NotificationOptionPluginError": "Сбой плагина", "NotificationOptionPluginInstalled": "Плагин установлен", "NotificationOptionPluginUninstalled": "Плагин удалён", @@ -70,12 +70,12 @@ "ScheduledTaskFailedWithName": "{0} - неудачна", "ScheduledTaskStartedWithName": "{0} - запущена", "ServerNameNeedsToBeRestarted": "Необходим перезапуск {0}", - "Shows": "Передачи", + "Shows": "Телешоу", "Songs": "Композиции", "StartupEmbyServerIsLoading": "Jellyfin Server загружается. Повторите попытку в ближайшее время.", "SubtitleDownloadFailureForItem": "Субтитры к {0} не удалось загрузить", "SubtitleDownloadFailureFromForItem": "Субтитры к {1} не удалось загрузить с {0}", - "Sync": "Синхро", + "Sync": "Синхронизация", "System": "Система", "TvShows": "ТВ", "User": "Пользователь", @@ -96,7 +96,7 @@ "TaskRefreshChannels": "Обновление каналов", "TaskCleanTranscode": "Очистка каталога перекодировки", "TaskUpdatePlugins": "Обновление плагинов", - "TaskRefreshPeople": "Подновление людей", + "TaskRefreshPeople": "Обновление информации о персонах", "TaskCleanLogs": "Очистка каталога журналов", "TaskRefreshLibrary": "Сканирование медиатеки", "TaskRefreshChapterImages": "Извлечение изображений сцен", @@ -117,11 +117,12 @@ "TaskCleanActivityLogDescription": "Удаляет записи журнала активности старше установленного возраста.", "TaskCleanActivityLog": "Очистка журнала активности", "Undefined": "Не определено", - "Forced": "Форсир-ые", + "Forced": "Принудительно", "Default": "По умолчанию", "TaskOptimizeDatabaseDescription": "Сжимает базу данных и вырезает свободные места. Выполнение этой задачи после сканирования библиотеки или внесения других изменений, предполагающих модификации базы данных, может повысить производительность.", "TaskOptimizeDatabase": "Оптимизация базы данных", "TaskKeyframeExtractorDescription": "Извлекаются ключевые кадры из видеофайлов для создания более точных списков плей-листов HLS. Эта задача может выполняться в течение длительного времени.", "TaskKeyframeExtractor": "Извлечение ключевых кадров", - "External": "Внешние" + "External": "Внешние", + "HearingImpaired": "Для слабослышащих" } diff --git a/Emby.Server.Implementations/Localization/Core/sk.json b/Emby.Server.Implementations/Localization/Core/sk.json index 7502969a6..858cc40dd 100644 --- a/Emby.Server.Implementations/Localization/Core/sk.json +++ b/Emby.Server.Implementations/Localization/Core/sk.json @@ -123,5 +123,6 @@ "TaskOptimizeDatabase": "Optimalizovať databázu", "TaskKeyframeExtractorDescription": "Extrahuje kľúčové snímky z video súborov na vytvorenie presnejších HLS playlistov. Táto úloha môže trvať dlhšiu dobu.", "TaskKeyframeExtractor": "Extraktor kľúčových snímkov", - "External": "Externé" + "External": "Externé", + "HearingImpaired": "Sluchovo Postihnutý" } diff --git a/Emby.Server.Implementations/Localization/Core/sl-SI.json b/Emby.Server.Implementations/Localization/Core/sl-SI.json index d845accac..4c23f71ef 100644 --- a/Emby.Server.Implementations/Localization/Core/sl-SI.json +++ b/Emby.Server.Implementations/Localization/Core/sl-SI.json @@ -123,5 +123,6 @@ "TaskOptimizeDatabase": "Optimiziraj bazo podatkov", "TaskKeyframeExtractor": "Ekstraktor ključnih sličic", "External": "Zunanji", - "TaskKeyframeExtractorDescription": "Iz video datoteke Izvleče ključne sličice, da ustvari bolj natančne sezname predvajanja HLS. Proces lahko traja dolgo časa." + "TaskKeyframeExtractorDescription": "Iz video datoteke Izvleče ključne sličice, da ustvari bolj natančne sezname predvajanja HLS. Proces lahko traja dolgo časa.", + "HearingImpaired": "Oslabljen sluh" } diff --git a/Emby.Server.Implementations/Localization/Core/sn.json b/Emby.Server.Implementations/Localization/Core/sn.json new file mode 100644 index 000000000..74720e764 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/sn.json @@ -0,0 +1,28 @@ +{ + "HeaderAlbumArtists": "Vaimbi vemadambarefu", + "HeaderContinueWatching": "Simudzira kuona", + "HeaderFavoriteSongs": "Nziyo dzaunofarira", + "Albums": "Dambarefu", + "AppDeviceValues": "Apu: {0}, Dhivhaisi: {1}", + "Application": "Purogiramu", + "Artists": "Vaimbi", + "AuthenticationSucceededWithUserName": "apinda", + "Books": "Mabhuku", + "CameraImageUploadedFrom": "Mufananidzo mutsva vabva pakamera {0}", + "Channels": "Machanewo", + "ChapterNameValue": "Chikamu {0}", + "Collections": "Akafanana", + "Default": "Zvakasarudzwa Kare", + "DeviceOfflineWithName": "{0} haasisipo", + "DeviceOnlineWithName": "{0} aripo", + "External": "Zvekunze", + "FailedLoginAttemptWithUserName": "Vatadza kuloga chimboedza kushandisa {0}", + "Favorites": "Zvaunofarira", + "Folders": "Mafoodha", + "Forced": "Zvekumanikidzira", + "Genres": "Mhando", + "HeaderFavoriteAlbums": "Madambarefu aunofarira", + "HeaderFavoriteArtists": "Vaimbi vaunofarira", + "HeaderFavoriteEpisodes": "Maepisodhi aunofarira", + "HeaderFavoriteShows": "Masirisi aunofarira" +} diff --git a/Emby.Server.Implementations/Localization/Core/sq.json b/Emby.Server.Implementations/Localization/Core/sq.json index 2766dab06..d1b73a3eb 100644 --- a/Emby.Server.Implementations/Localization/Core/sq.json +++ b/Emby.Server.Implementations/Localization/Core/sq.json @@ -119,5 +119,9 @@ "Forced": "I detyruar", "Default": "Parazgjedhur", "TaskOptimizeDatabaseDescription": "Kompakton bazën e të dhënave dhe shkurton hapësirën e lirë. Drejtimi i kësaj detyre pasi skanoni bibliotekën ose bëni ndryshime të tjera që nënkuptojnë modifikime të bazës së të dhënave mund të përmirësojë performancën.", - "TaskOptimizeDatabase": "Optimizo databazën" + "TaskOptimizeDatabase": "Optimizo databazën", + "TaskKeyframeExtractorDescription": "Nxjerrë kornizat kryesore nga skedarët video për të krijuar lista luajtjeje më të sakta HLS. Ky veprim mund të dojë një kohë të gjatë për tu kompletuar.", + "TaskKeyframeExtractor": "Nxjerrës i kornizës kryesore", + "External": "Jashtem", + "HearingImpaired": "Dëgjimi i dëmtuar" } diff --git a/Emby.Server.Implementations/Localization/Core/sr.json b/Emby.Server.Implementations/Localization/Core/sr.json index 1be8867f4..9739358df 100644 --- a/Emby.Server.Implementations/Localization/Core/sr.json +++ b/Emby.Server.Implementations/Localization/Core/sr.json @@ -122,5 +122,6 @@ "TaskOptimizeDatabaseDescription": "Сажима базу података и скраћује слободан простор. Покретање овог задатка након скенирања библиотеке или других промена које подразумевају измене базе података које могу побољшати перформансе.", "External": "Спољно", "TaskKeyframeExtractorDescription": "Екстрактује кљулне сличице из видео датотека да би креирао више преицзну HLS плеј-листу. Овај задатак може да потраје дуже време.", - "TaskKeyframeExtractor": "Екстрактор кључних сличица" + "TaskKeyframeExtractor": "Екстрактор кључних сличица", + "HearingImpaired": "ослабљен слух" } diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json index af5db1976..785e6b226 100644 --- a/Emby.Server.Implementations/Localization/Core/sv.json +++ b/Emby.Server.Implementations/Localization/Core/sv.json @@ -66,7 +66,7 @@ "PluginInstalledWithName": "{0} installerades", "PluginUninstalledWithName": "{0} avinstallerades", "PluginUpdatedWithName": "{0} uppdaterades", - "ProviderValue": "Källa: {0}", + "ProviderValue": "Leverantör: {0}", "ScheduledTaskFailedWithName": "{0} misslyckades", "ScheduledTaskStartedWithName": "{0} startades", "ServerNameNeedsToBeRestarted": "{0} behöver startas om", @@ -123,5 +123,6 @@ "TaskOptimizeDatabaseDescription": "Komprimerar databasen och trunkerar ledigt utrymme. Prestandan kan förbättras genom att köra denna aktivitet efter att du har skannat biblioteket eller gjort andra förändringar som indikerar att databasen har modifierats.", "TaskKeyframeExtractorDescription": "Exporterar nyckelbildrutor från videofiler för att skapa mer exakta HLS-spellistor. Denna rutin kan ta lång tid.", "TaskKeyframeExtractor": "Extraktor för nyckelbildrutor", - "External": "Extern" + "External": "Extern", + "HearingImpaired": "Hörselskadad" } diff --git a/Emby.Server.Implementations/Localization/Core/th.json b/Emby.Server.Implementations/Localization/Core/th.json index 9407a7b92..1a4fef64e 100644 --- a/Emby.Server.Implementations/Localization/Core/th.json +++ b/Emby.Server.Implementations/Localization/Core/th.json @@ -120,5 +120,6 @@ "Forced": "บังคับใช้", "TaskOptimizeDatabase": "ปรับปรุงประสิทธิภาพฐานข้อมูล", "TaskOptimizeDatabaseDescription": "ลดขนาดการจัดเก็บฐานข้อมูล ใช้งานคำสั่งนี้หลังจากสแกนไลบรารีหรือหลังจากการเปลี่ยนแปลงฐานข้อมูล อาจจะทำให้ระบบทำงานเร็วขึ้น", - "External": "ภายนอก" + "External": "ภายนอก", + "HearingImpaired": "บกพร่องทางการได้ยิน" } diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json index 3e0fd11c8..ff77fb8c5 100644 --- a/Emby.Server.Implementations/Localization/Core/uk.json +++ b/Emby.Server.Implementations/Localization/Core/uk.json @@ -86,7 +86,7 @@ "Shows": "Шоу", "ServerNameNeedsToBeRestarted": "{0} потрібно перезапустити", "ScheduledTaskStartedWithName": "{0} розпочато", - "ScheduledTaskFailedWithName": "Помилка {0}", + "ScheduledTaskFailedWithName": "{0} незавершено, збій", "ProviderValue": "Постачальник: {0}", "PluginUpdatedWithName": "{0} оновлено", "PluginUninstalledWithName": "{0} видалено", @@ -122,5 +122,6 @@ "TaskOptimizeDatabaseDescription": "Стискає базу даних та збільшує вільний простір. Виконання цього завдання після сканування медіатеки або внесення інших змін, які передбачають модифікацію бази даних може покращити продуктивність.", "TaskKeyframeExtractorDescription": "Витягує ключові кадри з відеофайлів для створення більш точних списків відтворення HLS. Це завдання може виконуватися протягом тривалого часу.", "TaskKeyframeExtractor": "Екстрактор ключових кадрів", - "External": "Зовнішній" + "External": "Зовнішній", + "HearingImpaired": "З порушеннями слуху" } diff --git a/Emby.Server.Implementations/Localization/Core/ur_PK.json b/Emby.Server.Implementations/Localization/Core/ur_PK.json index e7f3e492c..5d3f19432 100644 --- a/Emby.Server.Implementations/Localization/Core/ur_PK.json +++ b/Emby.Server.Implementations/Localization/Core/ur_PK.json @@ -5,18 +5,18 @@ "HeaderAlbumArtists": "البم کے فنکار", "Movies": "فلمیں", "HeaderFavoriteEpisodes": "پسندیدہ اقساط", - "Collections": "مجموعہ", + "Collections": "مجموعے", "Folders": "فولڈرز", "HeaderLiveTV": "براہ راست ٹی وی", "Channels": "چینلز", "HeaderContinueWatching": "دیکھنا جاری رکھیں", "Playlists": "پلے لسٹس", - "ValueSpecialEpisodeName": "خاص - {0}", - "Shows": "شوز", + "ValueSpecialEpisodeName": "خصوصی - {0}", + "Shows": "دکھاتا ہے", "Genres": "انواع", "Artists": "فنکار", - "Sync": "مطابقت", - "Photos": "تصوریں", + "Sync": "مطابقت پذیری", + "Photos": "تصاویر", "Albums": "البمز", "Favorites": "پسندیدہ", "Songs": "گانے", @@ -102,8 +102,8 @@ "LabelIpAddressValue": "آئ پی ایڈریس {0}", "ItemRemovedWithName": "لائبریری سے ہٹا دیا گیا ھے", "ItemAddedWithName": "[0} لائبریری میں شامل کیا گیا ھے", - "Inherit": "وراثت میں", - "HomeVideos": "ہوم ویڈیو", + "Inherit": "وراثت", + "HomeVideos": "ہوم ویڈیوز", "HeaderRecordingGroups": "ریکارڈنگ گروپس", "FailedLoginAttemptWithUserName": "{0} سے لاگ ان کی ناکام کوشش", "DeviceOnlineWithName": "{0} متصل ھو چکا ھے", @@ -115,5 +115,13 @@ "AppDeviceValues": "پروگرام:{0}, ڈیوائس:{1}", "Forced": "جَبری", "Undefined": "غير وضاحتى", - "Default": "طے شدہ" + "Default": "طے شدہ", + "TaskKeyframeExtractorDescription": "زیادہ درست HLS پلے لسٹس بنانے کے لیے ویڈیو فائلوں سے کلیدی فریم نکالتا ہے۔ یہ کام طویل عرصے تک چل سکتا ہے۔", + "TaskOptimizeDatabase": "ڈیٹا بیس کو بہتر بنائیں", + "TaskOptimizeDatabaseDescription": "ڈیٹا بیس کو کمپیکٹ کرتا ہے اور خالی جگہ کو چھوٹا کرتا ہے۔ لائبریری کو اسکین کرنے یا دیگر تبدیلیاں کرنے کے بعد اس کام کو چلانے سے کارکردگی بہتر ہو سکتی ہے۔", + "TaskKeyframeExtractor": "کی فریم ایکسٹریکٹر", + "TaskCleanActivityLogDescription": "تشکیل شدہ عمر سے زیادہ پرانی سرگرمی لاگ اندراجات کو حذف کرتا ہے۔", + "External": "بیرونی", + "HearingImpaired": "قوت سماعت سے محروم", + "TaskCleanActivityLog": "سرگرمی لاگ کو صاف کریں" } diff --git a/Emby.Server.Implementations/Localization/Core/vi.json b/Emby.Server.Implementations/Localization/Core/vi.json index b9e2f1e6c..44ce4ac5b 100644 --- a/Emby.Server.Implementations/Localization/Core/vi.json +++ b/Emby.Server.Implementations/Localization/Core/vi.json @@ -122,5 +122,6 @@ "TaskOptimizeDatabase": "Tối ưu hóa cơ sở dữ liệu", "TaskKeyframeExtractor": "Trích Xuất Khung Hình", "TaskKeyframeExtractorDescription": "Trích xuất khung hình chính từ các tệp video để tạo danh sách phát HLS chính xác hơn. Tác vụ này có thể chạy trong một thời gian dài.", - "External": "Bên ngoài" + "External": "Bên ngoài", + "HearingImpaired": "Khiếm Thính" } diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json index a121fc376..03265d3fb 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-CN.json +++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json @@ -14,7 +14,7 @@ "FailedLoginAttemptWithUserName": "从 {0} 尝试登录失败", "Favorites": "我的最爱", "Folders": "文件夹", - "Genres": "风格", + "Genres": "类型", "HeaderAlbumArtists": "专辑艺术家", "HeaderContinueWatching": "继续观看", "HeaderFavoriteAlbums": "收藏的专辑", @@ -123,5 +123,6 @@ "TaskOptimizeDatabase": "优化数据库", "TaskKeyframeExtractorDescription": "从视频文件中提取关键帧以创建更准确的HLS播放列表。这项任务可能需要很长时间。", "TaskKeyframeExtractor": "关键帧提取器", - "External": "外部" + "External": "外部", + "HearingImpaired": "听力障碍" } diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json index 6c8bf7627..e8b8c2c5f 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -4,124 +4,125 @@ "Application": "應用程式", "Artists": "藝人", "AuthenticationSucceededWithUserName": "{0} 授權成功", - "Books": "圖書", - "CameraImageUploadedFrom": "{0} 成功上傳一張新相片", + "Books": "書籍", + "CameraImageUploadedFrom": "{0} 成功上傳一張新照片", "Channels": "頻道", - "ChapterNameValue": "章節 {0}", - "Collections": "合輯", - "DeviceOfflineWithName": "{0} 已經斷開連結", - "DeviceOnlineWithName": "{0} 已經連接", - "FailedLoginAttemptWithUserName": "來自 {0} 的登入失敗", + "ChapterNameValue": "第 {0} 章", + "Collections": "系列", + "DeviceOfflineWithName": "{0} 已斷開連接", + "DeviceOnlineWithName": "{0} 已連接", + "FailedLoginAttemptWithUserName": "{0} 登入失敗", "Favorites": "我的最愛", "Folders": "資料夾", "Genres": "風格", - "HeaderAlbumArtists": "專輯藝人", + "HeaderAlbumArtists": "專輯歌手", "HeaderContinueWatching": "繼續觀看", - "HeaderFavoriteAlbums": "最愛專輯", + "HeaderFavoriteAlbums": "最愛的專輯", "HeaderFavoriteArtists": "最愛的藝人", "HeaderFavoriteEpisodes": "最愛的劇集", "HeaderFavoriteShows": "最愛的節目", "HeaderFavoriteSongs": "最愛的歌曲", "HeaderLiveTV": "電視直播", - "HeaderNextUp": "接下來", + "HeaderNextUp": "接著播放", "HeaderRecordingGroups": "錄製組", "HomeVideos": "家庭影片", "Inherit": "繼承", - "ItemAddedWithName": "{0} 已添加至媒體庫", + "ItemAddedWithName": "{0} 已被添加至媒體庫", "ItemRemovedWithName": "{0} 已從媒體庫移除", "LabelIpAddressValue": "IP 地址: {0}", "LabelRunningTimeValue": "運行時間: {0}", "Latest": "最新", - "MessageApplicationUpdated": "Jellyfin 伺服器已更新", - "MessageApplicationUpdatedTo": "Jellyfin 伺服器已更新至 {0}", - "MessageNamedServerConfigurationUpdatedWithValue": "伺服器設定 {0} 已更新", - "MessageServerConfigurationUpdated": "伺服器設定已經更新", + "MessageApplicationUpdated": "Jellyfin 已被更新", + "MessageApplicationUpdatedTo": "Jellyfin 已被更新至 {0}", + "MessageNamedServerConfigurationUpdatedWithValue": "伺服器設定 {0} 已被更新", + "MessageServerConfigurationUpdated": "伺服器設定已經被更新", "MixedContent": "混合內容", "Movies": "電影", "Music": "音樂", - "MusicVideos": "音樂影片", + "MusicVideos": "MV", "NameInstallFailed": "{0} 安裝失敗", "NameSeasonNumber": "第 {0} 季", - "NameSeasonUnknown": "未知季數", - "NewVersionIsAvailable": "新版本的 Jellyfin 伺服器可供下載。", - "NotificationOptionApplicationUpdateAvailable": "有可用的應用程式更新", - "NotificationOptionApplicationUpdateInstalled": "應用程式已更新", - "NotificationOptionAudioPlayback": "開始播放音頻", - "NotificationOptionAudioPlaybackStopped": "已停止播放音頻", - "NotificationOptionCameraImageUploaded": "相片已上傳", + "NameSeasonUnknown": "未知的季度", + "NewVersionIsAvailable": "有較新版本的 Jellyfin 可供下載。", + "NotificationOptionApplicationUpdateAvailable": "有可用的更新", + "NotificationOptionApplicationUpdateInstalled": "應用程式已被更新", + "NotificationOptionAudioPlayback": "開始播放音訊", + "NotificationOptionAudioPlaybackStopped": "停止播放音訊", + "NotificationOptionCameraImageUploaded": "相片已被上傳", "NotificationOptionInstallationFailed": "安裝失敗", "NotificationOptionNewLibraryContent": "已添加新内容", - "NotificationOptionPluginError": "擴充元件錯誤", - "NotificationOptionPluginInstalled": "擴充元件已安裝", - "NotificationOptionPluginUninstalled": "擴充元件已移除", - "NotificationOptionPluginUpdateInstalled": "擴充元件更新已安裝", - "NotificationOptionServerRestartRequired": "伺服器需要重啓", - "NotificationOptionTaskFailed": "計劃任務失敗", - "NotificationOptionUserLockedOut": "用家已鎖定", - "NotificationOptionVideoPlayback": "開始播放視頻", - "NotificationOptionVideoPlaybackStopped": "已停止播放視頻", + "NotificationOptionPluginError": "插件出現錯誤", + "NotificationOptionPluginInstalled": "插件已被安裝", + "NotificationOptionPluginUninstalled": "插件已被移除", + "NotificationOptionPluginUpdateInstalled": "插件已被更新", + "NotificationOptionServerRestartRequired": "伺服器需要重啟", + "NotificationOptionTaskFailed": "排程任務執行失敗", + "NotificationOptionUserLockedOut": "用戶已被鎖定", + "NotificationOptionVideoPlayback": "開始播放影片", + "NotificationOptionVideoPlaybackStopped": "已停止播放影片", "Photos": "相片", "Playlists": "播放清單", "Plugin": "插件", "PluginInstalledWithName": "已安裝 {0}", "PluginUninstalledWithName": "已移除 {0}", "PluginUpdatedWithName": "已更新 {0}", - "ProviderValue": "提供者: {0}", - "ScheduledTaskFailedWithName": "{0} 任務失敗", - "ScheduledTaskStartedWithName": "{0} 任務開始", - "ServerNameNeedsToBeRestarted": "{0} 需要重啓", + "ProviderValue": "提供者:{0}", + "ScheduledTaskFailedWithName": "{0} 執行失敗", + "ScheduledTaskStartedWithName": "{0} 開始執行", + "ServerNameNeedsToBeRestarted": "{0} 需要重啟", "Shows": "節目", "Songs": "歌曲", - "StartupEmbyServerIsLoading": "Jellyfin 伺服器載入中,請稍後再試。", + "StartupEmbyServerIsLoading": "正在載入 Jellyfin,請稍後再試。", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "無法從 {0} 下載 {1} 的字幕", "Sync": "同步", "System": "系統", "TvShows": "電視節目", - "User": "使用者", - "UserCreatedWithName": "使用者 {0} 已創建", - "UserDeletedWithName": "使用者 {0} 已移除", + "User": "用戶", + "UserCreatedWithName": "用戶 {0} 已被建立", + "UserDeletedWithName": "用戶 {0} 已被移除", "UserDownloadingItemWithValues": "{0} 正在下載 {1}", "UserLockedOutWithName": "使用者 {0} 已被鎖定", - "UserOfflineFromDevice": "{0} 已從 {1} 斷開", - "UserOnlineFromDevice": "{0} 已連綫,來自 {1}", - "UserPasswordChangedWithName": "使用者 {0} 的密碼已變更", + "UserOfflineFromDevice": "{0} 從 {1} 斷開連接", + "UserOnlineFromDevice": "{0} 從 {1} 連線", + "UserPasswordChangedWithName": "{0} 的密碼已被變改", "UserPolicyUpdatedWithName": "使用者協議已更新為 {0}", "UserStartedPlayingItemWithValues": "{0} 正在 {2} 上播放 {1}", - "UserStoppedPlayingItemWithValues": "{0} 已在 {2} 上停止播放 {1}", - "ValueHasBeenAddedToLibrary": "{0} 已添加到你的媒體庫", + "UserStoppedPlayingItemWithValues": "{0} 已停止在 {2} 上播放 {1}", + "ValueHasBeenAddedToLibrary": "已添加 {0} 到你的媒體庫", "ValueSpecialEpisodeName": "特典 - {0}", - "VersionNumber": "版本{0}", - "TaskDownloadMissingSubtitles": "下載遺失的字幕", + "VersionNumber": "版本 {0}", + "TaskDownloadMissingSubtitles": "下載缺少的字幕", "TaskUpdatePlugins": "更新插件", "TasksApplicationCategory": "應用程式", - "TaskRefreshLibraryDescription": "掃描媒體庫以查找新文件並刷新metadata。", + "TaskRefreshLibraryDescription": "掃描媒體庫以加入新增檔案及重新載入 metadata。", "TasksMaintenanceCategory": "維護", - "TaskDownloadMissingSubtitlesDescription": "根據metadata配置在互聯網上搜索缺少的字幕。", - "TaskRefreshChannelsDescription": "刷新互聯網頻道信息。", - "TaskRefreshChannels": "刷新頻道", + "TaskDownloadMissingSubtitlesDescription": "根據元數據中的設定,在互聯網上搜索缺少的字幕。", + "TaskRefreshChannelsDescription": "重新載入網絡頻道的資訊。", + "TaskRefreshChannels": "重新載入頻道", "TaskCleanTranscodeDescription": "刪除超過一天的轉碼文件。", "TaskCleanTranscode": "清理轉碼目錄", - "TaskUpdatePluginsDescription": "下載並安裝配置為自動更新的插件的更新。", + "TaskUpdatePluginsDescription": "下載並更新能夠被自動更新的插件。", "TaskRefreshPeopleDescription": "更新媒體庫中演員和導演的元數據。", - "TaskCleanLogsDescription": "刪除超過{0}天的日誌文件。", - "TaskCleanLogs": "清理日誌目錄", + "TaskCleanLogsDescription": "刪除超過{0}天的紀錄檔。", + "TaskCleanLogs": "清理紀錄檔目錄", "TaskRefreshLibrary": "掃描媒體庫", - "TaskRefreshChapterImagesDescription": "為帶有章節的視頻創建縮略圖。", + "TaskRefreshChapterImagesDescription": "為帶有章節的影片建立縮圖。", "TaskRefreshChapterImages": "提取章節圖像", "TaskCleanCacheDescription": "刪除系統不再需要的緩存文件。", "TaskCleanCache": "清理緩存目錄", - "TasksChannelsCategory": "互聯網頻道", + "TasksChannelsCategory": "網絡頻道", "TasksLibraryCategory": "庫", - "TaskRefreshPeople": "刷新人物", + "TaskRefreshPeople": "重新載入人物", "TaskCleanActivityLog": "清理活動記錄", "Undefined": "未定義", "Forced": "強制", "Default": "預設", "TaskOptimizeDatabaseDescription": "壓縮數據庫並截斷可用空間。在掃描媒體庫或執行其他數據庫的修改後運行此任務可能會提高性能。", "TaskOptimizeDatabase": "最佳化數據庫", - "TaskCleanActivityLogDescription": "刪除早於設定時間的日誌記錄。", - "TaskKeyframeExtractorDescription": "提取關鍵格以創建更準確的HLS播放列表。次指示可能用時很長。", + "TaskCleanActivityLogDescription": "刪除早於設定時間的活動記錄。", + "TaskKeyframeExtractorDescription": "提取關鍵幀以建立更準確的 HLS 播放列表。此工作或需要使用較長時間來完成。", "TaskKeyframeExtractor": "關鍵幀提取器", - "External": "外部" + "External": "外部", + "HearingImpaired": "聽力障礙" } diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json index 102a266f8..36f4df93d 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-TW.json +++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json @@ -37,7 +37,7 @@ "MixedContent": "混合內容", "Movies": "電影", "Music": "音樂", - "MusicVideos": "音樂錄影帶", + "MusicVideos": "MV", "NameInstallFailed": "{0} 安裝失敗", "NameSeasonNumber": "第 {0} 季", "NameSeasonUnknown": "未知季數", @@ -91,14 +91,14 @@ "HeaderRecordingGroups": "錄製組", "Inherit": "繼承", "SubtitleDownloadFailureFromForItem": "無法為 {1} 從 {0} 下載字幕", - "TaskDownloadMissingSubtitlesDescription": "透過中繼資料從網路上搜尋遺失的字幕。", + "TaskDownloadMissingSubtitlesDescription": "透過媒體資訊從網路上搜尋遺失的字幕。", "TaskDownloadMissingSubtitles": "下載遺失的字幕", "TaskRefreshChannels": "重新整理頻道", "TaskUpdatePlugins": "更新附加元件", "TaskRefreshPeople": "更新人物", "TaskCleanLogsDescription": "刪除超過 {0} 天的日誌文件。", "TaskCleanLogs": "清空日誌資料夾", - "TaskRefreshLibraryDescription": "重新掃描媒體庫的新檔案並更新中繼資料。", + "TaskRefreshLibraryDescription": "重新掃描媒體庫的新檔案並更新媒體資訊。", "TaskRefreshLibrary": "重新掃描媒體庫", "TaskRefreshChapterImages": "擷取章節圖片", "TaskCleanCacheDescription": "刪除系統已不需要的快取。", @@ -108,7 +108,7 @@ "TaskCleanTranscodeDescription": "刪除超過一天的轉碼檔案。", "TaskCleanTranscode": "清除轉碼資料夾", "TaskUpdatePluginsDescription": "為已設置為自動更新的附加元件下載並安裝更新。", - "TaskRefreshPeopleDescription": "更新媒體庫中演員和導演的中繼資料。", + "TaskRefreshPeopleDescription": "更新媒體庫中演員和導演的資訊。", "TaskRefreshChapterImagesDescription": "為有章節的影片建立縮圖。", "TasksChannelsCategory": "網路頻道", "TasksApplicationCategory": "應用程式", @@ -122,5 +122,6 @@ "TaskOptimizeDatabase": "最佳化資料庫", "TaskKeyframeExtractorDescription": "將關鍵幀從影片檔案提取出來並建立更精準的HLS播放清單。這可能需要很長時間。", "TaskKeyframeExtractor": "關鍵幀提取器", - "External": "外部" + "External": "外部", + "HearingImpaired": "聽力障礙" } diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index 281dbb00b..96f435399 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Linq; using System.Reflection; using System.Text.Json; using System.Threading.Tasks; @@ -25,7 +26,7 @@ namespace Emby.Server.Implementations.Localization private const string CulturesPath = "Emby.Server.Implementations.Localization.iso6392.txt"; private const string CountriesPath = "Emby.Server.Implementations.Localization.countries.json"; private static readonly Assembly _assembly = typeof(LocalizationManager).Assembly; - private static readonly string[] _unratedValues = { "n/a", "unrated", "not rated" }; + private static readonly string[] _unratedValues = { "n/a", "unrated", "not rated", "nr" }; private readonly IServerConfigurationManager _configurationManager; private readonly ILogger<LocalizationManager> _logger; @@ -86,12 +87,10 @@ namespace Emby.Server.Implementations.Localization var name = parts[0]; dict.Add(name, new ParentalRating(name, value)); } -#if DEBUG else { _logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode); } -#endif } _allParentalRatings[countryCode] = dict; @@ -184,83 +183,149 @@ namespace Emby.Server.Implementations.Localization /// <inheritdoc /> public IEnumerable<ParentalRating> GetParentalRatings() - => GetParentalRatingsDictionary().Values; - - /// <summary> - /// Gets the parental ratings dictionary. - /// </summary> - /// <returns><see cref="Dictionary{String, ParentalRating}" />.</returns> - private Dictionary<string, ParentalRating> GetParentalRatingsDictionary() { - var countryCode = _configurationManager.Configuration.MetadataCountryCode; + // Use server default language for ratings + // Fall back to empty list if there are no parental ratings for that language + var ratings = GetParentalRatingsDictionary()?.Values.ToList() + ?? new List<ParentalRating>(); + + // Add common ratings to ensure them being available for selection + // Based on the US rating system due to it being the main source of rating in the metadata providers + // Unrated + if (!ratings.Any(x => x.Value is null)) + { + ratings.Add(new ParentalRating("Unrated", null)); + } - if (string.IsNullOrEmpty(countryCode)) + // Minimum rating possible + if (ratings.All(x => x.Value != 0)) + { + ratings.Add(new ParentalRating("Approved", 0)); + } + + // Matches PG (this has different age restrictions depending on country) + if (ratings.All(x => x.Value != 10)) + { + ratings.Add(new ParentalRating("10", 10)); + } + + // Matches PG-13 + if (ratings.All(x => x.Value != 13)) + { + ratings.Add(new ParentalRating("13", 13)); + } + + // Matches TV-14 + if (ratings.All(x => x.Value != 14)) + { + ratings.Add(new ParentalRating("14", 14)); + } + + // Catchall if max rating of country is less than 21 + // Using 21 instead of 18 to be sure to allow access to all rated content except adult and banned + if (!ratings.Any(x => x.Value >= 21)) + { + ratings.Add(new ParentalRating("21", 21)); + } + + // A lot of countries don't excplicitly have a seperate rating for adult content + if (ratings.All(x => x.Value != 1000)) { - countryCode = "us"; + ratings.Add(new ParentalRating("XXX", 1000)); } - return GetRatings(countryCode) - ?? GetRatings("us") - ?? throw new InvalidOperationException($"Invalid resource path: '{CountriesPath}'"); + // A lot of countries don't excplicitly have a seperate rating for banned content + if (ratings.All(x => x.Value != 1001)) + { + ratings.Add(new ParentalRating("Banned", 1001)); + } + + return ratings.OrderBy(r => r.Value); } /// <summary> - /// Gets the ratings. + /// Gets the parental ratings dictionary. /// </summary> - /// <param name="countryCode">The country code.</param> - /// <returns>The ratings.</returns> - private Dictionary<string, ParentalRating>? GetRatings(string countryCode) + /// <param name="countryCode">The optional two letter ISO language string.</param> + /// <returns><see cref="Dictionary{String, ParentalRating}" />.</returns> + private Dictionary<string, ParentalRating>? GetParentalRatingsDictionary(string? countryCode = null) { - _allParentalRatings.TryGetValue(countryCode, out var value); + // Fallback to server default if no country code is specified. + if (string.IsNullOrEmpty(countryCode)) + { + countryCode = _configurationManager.Configuration.MetadataCountryCode; + } + + if (_allParentalRatings.TryGetValue(countryCode, out var countryValue)) + { + return countryValue; + } - return value; + return null; } /// <inheritdoc /> - public int? GetRatingLevel(string rating) + public int? GetRatingLevel(string rating, string? countryCode = null) { - if (string.IsNullOrEmpty(rating)) - { - throw new ArgumentNullException(nameof(rating)); - } + ArgumentException.ThrowIfNullOrEmpty(rating); + // Handle unrated content if (_unratedValues.Contains(rating.AsSpan(), StringComparison.OrdinalIgnoreCase)) { return null; } // Fairly common for some users to have "Rated R" in their rating field + rating = rating.Replace("Rated :", string.Empty, StringComparison.OrdinalIgnoreCase); rating = rating.Replace("Rated ", string.Empty, StringComparison.OrdinalIgnoreCase); - var ratingsDictionary = GetParentalRatingsDictionary(); - - if (ratingsDictionary.TryGetValue(rating, out ParentalRating? value)) + // Use rating system matching the language + if (!string.IsNullOrEmpty(countryCode)) { - return value.Value; + var ratingsDictionary = GetParentalRatingsDictionary(countryCode); + if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRating? value)) + { + return value.Value; + } + } + else + { + // Fall back to server default language for ratings check + // If it has no ratings, use the US ratings + var ratingsDictionary = GetParentalRatingsDictionary() ?? GetParentalRatingsDictionary("us"); + if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRating? value)) + { + return value.Value; + } } - // If we don't find anything check all ratings systems + // If we don't find anything, check all ratings systems foreach (var dictionary in _allParentalRatings.Values) { - if (dictionary.TryGetValue(rating, out value)) + if (dictionary.TryGetValue(rating, out var value)) { return value.Value; } } - // Try splitting by : to handle "Germany: FSK 18" - var index = rating.IndexOf(':', StringComparison.Ordinal); - if (index != -1) + // Try splitting by : to handle "Germany: FSK-18" + if (rating.Contains(':', StringComparison.OrdinalIgnoreCase)) { - var trimmedRating = rating.AsSpan(index).TrimStart(':').Trim(); + return GetRatingLevel(rating.AsSpan().RightPart(':').ToString()); + } - if (!trimmedRating.IsEmpty) - { - return GetRatingLevel(trimmedRating.ToString()); - } + // Handle prefix country code to handle "DE-18" + if (rating.Contains('-', StringComparison.OrdinalIgnoreCase)) + { + var ratingSpan = rating.AsSpan(); + + // Extract culture from country prefix + var culture = FindLanguageInfo(ratingSpan.LeftPart('-').ToString()); + + // Check rating system of culture + return GetRatingLevel(ratingSpan.RightPart('-').ToString(), culture?.TwoLetterISOLanguageName); } - // TODO: Further improve by normalizing out all spaces and dashes return null; } @@ -295,10 +360,7 @@ namespace Emby.Server.Implementations.Localization private Dictionary<string, string> GetLocalizationDictionary(string culture) { - if (string.IsNullOrEmpty(culture)) - { - throw new ArgumentNullException(nameof(culture)); - } + ArgumentException.ThrowIfNullOrEmpty(culture); const string Prefix = "Core"; @@ -310,10 +372,7 @@ namespace Emby.Server.Implementations.Localization private async Task<Dictionary<string, string>> GetDictionary(string prefix, string culture, string baseFilename) { - if (string.IsNullOrEmpty(culture)) - { - throw new ArgumentNullException(nameof(culture)); - } + ArgumentException.ThrowIfNullOrEmpty(culture); var dictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); @@ -329,14 +388,14 @@ namespace Emby.Server.Implementations.Localization { await using var stream = _assembly.GetManifestResourceStream(resourcePath); // If a Culture doesn't have a translation the stream will be null and it defaults to en-us further up the chain - if (stream == null) + if (stream is null) { _logger.LogError("Missing translation/culture resource: {ResourcePath}", resourcePath); return; } var dict = await JsonSerializer.DeserializeAsync<Dictionary<string, string>>(stream, _jsonOptions).ConfigureAwait(false); - if (dict == null) + if (dict is null) { throw new InvalidOperationException($"Resource contains invalid data: '{stream}'"); } @@ -386,6 +445,7 @@ namespace Emby.Server.Implementations.Localization yield return new LocalizationOption("Español (Dominicana)", "es_DO"); yield return new LocalizationOption("Español (México)", "es-MX"); yield return new LocalizationOption("Eesti", "et"); + yield return new LocalizationOption("Basque", "eu"); yield return new LocalizationOption("فارسی", "fa"); yield return new LocalizationOption("Suomi", "fi"); yield return new LocalizationOption("Filipino", "fil"); @@ -433,8 +493,8 @@ namespace Emby.Server.Implementations.Localization yield return new LocalizationOption("Українська", "uk"); yield return new LocalizationOption("اُردُو", "ur_PK"); yield return new LocalizationOption("Tiếng Việt", "vi"); - yield return new LocalizationOption("汉语 (简化字)", "zh-CN"); - yield return new LocalizationOption("漢語 (繁体字)", "zh-TW"); + yield return new LocalizationOption("汉语 (简体字)", "zh-CN"); + yield return new LocalizationOption("漢語 (繁體字)", "zh-TW"); yield return new LocalizationOption("廣東話 (香港)", "zh-HK"); } } diff --git a/Emby.Server.Implementations/Localization/Ratings/0-prefer.csv b/Emby.Server.Implementations/Localization/Ratings/0-prefer.csv new file mode 100644 index 000000000..36886ba76 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/0-prefer.csv @@ -0,0 +1,11 @@ +E,0 +EC,0 +T,7 +M,18 +AO,18 +UR,18 +RP,18 +X,1000 +XX,1000 +XXX,1000 +XXXX,1000 diff --git a/Emby.Server.Implementations/Localization/Ratings/au.csv b/Emby.Server.Implementations/Localization/Ratings/au.csv index 11f4ed94c..4ab808ae9 100644 --- a/Emby.Server.Implementations/Localization/Ratings/au.csv +++ b/Emby.Server.Implementations/Localization/Ratings/au.csv @@ -1,7 +1,13 @@ -AU-G,1 -AU-PG,5 -AU-M,6 -AU-MA15+,7 -AU-R18+,9 -AU-X18+,10 -AU-RC,11 +Exempt,0 +G,0 +7+,7 +M,15 +MA,15 +MA15+,15 +PG,16 +16+,16 +R,18 +R18+,18 +X18+,18 +18+,18 +X,1000 diff --git a/Emby.Server.Implementations/Localization/Ratings/be.csv b/Emby.Server.Implementations/Localization/Ratings/be.csv index d3937caf7..d171a7132 100644 --- a/Emby.Server.Implementations/Localization/Ratings/be.csv +++ b/Emby.Server.Implementations/Localization/Ratings/be.csv @@ -1,6 +1,11 @@ -BE-AL,1 -BE-MG6,2 -BE-6,3 -BE-9,5 -BE-12,6 -BE-16,8 +AL,0 +KT,0 +TOUS,0 +MG6,6 +6,6 +9,9 +KNT,12 +12,12 +14,14 +16,16 +18,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/br.csv b/Emby.Server.Implementations/Localization/Ratings/br.csv index e5edaf62c..5ec1eb262 100644 --- a/Emby.Server.Implementations/Localization/Ratings/br.csv +++ b/Emby.Server.Implementations/Localization/Ratings/br.csv @@ -1,6 +1,8 @@ -BR-L,1 -BR-10,5 -BR-12,7 -BR-14,8 -BR-16,8 -BR-18,9 +Livre,0 +L,0 +ER,9 +10,10 +12,12 +14,14 +16,16 +18,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/ca.csv b/Emby.Server.Implementations/Localization/Ratings/ca.csv index 5aef0580f..336ee2806 100644 --- a/Emby.Server.Implementations/Localization/Ratings/ca.csv +++ b/Emby.Server.Implementations/Localization/Ratings/ca.csv @@ -1,6 +1,20 @@ -CA-G,1 -CA-PG,5 -CA-14A,7 -CA-A,8 -CA-18A,9 -CA-R,10 +E,0 +G,0 +TV-Y,0 +TV-G,0 +TV-Y7,7 +TV-Y7-FV,7 +PG,9 +TV-PG,9 +PG-13,13 +13+,13 +TV-14,14 +14A,14 +16+,16 +NC-17,17 +R,18 +TV-MA,18 +18A,18 +18+,18 +A,1000 +Prohibited,1001 diff --git a/Emby.Server.Implementations/Localization/Ratings/co.csv b/Emby.Server.Implementations/Localization/Ratings/co.csv index 9684fa052..e1e96c590 100644 --- a/Emby.Server.Implementations/Localization/Ratings/co.csv +++ b/Emby.Server.Implementations/Localization/Ratings/co.csv @@ -1,8 +1,7 @@ -CO-T,1 -CO-7,5 -CO-12,7 -CO-15,8 -CO-18,10 -CO-X,100 -CO-BANNED,15 -CO-E,15 +T,0 +7,7 +12,12 +15,15 +18,18 +X,1000 +Prohibited,1001 diff --git a/Emby.Server.Implementations/Localization/Ratings/de.csv b/Emby.Server.Implementations/Localization/Ratings/de.csv index f944a140d..d633a5dab 100644 --- a/Emby.Server.Implementations/Localization/Ratings/de.csv +++ b/Emby.Server.Implementations/Localization/Ratings/de.csv @@ -1,10 +1,12 @@ -DE-0,1 -FSK-0,1 -DE-6,5 -FSK-6,5 -DE-12,7 -FSK-12,7 -DE-16,8 -FSK-16,8 -DE-18,9 -FSK-18,9 +Educational,0 +Infoprogramm,0 +FSK-0,0 +0,0 +FSK-6,6 +6,6 +FSK-12,12 +12,12 +FSK-16,16 +16,16 +FSK-18,18 +18,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/dk.csv b/Emby.Server.Implementations/Localization/Ratings/dk.csv index 5364ae1f2..4ef63b2ea 100644 --- a/Emby.Server.Implementations/Localization/Ratings/dk.csv +++ b/Emby.Server.Implementations/Localization/Ratings/dk.csv @@ -1,4 +1,7 @@ -DA-A,1 -DA-7,5 -DA-11,6 -DA-15,8 +F,0 +A,0 +7,7 +11,11 +12,12 +15,15 +16,16 diff --git a/Emby.Server.Implementations/Localization/Ratings/es.csv b/Emby.Server.Implementations/Localization/Ratings/es.csv index 887d91ba6..0bc1d3f7d 100644 --- a/Emby.Server.Implementations/Localization/Ratings/es.csv +++ b/Emby.Server.Implementations/Localization/Ratings/es.csv @@ -1,6 +1,24 @@ -ES-A,1 -ES-APTA,1 -ES-7,3 -ES-12,6 -ES-16,8 -ES-18,11 +A,0 +A/fig,0 +A/i,0 +A/fig/i,0 +APTA,0 +TP,0 +0+,0 +6+,6 +7/fig,7 +7/i,7 +7/i/fig,7 +7,7 +9+,9 +10,10 +12,12 +12/fig,12 +13,13 +14,14 +16,16 +16/fig,16 +18,18 +18/fig,18 +X,1000 +Banned,1001 diff --git a/Emby.Server.Implementations/Localization/Ratings/fi.csv b/Emby.Server.Implementations/Localization/Ratings/fi.csv index 782785890..7ff92f259 100644 --- a/Emby.Server.Implementations/Localization/Ratings/fi.csv +++ b/Emby.Server.Implementations/Localization/Ratings/fi.csv @@ -1,10 +1,10 @@ -FI-S,1 -FI-T,1 -FI-7,4 -FI-12,5 -FI-16,8 -FI-18,9 -FI-K7,4 -FI-K12,5 -FI-K16,8 -FI-K18,9 +S,0 +T,0 +K7,7 +7,7 +K12,12 +12,12 +K16,16 +16,16 +K18,18 +18,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/fr.csv b/Emby.Server.Implementations/Localization/Ratings/fr.csv index f586a3fa9..774a70589 100644 --- a/Emby.Server.Implementations/Localization/Ratings/fr.csv +++ b/Emby.Server.Implementations/Localization/Ratings/fr.csv @@ -1,5 +1,12 @@ -FR-U,1 -FR-10,5 -FR-12,7 -FR-16,9 -FR-18,10 +Public Averti,0 +Tous Publics,0 +U,0 +0+,0 +6+,6 +9+,9 +10,10 +12,12 +14+,14 +16,16 +18,18 +X,1000 diff --git a/Emby.Server.Implementations/Localization/Ratings/gb.csv b/Emby.Server.Implementations/Localization/Ratings/gb.csv index c1f7d0452..75b1c2058 100644 --- a/Emby.Server.Implementations/Localization/Ratings/gb.csv +++ b/Emby.Server.Implementations/Localization/Ratings/gb.csv @@ -1,7 +1,22 @@ -GB-U,1 -GB-PG,5 -GB-12,6 -GB-12A,7 -GB-15,8 -GB-18,9 -GB-R18,15 +All,0 +E,0 +G,0 +U,0 +0+,0 +6+,6 +7+,7 +PG,8 +9+,9 +12,12 +12+,12 +12A,12 +Teen,13 +13+,13 +14+,14 +15,15 +16,16 +Caution,18 +18,18 +Mature,1000 +Adult,1000 +R18,1000 diff --git a/Emby.Server.Implementations/Localization/Ratings/ie.csv b/Emby.Server.Implementations/Localization/Ratings/ie.csv index e42be5cd4..6ef2e5012 100644 --- a/Emby.Server.Implementations/Localization/Ratings/ie.csv +++ b/Emby.Server.Implementations/Localization/Ratings/ie.csv @@ -1,6 +1,9 @@ -IE-G,1 -IE-PG,5 -IE-12A,7 -IE-15A,8 -IE-16,9 -IE-18,10 +G,4 +PG,12 +12,12 +12A,12 +12PG,12 +15,15 +15A,15 +16,16 +18,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/jp.csv b/Emby.Server.Implementations/Localization/Ratings/jp.csv index a8fc2d143..bfb5fdaae 100644 --- a/Emby.Server.Implementations/Localization/Ratings/jp.csv +++ b/Emby.Server.Implementations/Localization/Ratings/jp.csv @@ -1,4 +1,11 @@ -JP-G,1 -JP-PG12,7 -JP-15+,8 -JP-18+,10 +A,0 +G,0 +B,12 +PG12,12 +C,15 +15+,15 +R15+,15 +16+,16 +D,17 +Z,18 +18+,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/kz.csv b/Emby.Server.Implementations/Localization/Ratings/kz.csv index d546bff53..e26b32b67 100644 --- a/Emby.Server.Implementations/Localization/Ratings/kz.csv +++ b/Emby.Server.Implementations/Localization/Ratings/kz.csv @@ -1,7 +1,6 @@ -KZ-6-,0 -KZ-6+,6 -KZ-12+,12 -KZ-14+,14 -KZ-16+,16 -KZ-18+,18 -KZ-21+,21 +K,0 +БА,12 +Б14,14 +E16,16 +E18,18 +HA,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/mx.csv b/Emby.Server.Implementations/Localization/Ratings/mx.csv index 785a8ba22..305912f23 100644 --- a/Emby.Server.Implementations/Localization/Ratings/mx.csv +++ b/Emby.Server.Implementations/Localization/Ratings/mx.csv @@ -1,6 +1,6 @@ -MX-AA,1 -MX-A,5 -MX-B,7 -MX-B-15,8 -MX-C,9 -MX-D,10 +A,0 +AA,0 +B,12 +B-15,15 +C,18 +D,1000 diff --git a/Emby.Server.Implementations/Localization/Ratings/nl.csv b/Emby.Server.Implementations/Localization/Ratings/nl.csv index 8c005092e..44f372b2d 100644 --- a/Emby.Server.Implementations/Localization/Ratings/nl.csv +++ b/Emby.Server.Implementations/Localization/Ratings/nl.csv @@ -1,6 +1,8 @@ -NL-AL,1 -NL-MG6,2 -NL-6,3 -NL-9,5 -NL-12,6 -NL-16,8 +AL,0 +MG6,6 +6,6 +9,9 +12,12 +14,14 +16,16 +18,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/no.csv b/Emby.Server.Implementations/Localization/Ratings/no.csv index 127407be8..c8f8e93db 100644 --- a/Emby.Server.Implementations/Localization/Ratings/no.csv +++ b/Emby.Server.Implementations/Localization/Ratings/no.csv @@ -1,6 +1,9 @@ -NO-A,1 -NO-6,3 -NO-9,4 -NO-12,5 -NO-15,8 -NO-18,9 +A,0 +6,6 +7,7 +9,9 +11,11 +12,12 +15,15 +18,18 +Not approved,1001 diff --git a/Emby.Server.Implementations/Localization/Ratings/nz.csv b/Emby.Server.Implementations/Localization/Ratings/nz.csv index bba99b764..f617f0c39 100644 --- a/Emby.Server.Implementations/Localization/Ratings/nz.csv +++ b/Emby.Server.Implementations/Localization/Ratings/nz.csv @@ -1,11 +1,15 @@ -NZ-G,1 -NZ-PG,5 -NZ-M,6 -NZ-R13,7 -NZ-RP13,7 -NZ-R15,8 -NZ-RP16,9 -NZ-R16,9 -NZ-R18,10 -NZ-R,10 -NZ-MA,10 +Exempt,0 +G,0 +GY,13 +PG,13 +R13,13 +RP13,13 +R15,15 +M,16 +R16,16 +RP16,16 +GA,18 +R18,18 +MA,1000 +R,1001 +Objectionable,1001 diff --git a/Emby.Server.Implementations/Localization/Ratings/ro.csv b/Emby.Server.Implementations/Localization/Ratings/ro.csv index 4089b282f..44c23e248 100644 --- a/Emby.Server.Implementations/Localization/Ratings/ro.csv +++ b/Emby.Server.Implementations/Localization/Ratings/ro.csv @@ -1 +1,6 @@ -RO-AG,1 +AG,0 +AP-12,12 +N-15,15 +IM-18,18 +IM-18-XXX,1000 +IC,1001 diff --git a/Emby.Server.Implementations/Localization/Ratings/ru.csv b/Emby.Server.Implementations/Localization/Ratings/ru.csv index 1bc94affd..8b264070b 100644 --- a/Emby.Server.Implementations/Localization/Ratings/ru.csv +++ b/Emby.Server.Implementations/Localization/Ratings/ru.csv @@ -1,5 +1,6 @@ -RU-0+,1 -RU-6+,3 -RU-12+,7 -RU-16+,9 -RU-18+,10 +0+,0 +6+,6 +12+,12 +16+,16 +18+,18 +Refused classification,1001 diff --git a/Emby.Server.Implementations/Localization/Ratings/se.csv b/Emby.Server.Implementations/Localization/Ratings/se.csv index 1443c07df..e129c3561 100644 --- a/Emby.Server.Implementations/Localization/Ratings/se.csv +++ b/Emby.Server.Implementations/Localization/Ratings/se.csv @@ -1,5 +1,10 @@ -SE-Btl,1 -SE-Barntillåten,1 -SE-7,3 -SE-11,5 -SE-15,8 +Alla,0 +Barntillåten,0 +Btl,0 +0+,0 +7,7 +9+,9 +10+,10 +11,11 +14,14 +15,15 diff --git a/Emby.Server.Implementations/Localization/Ratings/uk.csv b/Emby.Server.Implementations/Localization/Ratings/uk.csv index 6c8005b3f..75b1c2058 100644 --- a/Emby.Server.Implementations/Localization/Ratings/uk.csv +++ b/Emby.Server.Implementations/Localization/Ratings/uk.csv @@ -1,7 +1,22 @@ -UK-U,1 -UK-PG,5 -UK-12,7 -UK-12A,7 -UK-15,9 -UK-18,10 -UK-R18,15 +All,0 +E,0 +G,0 +U,0 +0+,0 +6+,6 +7+,7 +PG,8 +9+,9 +12,12 +12+,12 +12A,12 +Teen,13 +13+,13 +14+,14 +15,15 +16,16 +Caution,18 +18,18 +Mature,1000 +Adult,1000 +R18,1000 diff --git a/Emby.Server.Implementations/Localization/Ratings/us.csv b/Emby.Server.Implementations/Localization/Ratings/us.csv index 34c897fe3..d103ddf42 100644 --- a/Emby.Server.Implementations/Localization/Ratings/us.csv +++ b/Emby.Server.Implementations/Localization/Ratings/us.csv @@ -1,23 +1,50 @@ -TV-Y,1 -APPROVED,1 -G,1 -E,1 -EC,1 -TV-G,1 -TV-Y7,3 -TV-Y7-FV,4 -PG,5 -TV-PG,5 -PG-13,7 -T,7 -TV-14,8 -R,9 -M,9 -TV-MA,9 -NC-17,10 -AO,15 -RP,15 -UR,15 -NR,15 -X,15 -XXX,100 +Approved,0 +G,0 +TV-G,0 +TV-Y,0 +TV-Y7,7 +TV-Y7-FV,7 +PG,10 +PG-13,13 +TV-PG,13 +TV-PG-D,13 +TV-PG-L,13 +TV-PG-S,13 +TV-PG-V,13 +TV-PG-DL,13 +TV-PG-DS,13 +TV-PG-DV,13 +TV-PG-LS,13 +TV-PG-LV,13 +TV-PG-SV,13 +TV-PG-DLS,13 +TV-PG-DLV,13 +TV-PG-DSV,13 +TV-PG-LSV,13 +TV-PG-DLSV,13 +TV-14,14 +TV-14-D,14 +TV-14-L,14 +TV-14-S,14 +TV-14-V,14 +TV-14-DL,14 +TV-14-DS,14 +TV-14-DV,14 +TV-14-LS,14 +TV-14-LV,14 +TV-14-SV,14 +TV-14-DLS,14 +TV-14-DLV,14 +TV-14-DSV,14 +TV-14-LSV,14 +TV-14-DLSV,14 +NC-17,17 +R,17 +TV-MA,17 +TV-MA-L,17 +TV-MA-S,17 +TV-MA-V,17 +TV-MA-LS,17 +TV-MA-LV,17 +TV-MA-SV,17 +TV-MA-LSV,17 diff --git a/Emby.Server.Implementations/Localization/iso6392.txt b/Emby.Server.Implementations/Localization/iso6392.txt index 66fba3330..b55c0fa33 100644 --- a/Emby.Server.Implementations/Localization/iso6392.txt +++ b/Emby.Server.Implementations/Localization/iso6392.txt @@ -77,6 +77,7 @@ chb|||Chibcha|chibcha che||ce|Chechen|tchétchène chg|||Chagatai|djaghataï chi|zho|zh|Chinese|chinois +chi|zho|ze|Chinese; Bilingual|chinois chi|zho|zh-tw|Chinese; Traditional|chinois chi|zho|zh-hk|Chinese; Hong Kong|chinois chk|||Chuukese|chuuk diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs index 6e1dc725d..7732e32d0 100644 --- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs +++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs @@ -15,6 +15,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; @@ -62,23 +63,16 @@ namespace Emby.Server.Implementations.MediaEncoder /// Determines whether [is eligible for chapter image extraction] [the specified video]. /// </summary> /// <param name="video">The video.</param> + /// <param name="libraryOptions">The library options for the video.</param> /// <returns><c>true</c> if [is eligible for chapter image extraction] [the specified video]; otherwise, <c>false</c>.</returns> - private bool IsEligibleForChapterImageExtraction(Video video) + private bool IsEligibleForChapterImageExtraction(Video video, LibraryOptions libraryOptions) { if (video.IsPlaceHolder) { return false; } - var libraryOptions = _libraryManager.GetLibraryOptions(video); - if (libraryOptions != null) - { - if (!libraryOptions.EnableChapterImageExtraction) - { - return false; - } - } - else + if (libraryOptions is null || !libraryOptions.EnableChapterImageExtraction) { return false; } @@ -99,7 +93,9 @@ namespace Emby.Server.Implementations.MediaEncoder public async Task<bool> RefreshChapterImages(Video video, IDirectoryService directoryService, IReadOnlyList<ChapterInfo> chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken) { - if (!IsEligibleForChapterImageExtraction(video)) + var libraryOptions = _libraryManager.GetLibraryOptions(video); + + if (!IsEligibleForChapterImageExtraction(video, libraryOptions)) { extractImages = false; } @@ -179,6 +175,12 @@ namespace Emby.Server.Implementations.MediaEncoder chapter.ImageDateModified = _fileSystem.GetLastWriteTimeUtc(path); changesMade = true; } + else if (libraryOptions?.EnableChapterImageExtraction != true) + { + // We have an image for the current chapter but the user has disabled chapter image extraction -> delete this chapter's image + chapter.ImagePath = null; + changesMade = true; + } } if (saveChapters && changesMade) diff --git a/Emby.Server.Implementations/Net/UdpSocket.cs b/Emby.Server.Implementations/Net/UdpSocket.cs index c3994fc04..577b79283 100644 --- a/Emby.Server.Implementations/Net/UdpSocket.cs +++ b/Emby.Server.Implementations/Net/UdpSocket.cs @@ -74,7 +74,7 @@ namespace Emby.Server.Implementations.Net private void OnReceiveSocketAsyncEventArgsCompleted(object sender, SocketAsyncEventArgs e) { var tcs = _currentReceiveTaskCompletionSource; - if (tcs != null) + if (tcs is not null) { _currentReceiveTaskCompletionSource = null; @@ -98,7 +98,7 @@ namespace Emby.Server.Implementations.Net private void OnSendSocketAsyncEventArgsCompleted(object sender, SocketAsyncEventArgs e) { var tcs = _currentSendTaskCompletionSource; - if (tcs != null) + if (tcs is not null) { _currentSendTaskCompletionSource = null; diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 9e7035cb3..6176879b6 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -70,7 +70,7 @@ namespace Emby.Server.Implementations.Playlists var folderName = _fileSystem.GetValidFilename(name); var parentFolder = GetPlaylistsFolder(Guid.Empty); - if (parentFolder == null) + if (parentFolder is null) { throw new ArgumentException(nameof(parentFolder)); } @@ -81,7 +81,7 @@ namespace Emby.Server.Implementations.Playlists { var item = _libraryManager.GetItemById(itemId); - if (item == null) + if (item is null) { throw new ArgumentException("No item exists with the supplied Id"); } @@ -135,16 +135,8 @@ namespace Emby.Server.Implementations.Playlists { Name = name, Path = path, - Shares = new[] - { - new Share - { - UserId = options.UserId.Equals(default) - ? null - : options.UserId.ToString("N", CultureInfo.InvariantCulture), - CanEdit = true - } - } + OwnerUserId = options.UserId, + Shares = options.Shares ?? Array.Empty<Share>() }; playlist.SetMediaType(options.MediaType); @@ -183,7 +175,7 @@ namespace Emby.Server.Implementations.Playlists private List<BaseItem> GetPlaylistItems(IEnumerable<Guid> itemIds, string playlistMediaType, User user, DtoOptions options) { - var items = itemIds.Select(i => _libraryManager.GetItemById(i)).Where(i => i != null); + var items = itemIds.Select(i => _libraryManager.GetItemById(i)).Where(i => i is not null); return Playlist.GetPlaylistItems(playlistMediaType, items, user, options); } @@ -502,15 +494,8 @@ namespace Emby.Server.Implementations.Playlists private static string MakeRelativePath(string folderPath, string fileAbsolutePath) { - if (string.IsNullOrEmpty(folderPath)) - { - throw new ArgumentException("Folder path was null or empty.", nameof(folderPath)); - } - - if (string.IsNullOrEmpty(fileAbsolutePath)) - { - throw new ArgumentException("File absolute path was null or empty.", nameof(fileAbsolutePath)); - } + ArgumentException.ThrowIfNullOrEmpty(folderPath); + ArgumentException.ThrowIfNullOrEmpty(fileAbsolutePath); if (!folderPath.EndsWith(Path.DirectorySeparatorChar)) { @@ -544,5 +529,55 @@ namespace Emby.Server.Implementations.Playlists return _libraryManager.RootFolder.Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, TypeName, StringComparison.Ordinal)) ?? _libraryManager.GetUserRootFolder().Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, TypeName, StringComparison.Ordinal)); } + + /// <inheritdoc /> + public async Task RemovePlaylistsAsync(Guid userId) + { + var playlists = GetPlaylists(userId); + foreach (var playlist in playlists) + { + // Update owner if shared + var rankedShares = playlist.Shares.OrderByDescending(x => x.CanEdit).ToArray(); + if (rankedShares.Length > 0 && Guid.TryParse(rankedShares[0].UserId, out var guid)) + { + playlist.OwnerUserId = guid; + playlist.Shares = rankedShares.Skip(1).ToArray(); + await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); + + if (playlist.IsFile) + { + SavePlaylistFile(playlist); + } + } + else + { + // Remove playlist if not shared + _libraryManager.DeleteItem( + playlist, + new DeleteOptions + { + DeleteFileLocation = false, + DeleteFromExternalProvider = false + }, + playlist.GetParent(), + false); + } + } + } + + /// <inheritdoc /> + public async Task UpdatePlaylistAsync(Playlist playlist) + { + var currentPlaylist = (Playlist)_libraryManager.GetItemById(playlist.Id); + currentPlaylist.OwnerUserId = playlist.OwnerUserId; + currentPlaylist.Shares = playlist.Shares; + + await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); + + if (currentPlaylist.IsFile) + { + SavePlaylistFile(currentPlaylist); + } + } } } diff --git a/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs b/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs index 8ec9f6161..e2f2e436f 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs @@ -39,7 +39,7 @@ namespace Emby.Server.Implementations.Playlists protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query) { - if (query.User == null) + if (query.User is null) { query.Recursive = false; return base.GetItemsInternal(query); diff --git a/Emby.Server.Implementations/Plugins/PluginLoadContext.cs b/Emby.Server.Implementations/Plugins/PluginLoadContext.cs new file mode 100644 index 000000000..d04e9cf68 --- /dev/null +++ b/Emby.Server.Implementations/Plugins/PluginLoadContext.cs @@ -0,0 +1,33 @@ +using System.Reflection; +using System.Runtime.Loader; + +namespace Emby.Server.Implementations.Plugins; + +/// <summary> +/// A custom <see cref="AssemblyLoadContext"/> for loading Jellyfin plugins. +/// </summary> +public class PluginLoadContext : AssemblyLoadContext +{ + private readonly AssemblyDependencyResolver _resolver; + + /// <summary> + /// Initializes a new instance of the <see cref="PluginLoadContext"/> class. + /// </summary> + /// <param name="path">The path of the plugin assembly.</param> + public PluginLoadContext(string path) : base(true) + { + _resolver = new AssemblyDependencyResolver(path); + } + + /// <inheritdoc /> + protected override Assembly? Load(AssemblyName assemblyName) + { + var assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName); + if (assemblyPath is not null) + { + return LoadFromAssemblyPath(assemblyPath); + } + + return null; + } +} diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs index ec4e0dbeb..f14f87ccd 100644 --- a/Emby.Server.Implementations/Plugins/PluginManager.cs +++ b/Emby.Server.Implementations/Plugins/PluginManager.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Net.Http; using System.Reflection; +using System.Runtime.Loader; using System.Text; using System.Text.Json; using System.Threading.Tasks; @@ -30,6 +31,7 @@ namespace Emby.Server.Implementations.Plugins { private readonly string _pluginsPath; private readonly Version _appVersion; + private readonly List<AssemblyLoadContext> _assemblyLoadContexts; private readonly JsonSerializerOptions _jsonOptions; private readonly ILogger<PluginManager> _logger; private readonly IApplicationHost _appHost; @@ -76,6 +78,8 @@ namespace Emby.Server.Implementations.Plugins _appHost = appHost; _minimumVersion = new Version(0, 0, 0, 1); _plugins = Directory.Exists(_pluginsPath) ? DiscoverPlugins().ToList() : new List<LocalPlugin>(); + + _assemblyLoadContexts = new List<AssemblyLoadContext>(); } private IHttpClientFactory HttpClientFactory @@ -119,43 +123,78 @@ namespace Emby.Server.Implementations.Plugins continue; } + var assemblyLoadContext = new PluginLoadContext(plugin.Path); + _assemblyLoadContexts.Add(assemblyLoadContext); + + var assemblies = new List<Assembly>(plugin.DllFiles.Count); + var loadedAll = true; + foreach (var file in plugin.DllFiles) { - Assembly assembly; try { - assembly = Assembly.LoadFrom(file); - - // Load all required types to verify that the plugin will load - assembly.GetTypes(); + assemblies.Add(assemblyLoadContext.LoadFromAssemblyPath(file)); } catch (FileLoadException ex) { - _logger.LogError(ex, "Failed to load assembly {Path}. Disabling plugin.", file); + _logger.LogError(ex, "Failed to load assembly {Path}. Disabling plugin", file); ChangePluginState(plugin, PluginStatus.Malfunctioned); - continue; + loadedAll = false; + break; + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception ex) +#pragma warning restore CA1031 // Do not catch general exception types + { + _logger.LogError(ex, "Failed to load assembly {Path}. Unknown exception was thrown. Disabling plugin", file); + ChangePluginState(plugin, PluginStatus.Malfunctioned); + loadedAll = false; + break; + } + } + + if (!loadedAll) + { + continue; + } + + foreach (var assembly in assemblies) + { + try + { + // Load all required types to verify that the plugin will load + assembly.GetTypes(); } catch (SystemException ex) when (ex is TypeLoadException or ReflectionTypeLoadException) // Undocumented exception { - _logger.LogError(ex, "Failed to load assembly {Path}. This error occurs when a plugin references an incompatible version of one of the shared libraries. Disabling plugin.", file); + _logger.LogError(ex, "Failed to load assembly {Path}. This error occurs when a plugin references an incompatible version of one of the shared libraries. Disabling plugin", assembly.Location); ChangePluginState(plugin, PluginStatus.NotSupported); - continue; + break; } #pragma warning disable CA1031 // Do not catch general exception types catch (Exception ex) #pragma warning restore CA1031 // Do not catch general exception types { - _logger.LogError(ex, "Failed to load assembly {Path}. Unknown exception was thrown. Disabling plugin.", file); + _logger.LogError(ex, "Failed to load assembly {Path}. Unknown exception was thrown. Disabling plugin", assembly.Location); ChangePluginState(plugin, PluginStatus.Malfunctioned); - continue; + break; } - _logger.LogInformation("Loaded assembly {Assembly} from {Path}", assembly.FullName, file); + _logger.LogInformation("Loaded assembly {Assembly} from {Path}", assembly.FullName, assembly.Location); yield return assembly; } } } + /// <inheritdoc /> + public void UnloadAssemblies() + { + foreach (var assemblyLoadContext in _assemblyLoadContexts) + { + assemblyLoadContext.Unload(); + } + } + /// <summary> /// Creates all the plugin instances. /// </summary> @@ -174,7 +213,7 @@ namespace Emby.Server.Implementations.Plugins foreach (var pluginServiceRegistrator in _appHost.GetExportTypes<IPluginServiceRegistrator>()) { var plugin = GetPluginByAssembly(pluginServiceRegistrator.Assembly); - if (plugin == null) + if (plugin is null) { _logger.LogError("Unable to find plugin in assembly {Assembly}", pluginServiceRegistrator.Assembly.FullName); continue; @@ -210,10 +249,7 @@ namespace Emby.Server.Implementations.Plugins /// <param name="folder">Folder of the plugin.</param> public void ImportPluginFrom(string folder) { - if (string.IsNullOrEmpty(folder)) - { - throw new ArgumentNullException(nameof(folder)); - } + ArgumentException.ThrowIfNullOrEmpty(folder); // Load the plugin. var plugin = LoadManifest(folder); @@ -263,12 +299,12 @@ namespace Emby.Server.Implementations.Plugins { LocalPlugin? plugin; - if (version == null) + if (version is null) { // If no version is given, return the current instance. var plugins = _plugins.Where(p => p.Id.Equals(id)).ToList(); - plugin = plugins.FirstOrDefault(p => p.Instance != null) ?? plugins.OrderByDescending(p => p.Version).FirstOrDefault(); + plugin = plugins.FirstOrDefault(p => p.Instance is not null) ?? plugins.MaxBy(p => p.Version); } else { @@ -320,7 +356,7 @@ namespace Emby.Server.Implementations.Plugins ArgumentNullException.ThrowIfNull(assembly); var plugin = _plugins.FirstOrDefault(p => p.DllFiles.Contains(assembly.Location)); - if (plugin == null) + if (plugin is null) { // A plugin's assembly didn't cause this issue, so ignore it. return; @@ -442,7 +478,7 @@ namespace Emby.Server.Implementations.Plugins _logger.LogDebug("Creating instance of {Type}", type); // _appHost.ServiceProvider is already assigned when we create the plugins var instance = (IPlugin)ActivatorUtilities.CreateInstance(_appHost.ServiceProvider!, type); - if (plugin == null) + if (plugin is null) { // Create a dummy record for the providers. // TODO: remove this code once all provided have been released as separate plugins. @@ -500,7 +536,7 @@ namespace Emby.Server.Implementations.Plugins #pragma warning restore CA1031 // Do not catch general exception types { _logger.LogError(ex, "Error creating {Type}", type.FullName); - if (plugin != null) + if (plugin is not null) { if (ChangePluginState(plugin, PluginStatus.Malfunctioned)) { @@ -523,7 +559,7 @@ namespace Emby.Server.Implementations.Plugins var predecessor = _plugins.OrderByDescending(p => p.Version) .FirstOrDefault(p => p.Id.Equals(plugin.Id) && p.IsEnabledAndSupported && p.Version != plugin.Version); - if (predecessor != null) + if (predecessor is not null) { return; } @@ -577,7 +613,7 @@ namespace Emby.Server.Implementations.Plugins _logger.LogError(ex, "Error deserializing {Json}.", Encoding.UTF8.GetString(data!)); } - if (manifest != null) + if (manifest is not null) { if (!Version.TryParse(manifest.TargetAbi, out var targetAbi)) { @@ -711,10 +747,11 @@ namespace Emby.Server.Implementations.Plugins && p.IsEnabledAndSupported && p.Version != plugin.Version); - if (previousVersion == null) + if (previousVersion is null) { // This value is memory only - so that the web will show restart required. plugin.Manifest.Status = PluginStatus.Restart; + plugin.Manifest.AutoUpdate = false; return; } @@ -729,6 +766,7 @@ namespace Emby.Server.Implementations.Plugins // This value is memory only - so that the web will show restart required. plugin.Manifest.Status = PluginStatus.Restart; + plugin.Manifest.AutoUpdate = false; } } } diff --git a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs index 532c8d1e3..c4bda9637 100644 --- a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs +++ b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs @@ -71,25 +71,10 @@ namespace Emby.Server.Implementations.QuickConnect /// <inheritdoc/> public QuickConnectResult TryConnect(AuthorizationInfo authorizationInfo) { - if (string.IsNullOrEmpty(authorizationInfo.DeviceId)) - { - throw new ArgumentException(nameof(authorizationInfo.DeviceId) + " is required"); - } - - if (string.IsNullOrEmpty(authorizationInfo.Device)) - { - throw new ArgumentException(nameof(authorizationInfo.Device) + " is required"); - } - - if (string.IsNullOrEmpty(authorizationInfo.Client)) - { - throw new ArgumentException(nameof(authorizationInfo.Client) + " is required"); - } - - if (string.IsNullOrEmpty(authorizationInfo.Version)) - { - throw new ArgumentException(nameof(authorizationInfo.Version) + "is required"); - } + ArgumentException.ThrowIfNullOrEmpty(authorizationInfo.DeviceId); + ArgumentException.ThrowIfNullOrEmpty(authorizationInfo.Device); + ArgumentException.ThrowIfNullOrEmpty(authorizationInfo.Client); + ArgumentException.ThrowIfNullOrEmpty(authorizationInfo.Version); AssertActive(); ExpireRequests(); diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs index b370e06ef..1af2c96d2 100644 --- a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs +++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs @@ -93,11 +93,8 @@ namespace Emby.Server.Implementations.ScheduledTasks public ScheduledTaskWorker(IScheduledTask scheduledTask, IApplicationPaths applicationPaths, ITaskManager taskManager, ILogger logger) { ArgumentNullException.ThrowIfNull(scheduledTask); - ArgumentNullException.ThrowIfNull(applicationPaths); - ArgumentNullException.ThrowIfNull(taskManager); - ArgumentNullException.ThrowIfNull(logger); ScheduledTask = scheduledTask; @@ -128,7 +125,7 @@ namespace Emby.Server.Implementations.ScheduledTasks lock (_lastExecutionResultSyncLock) { - if (_lastExecutionResult == null && !_readFromFile) + if (_lastExecutionResult is null && !_readFromFile) { if (File.Exists(path)) { @@ -211,7 +208,7 @@ namespace Emby.Server.Implementations.ScheduledTasks { get { - if (CurrentCancellationTokenSource != null) + if (CurrentCancellationTokenSource is not null) { return CurrentCancellationTokenSource.IsCancellationRequested ? TaskState.Cancelling @@ -240,7 +237,7 @@ namespace Emby.Server.Implementations.ScheduledTasks ArgumentNullException.ThrowIfNull(value); // Cleanup current triggers - if (_triggers != null) + if (_triggers is not null) { DisposeTriggers(); } @@ -269,7 +266,7 @@ namespace Emby.Server.Implementations.ScheduledTasks ArgumentNullException.ThrowIfNull(value); // This null check is not great, but is needed to handle bad user input, or user mucking with the config file incorrectly - var triggerList = value.Where(i => i != null).ToArray(); + var triggerList = value.Where(i => i is not null).ToArray(); SaveTriggers(triggerList); @@ -332,7 +329,7 @@ namespace Emby.Server.Implementations.ScheduledTasks return; } - _logger.LogInformation("{0} fired for task: {1}", trigger.GetType().Name, Name); + _logger.LogDebug("{0} fired for task: {1}", trigger.GetType().Name, Name); trigger.Stop(); @@ -369,7 +366,7 @@ namespace Emby.Server.Implementations.ScheduledTasks private async Task ExecuteInternal(TaskOptions options) { // Cancel the current execution, if any - if (CurrentCancellationTokenSource != null) + if (CurrentCancellationTokenSource is not null) { throw new InvalidOperationException("Cannot execute a Task that is already running"); } @@ -378,7 +375,7 @@ namespace Emby.Server.Implementations.ScheduledTasks CurrentCancellationTokenSource = new CancellationTokenSource(); - _logger.LogInformation("Executing {0}", Name); + _logger.LogDebug("Executing {0}", Name); ((TaskManager)_taskManager).OnTaskExecuting(this); @@ -391,7 +388,7 @@ namespace Emby.Server.Implementations.ScheduledTasks try { - if (options != null && options.MaxRuntimeTicks.HasValue) + if (options is not null && options.MaxRuntimeTicks.HasValue) { CurrentCancellationTokenSource.CancelAfter(TimeSpan.FromTicks(options.MaxRuntimeTicks.Value)); } @@ -406,7 +403,7 @@ namespace Emby.Server.Implementations.ScheduledTasks } catch (Exception ex) { - _logger.LogError(ex, "Error"); + _logger.LogError(ex, "Error executing Scheduled Task"); failureException = ex; @@ -507,7 +504,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 != null).ToArray(); + var settings = LoadTriggerSettings().Where(i => i is not null).ToArray(); return settings.Select(i => new Tuple<TaskTriggerInfo, ITaskTrigger>(i, GetTrigger(i))).ToArray(); } @@ -583,7 +580,7 @@ namespace Emby.Server.Implementations.ScheduledTasks result.Key = ScheduledTask.Key; - if (ex != null) + if (ex is not null) { result.ErrorMessage = ex.Message; result.LongErrorMessage = ex.StackTrace; @@ -617,7 +614,7 @@ namespace Emby.Server.Implementations.ScheduledTasks var startTime = CurrentExecutionStartTime; var token = CurrentCancellationTokenSource; - if (token != null) + if (token is not null) { try { @@ -631,7 +628,7 @@ namespace Emby.Server.Implementations.ScheduledTasks } var task = _currentTask; - if (task != null) + if (task is not null) { try { @@ -653,7 +650,7 @@ namespace Emby.Server.Implementations.ScheduledTasks } } - if (token != null) + if (token is not null) { try { diff --git a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs index 0431858fc..42c30c959 100644 --- a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs +++ b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -43,9 +41,9 @@ namespace Emby.Server.Implementations.ScheduledTasks ScheduledTasks = Array.Empty<IScheduledTaskWorker>(); } - public event EventHandler<GenericEventArgs<IScheduledTaskWorker>> TaskExecuting; + public event EventHandler<GenericEventArgs<IScheduledTaskWorker>>? TaskExecuting; - public event EventHandler<TaskCompletionEventArgs> TaskCompleted; + public event EventHandler<TaskCompletionEventArgs>? TaskCompleted; /// <summary> /// Gets the list of Scheduled Tasks. @@ -94,7 +92,7 @@ namespace Emby.Server.Implementations.ScheduledTasks { var scheduledTask = ScheduledTasks.FirstOrDefault(t => t.ScheduledTask.GetType() == typeof(T)); - if (scheduledTask == null) + if (scheduledTask is null) { _logger.LogError("Unable to find scheduled task of type {0} in QueueScheduledTask.", typeof(T).Name); } @@ -126,7 +124,7 @@ namespace Emby.Server.Implementations.ScheduledTasks { var scheduledTask = ScheduledTasks.FirstOrDefault(t => t.ScheduledTask.GetType() == typeof(T)); - if (scheduledTask == null) + if (scheduledTask is null) { _logger.LogError("Unable to find scheduled task of type {0} in Execute.", typeof(T).Name); } @@ -134,7 +132,7 @@ namespace Emby.Server.Implementations.ScheduledTasks { var type = scheduledTask.ScheduledTask.GetType(); - _logger.LogInformation("Queuing task {0}", type.Name); + _logger.LogDebug("Queuing task {0}", type.Name); lock (_taskQueue) { @@ -155,7 +153,7 @@ namespace Emby.Server.Implementations.ScheduledTasks { var scheduledTask = ScheduledTasks.FirstOrDefault(t => t.ScheduledTask.GetType() == task.GetType()); - if (scheduledTask == null) + if (scheduledTask is null) { _logger.LogError("Unable to find scheduled task of type {0} in QueueScheduledTask.", task.GetType().Name); } @@ -174,7 +172,7 @@ namespace Emby.Server.Implementations.ScheduledTasks { var type = task.ScheduledTask.GetType(); - _logger.LogInformation("Queuing task {0}", type.Name); + _logger.LogDebug("Queuing task {0}", type.Name); lock (_taskQueue) { @@ -256,9 +254,6 @@ namespace Emby.Server.Implementations.ScheduledTasks /// </summary> private void ExecuteQueuedTasks() { - _logger.LogInformation("ExecuteQueuedTasks"); - - // Execute queued tasks lock (_taskQueue) { var list = new List<Tuple<Type, TaskOptions>>(); diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs index 0bf0838fa..6ad6c4cbd 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs @@ -16,6 +16,7 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.Tasks; +using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.ScheduledTasks.Tasks { @@ -24,15 +25,10 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks /// </summary> public class ChapterImagesTask : IScheduledTask { - /// <summary> - /// The _library manager. - /// </summary> + private readonly ILogger<ChapterImagesTask> _logger; private readonly ILibraryManager _libraryManager; - private readonly IItemRepository _itemRepo; - private readonly IApplicationPaths _appPaths; - private readonly IEncodingManager _encodingManager; private readonly IFileSystem _fileSystem; private readonly ILocalizationManager _localization; @@ -40,6 +36,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks /// <summary> /// Initializes a new instance of the <see cref="ChapterImagesTask" /> class. /// </summary> + /// <param name="logger">The logger.</param>. /// <param name="libraryManager">The library manager.</param>. /// <param name="itemRepo">The item repository.</param> /// <param name="appPaths">The application paths.</param> @@ -47,6 +44,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks /// <param name="fileSystem">The filesystem.</param> /// <param name="localization">The localization manager.</param> public ChapterImagesTask( + ILogger<ChapterImagesTask> logger, ILibraryManager libraryManager, IItemRepository itemRepo, IApplicationPaths appPaths, @@ -54,6 +52,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks IFileSystem fileSystem, ILocalizationManager localization) { + _logger = logger; _libraryManager = libraryManager; _itemRepo = itemRepo; _appPaths = appPaths; @@ -101,7 +100,6 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks EnableImages = false }, SourceTypes = new SourceType[] { SourceType.Library }, - HasChapterImages = false, IsVirtualItem = false }) .OfType<Video>() @@ -152,7 +150,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks previouslyFailedImages.Add(key); var parentPath = Path.GetDirectoryName(failHistoryPath); - if (parentPath != null) + if (parentPath is not null) { Directory.CreateDirectory(parentPath); } @@ -167,9 +165,10 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks progress.Report(100 * percent); } - catch (ObjectDisposedException) + catch (ObjectDisposedException ex) { // TODO Investigate and properly fix. + _logger.LogError(ex, "Object Disposed"); break; } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs index 98e45fa46..1f3cb9b63 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs @@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks { private readonly ILogger<OptimizeDatabaseTask> _logger; private readonly ILocalizationManager _localization; - private readonly JellyfinDbProvider _provider; + private readonly IDbContextFactory<JellyfinDbContext> _provider; /// <summary> /// Initializes a new instance of the <see cref="OptimizeDatabaseTask" /> class. @@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks public OptimizeDatabaseTask( ILogger<OptimizeDatabaseTask> logger, ILocalizationManager localization, - JellyfinDbProvider provider) + IDbContextFactory<JellyfinDbContext> provider) { _logger = logger; _localization = localization; @@ -70,30 +70,31 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks } /// <inheritdoc /> - public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) + public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) { _logger.LogInformation("Optimizing and vacuuming jellyfin.db..."); try { - using var context = _provider.CreateContext(); - if (context.Database.IsSqlite()) + var context = await _provider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + await using (context.ConfigureAwait(false)) { - context.Database.ExecuteSqlRaw("PRAGMA optimize"); - context.Database.ExecuteSqlRaw("VACUUM"); - _logger.LogInformation("jellyfin.db optimized successfully!"); - } - else - { - _logger.LogInformation("This database doesn't support optimization"); + if (context.Database.IsSqlite()) + { + await context.Database.ExecuteSqlRawAsync("PRAGMA optimize", cancellationToken).ConfigureAwait(false); + await context.Database.ExecuteSqlRawAsync("VACUUM", cancellationToken).ConfigureAwait(false); + _logger.LogInformation("jellyfin.db optimized successfully!"); + } + else + { + _logger.LogInformation("This database doesn't support optimization"); + } } } catch (Exception e) { _logger.LogError(e, "Error while optimizing jellyfin.db"); } - - return Task.CompletedTask; } } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs index 3eb800199..d65ac2e5e 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs @@ -50,7 +50,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Triggers DateTime triggerDate; - if (lastResult == null) + if (lastResult is null) { // Task has never been completed before triggerDate = DateTime.UtcNow.AddHours(1); @@ -100,7 +100,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Triggers { DisposeTimer(); - if (Triggered != null) + if (Triggered is not null) { _lastStartDate = DateTime.UtcNow; Triggered(this, EventArgs.Empty); diff --git a/Emby.Server.Implementations/ServerApplicationPaths.cs b/Emby.Server.Implementations/ServerApplicationPaths.cs index 369a2b0d8..725df98da 100644 --- a/Emby.Server.Implementations/ServerApplicationPaths.cs +++ b/Emby.Server.Implementations/ServerApplicationPaths.cs @@ -83,24 +83,6 @@ namespace Emby.Server.Implementations public string YearPath => Path.Combine(InternalMetadataPath, "Year"); /// <summary> - /// Gets the path to the General IBN directory. - /// </summary> - /// <value>The general path.</value> - public string GeneralPath => Path.Combine(InternalMetadataPath, "general"); - - /// <summary> - /// Gets the path to the Ratings IBN directory. - /// </summary> - /// <value>The ratings path.</value> - public string RatingsPath => Path.Combine(InternalMetadataPath, "ratings"); - - /// <summary> - /// Gets the media info images path. - /// </summary> - /// <value>The media info images path.</value> - public string MediaInfoImagesPath => Path.Combine(InternalMetadataPath, "mediainfo"); - - /// <summary> /// Gets the path to the user configuration directory. /// </summary> /// <value>The user configuration directory path.</value> diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 0d1029882..5f6dc93fb 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -95,12 +95,6 @@ namespace Emby.Server.Implementations.Session _deviceManager.DeviceOptionsUpdated += OnDeviceManagerDeviceOptionsUpdated; } - /// <inheritdoc /> - public event EventHandler<GenericEventArgs<AuthenticationRequest>> AuthenticationFailed; - - /// <inheritdoc /> - public event EventHandler<GenericEventArgs<AuthenticationResult>> AuthenticationSucceeded; - /// <summary> /// Occurs when playback has started. /// </summary> @@ -200,7 +194,7 @@ namespace Emby.Server.Implementations.Session { var capabilities = _deviceManager.GetCapabilities(info.DeviceId); - if (capabilities != null) + if (capabilities is not null) { ReportCapabilities(info, capabilities, false); } @@ -238,7 +232,7 @@ namespace Emby.Server.Implementations.Session public void UpdateDeviceName(string sessionId, string reportedDeviceName) { var session = GetSession(sessionId); - if (session != null) + if (session is not null) { session.DeviceName = reportedDeviceName; } @@ -264,27 +258,16 @@ namespace Emby.Server.Implementations.Session { CheckDisposed(); - if (string.IsNullOrEmpty(appName)) - { - throw new ArgumentNullException(nameof(appName)); - } - - if (string.IsNullOrEmpty(appVersion)) - { - throw new ArgumentNullException(nameof(appVersion)); - } - - if (string.IsNullOrEmpty(deviceId)) - { - throw new ArgumentNullException(nameof(deviceId)); - } + ArgumentException.ThrowIfNullOrEmpty(appName); + ArgumentException.ThrowIfNullOrEmpty(appVersion); + ArgumentException.ThrowIfNullOrEmpty(deviceId); var activityDate = DateTime.UtcNow; var session = await GetSessionInfo(appName, appVersion, deviceId, deviceName, remoteEndPoint, user).ConfigureAwait(false); var lastActivityDate = session.LastActivityDate; session.LastActivityDate = activityDate; - if (user != null) + if (user is not null) { var userLastActivityDate = user.LastActivityDate ?? DateTime.MinValue; @@ -351,7 +334,7 @@ namespace Emby.Server.Implementations.Session CheckDisposed(); var session = GetSession(sessionId, false); - if (session != null) + if (session is not null) { var key = GetSessionKey(session.Client, session.DeviceId); @@ -377,11 +360,11 @@ namespace Emby.Server.Implementations.Session info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture); } - if (!info.ItemId.Equals(default) && info.Item == null && libraryItem != null) + if (!info.ItemId.Equals(default) && info.Item is null && libraryItem is not null) { var current = session.NowPlayingItem; - if (current == null || !info.ItemId.Equals(current.Id)) + if (current is null || !info.ItemId.Equals(current.Id)) { var runtimeTicks = libraryItem.RunTimeTicks; @@ -390,7 +373,7 @@ namespace Emby.Server.Implementations.Session { mediaSource = await GetMediaSource(libraryItem, info.MediaSourceId, info.LiveStreamId).ConfigureAwait(false); - if (mediaSource != null) + if (mediaSource is not null) { runtimeTicks = mediaSource.RunTimeTicks; } @@ -478,10 +461,7 @@ namespace Emby.Server.Implementations.Session { CheckDisposed(); - if (string.IsNullOrEmpty(deviceId)) - { - throw new ArgumentNullException(nameof(deviceId)); - } + ArgumentException.ThrowIfNullOrEmpty(deviceId); var key = GetSessionKey(appName, deviceId); @@ -495,7 +475,7 @@ namespace Emby.Server.Implementations.Session sessionInfo.UserId = user?.Id ?? Guid.Empty; sessionInfo.UserName = user?.Username; - sessionInfo.UserPrimaryImageTag = user?.ProfileImage == null ? null : GetImageCacheTag(user); + sessionInfo.UserPrimaryImageTag = user?.ProfileImage is null ? null : GetImageCacheTag(user); sessionInfo.RemoteEndPoint = remoteEndPoint; sessionInfo.Client = appName; @@ -506,7 +486,7 @@ namespace Emby.Server.Implementations.Session sessionInfo.ApplicationVersion = appVersion; - if (user == null) + if (user is null) { sessionInfo.AdditionalUsers = Array.Empty<SessionUserInfo>(); } @@ -536,7 +516,7 @@ namespace Emby.Server.Implementations.Session sessionInfo.UserId = user?.Id ?? Guid.Empty; sessionInfo.UserName = username; - sessionInfo.UserPrimaryImageTag = user?.ProfileImage == null ? null : GetImageCacheTag(user); + sessionInfo.UserPrimaryImageTag = user?.ProfileImage is null ? null : GetImageCacheTag(user); sessionInfo.RemoteEndPoint = remoteEndPoint; if (string.IsNullOrEmpty(deviceName)) @@ -570,7 +550,7 @@ namespace Emby.Server.Implementations.Session var user = _userManager.GetUserById(session.UserId); - if (user == null) + if (user is null) { throw new InvalidOperationException("User not found"); } @@ -579,7 +559,7 @@ namespace Emby.Server.Implementations.Session users.AddRange(session.AdditionalUsers .Select(i => _userManager.GetUserById(i.UserId)) - .Where(i => i != null)); + .Where(i => i is not null)); return users; } @@ -591,7 +571,7 @@ namespace Emby.Server.Implementations.Session private void StopIdleCheckTimer() { - if (_idleTimer != null) + if (_idleTimer is not null) { _idleTimer.Dispose(); _idleTimer = null; @@ -600,7 +580,7 @@ namespace Emby.Server.Implementations.Session private async void CheckForIdlePlayback(object state) { - var playingSessions = Sessions.Where(i => i.NowPlayingItem != null) + var playingSessions = Sessions.Where(i => i.NowPlayingItem is not null) .ToList(); if (playingSessions.Count > 0) @@ -618,7 +598,7 @@ namespace Emby.Server.Implementations.Session await OnPlaybackStopped(new PlaybackStopInfo { Item = session.NowPlayingItem, - ItemId = session.NowPlayingItem == null ? Guid.Empty : session.NowPlayingItem.Id, + ItemId = session.NowPlayingItem is null ? Guid.Empty : session.NowPlayingItem.Id, SessionId = session.Id, MediaSourceId = session.PlayState?.MediaSourceId, PositionTicks = session.PlayState?.PositionTicks @@ -626,11 +606,11 @@ namespace Emby.Server.Implementations.Session } catch (Exception ex) { - _logger.LogDebug("Error calling OnPlaybackStopped", ex); + _logger.LogDebug(ex, "Error calling OnPlaybackStopped"); } } - playingSessions = Sessions.Where(i => i.NowPlayingItem != null) + playingSessions = Sessions.Where(i => i.NowPlayingItem is not null) .ToList(); } @@ -643,7 +623,7 @@ namespace Emby.Server.Implementations.Session private BaseItem GetNowPlayingItem(SessionInfo session, Guid itemId) { var item = session.FullNowPlayingItem; - if (item != null && item.Id.Equals(itemId)) + if (item is not null && item.Id.Equals(itemId)) { return item; } @@ -684,7 +664,7 @@ namespace Emby.Server.Implementations.Session var users = GetUsers(session); - if (libraryItem != null) + if (libraryItem is not null) { foreach (var user in users) { @@ -777,7 +757,7 @@ namespace Emby.Server.Implementations.Session var users = GetUsers(session); // only update saved user data on actual check-ins, not automated ones - if (libraryItem != null && !isAutomated) + if (libraryItem is not null && !isAutomated) { foreach (var user in users) { @@ -912,11 +892,11 @@ namespace Emby.Server.Implementations.Session info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture); } - if (!info.ItemId.Equals(default) && info.Item == null && libraryItem != null) + if (!info.ItemId.Equals(default) && info.Item is null && libraryItem is not null) { var current = session.NowPlayingItem; - if (current == null || !info.ItemId.Equals(current.Id)) + if (current is null || !info.ItemId.Equals(current.Id)) { MediaSourceInfo mediaSource = null; @@ -933,7 +913,7 @@ namespace Emby.Server.Implementations.Session } } - if (info.Item != null) + if (info.Item is not null) { var msString = info.PositionTicks.HasValue ? (info.PositionTicks.Value / 10000).ToString(CultureInfo.InvariantCulture) : "unknown"; @@ -945,7 +925,7 @@ namespace Emby.Server.Implementations.Session msString); } - if (info.NowPlayingQueue != null) + if (info.NowPlayingQueue is not null) { session.NowPlayingQueue = info.NowPlayingQueue; } @@ -957,7 +937,7 @@ namespace Emby.Server.Implementations.Session var users = GetUsers(session); var playedToCompletion = false; - if (libraryItem != null) + if (libraryItem is not null) { foreach (var user in users) { @@ -973,7 +953,7 @@ namespace Emby.Server.Implementations.Session } catch (Exception ex) { - _logger.LogError("Error closing live stream", ex); + _logger.LogError(ex, "Error closing live stream"); } } @@ -1037,7 +1017,7 @@ namespace Emby.Server.Implementations.Session private SessionInfo GetSession(string sessionId, bool throwOnMissing = true) { var session = Sessions.FirstOrDefault(i => string.Equals(i.Id, sessionId, StringComparison.Ordinal)); - if (session == null && throwOnMissing) + if (session is null && throwOnMissing) { throw new ResourceNotFoundException( string.Format(CultureInfo.InvariantCulture, "Session {0} not found.", sessionId)); @@ -1051,7 +1031,7 @@ namespace Emby.Server.Implementations.Session // Accept either device id or session id var session = Sessions.FirstOrDefault(i => string.Equals(i.Id, sessionId, StringComparison.Ordinal)); - if (session == null) + if (session is null) { throw new ResourceNotFoundException( string.Format(CultureInfo.InvariantCulture, "Session {0} not found.", sessionId)); @@ -1164,7 +1144,7 @@ namespace Emby.Server.Implementations.Session command.ItemIds = items.Select(i => i.Id).ToArray(); - if (user != null) + if (user is not null) { if (items.Any(i => i.GetPlayAccess(user) != PlayAccess.Full)) { @@ -1173,13 +1153,13 @@ namespace Emby.Server.Implementations.Session } } - if (user != null + if (user is not null && command.ItemIds.Length == 1 && user.EnableNextEpisodeAutoPlay && _libraryManager.GetItemById(command.ItemIds[0]) is Episode episode) { var series = episode.Series; - if (series != null) + if (series is not null) { var episodes = series.GetEpisodes( user, @@ -1231,7 +1211,7 @@ namespace Emby.Server.Implementations.Session { var item = _libraryManager.GetItemById(id); - if (item == null) + if (item is null) { _logger.LogError("A non-existent item Id {0} was passed into TranslateItemForPlayback", id); return Array.Empty<BaseItem>(); @@ -1284,7 +1264,7 @@ namespace Emby.Server.Implementations.Session { var item = _libraryManager.GetItemById(id); - if (item == null) + if (item is null) { _logger.LogError("A non-existent item Id {0} was passed into TranslateItemForInstantMix", id); return new List<BaseItem>(); @@ -1429,7 +1409,7 @@ namespace Emby.Server.Implementations.Session var user = session.AdditionalUsers.FirstOrDefault(i => i.UserId.Equals(userId)); - if (user != null) + if (user is not null) { var list = session.AdditionalUsers.ToList(); list.Remove(user); @@ -1480,9 +1460,9 @@ namespace Emby.Server.Implementations.Session true).ConfigureAwait(false); } - if (user == null) + if (user is null) { - AuthenticationFailed?.Invoke(this, new GenericEventArgs<AuthenticationRequest>(request)); + await _eventManager.PublishAsync(new GenericEventArgs<AuthenticationRequest>(request)).ConfigureAwait(false); throw new AuthenticationException("Invalid username or password entered."); } @@ -1518,8 +1498,7 @@ namespace Emby.Server.Implementations.Session ServerId = _appHost.SystemId }; - AuthenticationSucceeded?.Invoke(this, new GenericEventArgs<AuthenticationResult>(returnResult)); - + await _eventManager.PublishAsync(new GenericEventArgs<AuthenticationResult>(returnResult)).ConfigureAwait(false); return returnResult; } @@ -1541,7 +1520,7 @@ namespace Emby.Server.Implementations.Session foreach (var auth in allExistingForDevice) { - if (existing == null || !string.Equals(auth.AccessToken, existing.AccessToken, StringComparison.Ordinal)) + if (existing is null || !string.Equals(auth.AccessToken, existing.AccessToken, StringComparison.Ordinal)) { try { @@ -1554,7 +1533,7 @@ namespace Emby.Server.Implementations.Session } } - if (existing != null) + if (existing is not null) { _logger.LogInformation("Reissuing access token: {Token}", existing.AccessToken); return existing.AccessToken; @@ -1571,10 +1550,7 @@ namespace Emby.Server.Implementations.Session { CheckDisposed(); - if (string.IsNullOrEmpty(accessToken)) - { - throw new ArgumentNullException(nameof(accessToken)); - } + ArgumentException.ThrowIfNullOrEmpty(accessToken); var existing = (await _deviceManager.GetDevices( new DeviceQuery @@ -1677,7 +1653,7 @@ namespace Emby.Server.Implementations.Session var dtoOptions = _itemInfoDtoOptions; - if (_itemInfoDtoOptions == null) + if (_itemInfoDtoOptions is null) { dtoOptions = new DtoOptions { @@ -1720,7 +1696,7 @@ namespace Emby.Server.Implementations.Session var info = _dtoService.GetBaseItemDto(item, dtoOptions); - if (mediaSource != null) + if (mediaSource is not null) { info.MediaStreams = mediaSource.MediaStreams.ToArray(); } @@ -1744,10 +1720,7 @@ namespace Emby.Server.Implementations.Session /// <inheritdoc /> public void ReportNowViewingItem(string sessionId, string itemId) { - if (string.IsNullOrEmpty(itemId)) - { - throw new ArgumentNullException(nameof(itemId)); - } + ArgumentException.ThrowIfNullOrEmpty(itemId); var item = _libraryManager.GetItemById(new Guid(itemId)); var session = GetSession(sessionId); @@ -1761,7 +1734,7 @@ namespace Emby.Server.Implementations.Session var session = Sessions.FirstOrDefault(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase)); - if (session != null) + if (session is not null) { session.TranscodingInfo = info; } diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs index c654828b1..4e427b1a4 100644 --- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs +++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Collections.Generic; using System.Linq; @@ -58,7 +56,7 @@ namespace Emby.Server.Implementations.Session /// <summary> /// The KeepAlive cancellation token. /// </summary> - private CancellationTokenSource _keepAliveCancellationToken; + private CancellationTokenSource? _keepAliveCancellationToken; /// <summary> /// Initializes a new instance of the <see cref="SessionWebSocketListener" /> class. @@ -94,7 +92,7 @@ namespace Emby.Server.Implementations.Session public async Task ProcessWebSocketConnectedAsync(IWebSocketConnection connection, HttpContext httpContext) { var session = await GetSession(httpContext, connection.RemoteEndPoint?.ToString()).ConfigureAwait(false); - if (session != null) + if (session is not null) { EnsureController(session, connection); await KeepAliveWebSocket(connection).ConfigureAwait(false); @@ -105,7 +103,7 @@ namespace Emby.Server.Implementations.Session } } - private async Task<SessionInfo> GetSession(HttpContext httpContext, string remoteEndpoint) + private async Task<SessionInfo?> GetSession(HttpContext httpContext, string? remoteEndpoint) { if (!httpContext.User.Identity?.IsAuthenticated ?? false) { @@ -138,8 +136,13 @@ namespace Emby.Server.Implementations.Session /// </summary> /// <param name="sender">The WebSocket.</param> /// <param name="e">The event arguments.</param> - private void OnWebSocketClosed(object sender, EventArgs e) + private void OnWebSocketClosed(object? sender, EventArgs e) { + if (sender is null) + { + return; + } + var webSocket = (IWebSocketConnection)sender; _logger.LogDebug("WebSocket {0} is closed.", webSocket); RemoveWebSocket(webSocket); @@ -202,7 +205,7 @@ namespace Emby.Server.Implementations.Session { lock (_keepAliveLock) { - if (_keepAliveCancellationToken == null) + if (_keepAliveCancellationToken is null) { _keepAliveCancellationToken = new CancellationTokenSource(); // Start KeepAlive watcher @@ -221,7 +224,7 @@ namespace Emby.Server.Implementations.Session { lock (_keepAliveLock) { - if (_keepAliveCancellationToken != null) + if (_keepAliveCancellationToken is not null) { _keepAliveCancellationToken.Cancel(); _keepAliveCancellationToken.Dispose(); diff --git a/Emby.Server.Implementations/Session/WebSocketController.cs b/Emby.Server.Implementations/Session/WebSocketController.cs index 1f3248f07..cdc736950 100644 --- a/Emby.Server.Implementations/Session/WebSocketController.cs +++ b/Emby.Server.Implementations/Session/WebSocketController.cs @@ -69,11 +69,9 @@ namespace Emby.Server.Implementations.Session T data, CancellationToken cancellationToken) { - var socket = GetActiveSockets() - .OrderByDescending(i => i.LastActivityDate) - .FirstOrDefault(); + var socket = GetActiveSockets().MaxBy(i => i.LastActivityDate); - if (socket == null) + if (socket is null) { return Task.CompletedTask; } diff --git a/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs b/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs index 2d21bd9c2..964004ecc 100644 --- a/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs +++ b/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs @@ -31,9 +31,9 @@ namespace Emby.Server.Implementations.Sorting var episode1 = x as Episode; var episode2 = y as Episode; - if (episode1 == null) + if (episode1 is null) { - if (episode2 == null) + if (episode2 is null) { return 0; } @@ -41,7 +41,7 @@ namespace Emby.Server.Implementations.Sorting return 1; } - if (episode2 == null) + if (episode2 is null) { return -1; } diff --git a/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs b/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs index ec818253b..453d817c7 100644 --- a/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs +++ b/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs @@ -58,7 +58,7 @@ namespace Emby.Server.Implementations.Sorting { var userdata = UserDataRepository.GetUserData(User, x); - if (userdata != null && userdata.LastPlayedDate.HasValue) + if (userdata is not null && userdata.LastPlayedDate.HasValue) { return userdata.LastPlayedDate.Value; } diff --git a/Emby.Server.Implementations/Sorting/PlayCountComparer.cs b/Emby.Server.Implementations/Sorting/PlayCountComparer.cs index 45c9044c5..16f1b79b3 100644 --- a/Emby.Server.Implementations/Sorting/PlayCountComparer.cs +++ b/Emby.Server.Implementations/Sorting/PlayCountComparer.cs @@ -57,7 +57,7 @@ namespace Emby.Server.Implementations.Sorting { var userdata = UserDataRepository.GetUserData(User, x); - return userdata == null ? 0 : userdata.PlayCount; + return userdata is null ? 0 : userdata.PlayCount; } } } diff --git a/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs b/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs index b217556ef..db86b8002 100644 --- a/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs +++ b/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs @@ -34,7 +34,7 @@ namespace Emby.Server.Implementations.Sorting /// <returns>DateTime.</returns> private static DateTime GetDate(BaseItem? x) { - if (x == null) + if (x is null) { return DateTime.MinValue; } diff --git a/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs b/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs index d2022df7a..7fd1e024d 100644 --- a/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs +++ b/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs @@ -33,7 +33,7 @@ namespace Emby.Server.Implementations.Sorting /// <returns>DateTime.</returns> private static int GetValue(BaseItem? x) { - if (x == null) + if (x is null) { return 0; } diff --git a/Emby.Server.Implementations/Sorting/RuntimeComparer.cs b/Emby.Server.Implementations/Sorting/RuntimeComparer.cs index 646bafbb5..753e58324 100644 --- a/Emby.Server.Implementations/Sorting/RuntimeComparer.cs +++ b/Emby.Server.Implementations/Sorting/RuntimeComparer.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; @@ -24,10 +22,9 @@ namespace Emby.Server.Implementations.Sorting /// <param name="x">The x.</param> /// <param name="y">The y.</param> /// <returns>System.Int32.</returns> - public int Compare(BaseItem x, BaseItem y) + public int Compare(BaseItem? x, BaseItem? y) { ArgumentNullException.ThrowIfNull(x); - ArgumentNullException.ThrowIfNull(y); return (x.RunTimeTicks ?? 0).CompareTo(y.RunTimeTicks ?? 0); diff --git a/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs index 0bd9600b9..5b6c64f63 100644 --- a/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs +++ b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -23,15 +21,14 @@ namespace Emby.Server.Implementations.Sorting /// <param name="x">The x.</param> /// <param name="y">The y.</param> /// <returns>System.Int32.</returns> - public int Compare(BaseItem x, BaseItem y) + public int Compare(BaseItem? x, BaseItem? y) { return string.Compare(GetValue(x), GetValue(y), StringComparison.OrdinalIgnoreCase); } - private static string GetValue(BaseItem item) + private static string? GetValue(BaseItem? item) { var hasSeries = item as IHasSeries; - return hasSeries?.FindSeriesSortName(); } } diff --git a/Emby.Server.Implementations/Sorting/SortNameComparer.cs b/Emby.Server.Implementations/Sorting/SortNameComparer.cs index 628b9b3dd..19abafe19 100644 --- a/Emby.Server.Implementations/Sorting/SortNameComparer.cs +++ b/Emby.Server.Implementations/Sorting/SortNameComparer.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; @@ -24,10 +22,9 @@ namespace Emby.Server.Implementations.Sorting /// <param name="x">The x.</param> /// <param name="y">The y.</param> /// <returns>System.Int32.</returns> - public int Compare(BaseItem x, BaseItem y) + public int Compare(BaseItem? x, BaseItem? y) { ArgumentNullException.ThrowIfNull(x); - ArgumentNullException.ThrowIfNull(y); return string.Compare(x.SortName, y.SortName, StringComparison.OrdinalIgnoreCase); diff --git a/Emby.Server.Implementations/Sorting/StartDateComparer.cs b/Emby.Server.Implementations/Sorting/StartDateComparer.cs index c3df7c47e..2759d20de 100644 --- a/Emby.Server.Implementations/Sorting/StartDateComparer.cs +++ b/Emby.Server.Implementations/Sorting/StartDateComparer.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -24,7 +22,7 @@ namespace Emby.Server.Implementations.Sorting /// <param name="x">The x.</param> /// <param name="y">The y.</param> /// <returns>System.Int32.</returns> - public int Compare(BaseItem x, BaseItem y) + public int Compare(BaseItem? x, BaseItem? y) { return GetDate(x).CompareTo(GetDate(y)); } @@ -34,7 +32,7 @@ namespace Emby.Server.Implementations.Sorting /// </summary> /// <param name="x">The x.</param> /// <returns>DateTime.</returns> - private static DateTime GetDate(BaseItem x) + private static DateTime GetDate(BaseItem? x) { if (x is LiveTvProgram hasStartDate) { diff --git a/Emby.Server.Implementations/Sorting/StudioComparer.cs b/Emby.Server.Implementations/Sorting/StudioComparer.cs index 457c06271..89d10f3d2 100644 --- a/Emby.Server.Implementations/Sorting/StudioComparer.cs +++ b/Emby.Server.Implementations/Sorting/StudioComparer.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -24,10 +22,9 @@ namespace Emby.Server.Implementations.Sorting /// <param name="x">The x.</param> /// <param name="y">The y.</param> /// <returns>System.Int32.</returns> - public int Compare(BaseItem x, BaseItem y) + public int Compare(BaseItem? x, BaseItem? y) { ArgumentNullException.ThrowIfNull(x); - ArgumentNullException.ThrowIfNull(y); return AlphanumericComparator.CompareValues(x.Studios.FirstOrDefault(), y.Studios.FirstOrDefault()); diff --git a/Emby.Server.Implementations/SyncPlay/Group.cs b/Emby.Server.Implementations/SyncPlay/Group.cs index 52becfec6..da8f94932 100644 --- a/Emby.Server.Implementations/SyncPlay/Group.cs +++ b/Emby.Server.Implementations/SyncPlay/Group.cs @@ -197,7 +197,7 @@ namespace Emby.Server.Implementations.SyncPlay private bool HasAccessToQueue(User user, IReadOnlyList<Guid> queue) { // Check if queue is empty. - if (queue == null || queue.Count == 0) + if (queue is null || queue.Count == 0) { return true; } @@ -217,7 +217,7 @@ namespace Emby.Server.Implementations.SyncPlay private bool AllUsersHaveAccessToQueue(IReadOnlyList<Guid> queue) { // Check if queue is empty. - if (queue == null || queue.Count == 0) + if (queue is null || queue.Count == 0) { return true; } @@ -251,7 +251,7 @@ namespace Emby.Server.Implementations.SyncPlay GroupName = request.GroupName; AddSession(session); - var sessionIsPlayingAnItem = session.FullNowPlayingItem != null; + var sessionIsPlayingAnItem = session.FullNowPlayingItem is not null; RestartCurrentItem(); @@ -620,10 +620,8 @@ namespace Emby.Server.Implementations.SyncPlay RestartCurrentItem(); return true; } - else - { - return false; - } + + return false; } /// <inheritdoc /> @@ -637,10 +635,8 @@ namespace Emby.Server.Implementations.SyncPlay RestartCurrentItem(); return true; } - else - { - return false; - } + + return false; } /// <inheritdoc /> diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs index 53e3b3577..00c655634 100644 --- a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs +++ b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs @@ -102,12 +102,12 @@ namespace Emby.Server.Implementations.SyncPlay /// <inheritdoc /> public void NewGroup(SessionInfo session, NewGroupRequest request, CancellationToken cancellationToken) { - if (session == null) + if (session is null) { throw new InvalidOperationException("Session is null!"); } - if (request == null) + if (request is null) { throw new InvalidOperationException("Request is null!"); } @@ -138,12 +138,12 @@ namespace Emby.Server.Implementations.SyncPlay /// <inheritdoc /> public void JoinGroup(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken) { - if (session == null) + if (session is null) { throw new InvalidOperationException("Session is null!"); } - if (request == null) + if (request is null) { throw new InvalidOperationException("Request is null!"); } @@ -155,7 +155,7 @@ namespace Emby.Server.Implementations.SyncPlay { _groups.TryGetValue(request.GroupId, out Group group); - if (group == null) + if (group is null) { _logger.LogWarning("Session {SessionId} tried to join group {GroupId} that does not exist.", session.Id, request.GroupId); @@ -204,12 +204,12 @@ namespace Emby.Server.Implementations.SyncPlay /// <inheritdoc /> public void LeaveGroup(SessionInfo session, LeaveGroupRequest request, CancellationToken cancellationToken) { - if (session == null) + if (session is null) { throw new InvalidOperationException("Session is null!"); } - if (request == null) + if (request is null) { throw new InvalidOperationException("Request is null!"); } @@ -257,12 +257,12 @@ namespace Emby.Server.Implementations.SyncPlay /// <inheritdoc /> public List<GroupInfoDto> ListGroups(SessionInfo session, ListGroupsRequest request) { - if (session == null) + if (session is null) { throw new InvalidOperationException("Session is null!"); } - if (request == null) + if (request is null) { throw new InvalidOperationException("Request is null!"); } @@ -291,12 +291,12 @@ namespace Emby.Server.Implementations.SyncPlay /// <inheritdoc /> public void HandleRequest(SessionInfo session, IGroupPlaybackRequest request, CancellationToken cancellationToken) { - if (session == null) + if (session is null) { throw new InvalidOperationException("Session is null!"); } - if (request == null) + if (request is null) { throw new InvalidOperationException("Request is null!"); } @@ -339,10 +339,8 @@ namespace Emby.Server.Implementations.SyncPlay { return sessionsCounter > 0; } - else - { - return false; - } + + return false; } /// <summary> diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs index 6005896ad..f0e173f0b 100644 --- a/Emby.Server.Implementations/TV/TVSeriesManager.cs +++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -37,12 +35,12 @@ namespace Emby.Server.Implementations.TV { var user = _userManager.GetUserById(query.UserId); - if (user == null) + if (user is null) { throw new ArgumentException("User not found"); } - string presentationUniqueKey = null; + string? presentationUniqueKey = null; if (query.SeriesId.HasValue && !query.SeriesId.Value.Equals(default)) { if (_libraryManager.GetItemById(query.SeriesId.Value) is Series series) @@ -62,7 +60,7 @@ namespace Emby.Server.Implementations.TV { var parent = _libraryManager.GetItemById(query.ParentId.Value); - if (parent != null) + if (parent is not null) { parents = new[] { parent }; } @@ -86,12 +84,12 @@ namespace Emby.Server.Implementations.TV { var user = _userManager.GetUserById(request.UserId); - if (user == null) + if (user is null) { throw new ArgumentException("User not found"); } - string presentationUniqueKey = null; + string? presentationUniqueKey = null; int? limit = null; if (request.SeriesId.HasValue && !request.SeriesId.Value.Equals(default)) { @@ -168,7 +166,7 @@ namespace Emby.Server.Implementations.TV return !anyFound && i.LastWatchedDate == DateTime.MinValue; }) .Select(i => i.GetEpisodeFunction()) - .Where(i => i != null); + .Where(i => i is not null)!; } private static string GetUniqueSeriesKey(Episode episode) @@ -185,14 +183,13 @@ namespace Emby.Server.Implementations.TV /// Gets the next up. /// </summary> /// <returns>Task{Episode}.</returns> - private (DateTime LastWatchedDate, Func<Episode> GetEpisodeFunction) GetNextUp(string seriesKey, User user, DtoOptions dtoOptions, bool rewatching) + private (DateTime LastWatchedDate, Func<Episode?> GetEpisodeFunction) GetNextUp(string seriesKey, User user, DtoOptions dtoOptions, bool rewatching) { var lastQuery = new InternalItemsQuery(user) { AncestorWithPresentationUniqueKey = null, SeriesPresentationUniqueKey = seriesKey, IncludeItemTypes = new[] { BaseItemKind.Episode }, - OrderBy = new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) }, IsPlayed = true, Limit = 1, ParentIndexNumberNotEquals = 0, @@ -203,15 +200,14 @@ namespace Emby.Server.Implementations.TV } }; - if (rewatching) - { - // find last watched by date played, not by newest episode watched - lastQuery.OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) }; - } + // If rewatching is enabled, sort first by date played and then by season and episode numbers + lastQuery.OrderBy = rewatching + ? new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) } + : new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) }; var lastWatchedEpisode = _libraryManager.GetItemList(lastQuery).Cast<Episode>().FirstOrDefault(); - Episode GetEpisode() + Episode? GetEpisode() { var nextQuery = new InternalItemsQuery(user) { @@ -226,18 +222,16 @@ namespace Emby.Server.Implementations.TV DtoOptions = dtoOptions }; - Episode nextEpisode; - if (rewatching) + // Locate the next up episode based on the last watched episode's season and episode number + var lastWatchedParentIndexNumber = lastWatchedEpisode?.ParentIndexNumber; + var lastWatchedIndexNumber = lastWatchedEpisode?.IndexNumberEnd ?? lastWatchedEpisode?.IndexNumber; + if (lastWatchedParentIndexNumber.HasValue && lastWatchedIndexNumber.HasValue) { - nextQuery.Limit = 2; - // get watched episode after most recently watched - nextEpisode = _libraryManager.GetItemList(nextQuery).Cast<Episode>().ElementAtOrDefault(1); - } - else - { - nextEpisode = _libraryManager.GetItemList(nextQuery).Cast<Episode>().FirstOrDefault(); + nextQuery.MinParentAndIndexNumber = (lastWatchedParentIndexNumber.Value, lastWatchedIndexNumber.Value + 1); } + var nextEpisode = _libraryManager.GetItemList(nextQuery).Cast<Episode>().FirstOrDefault(); + if (_configurationManager.Configuration.DisplaySpecialsWithinSeasons) { var consideredEpisodes = _libraryManager.GetItemList(new InternalItemsQuery(user) @@ -251,23 +245,23 @@ namespace Emby.Server.Implementations.TV DtoOptions = dtoOptions }) .Cast<Episode>() - .Where(episode => episode.AirsBeforeSeasonNumber != null || episode.AirsAfterSeasonNumber != null) + .Where(episode => episode.AirsBeforeSeasonNumber is not null || episode.AirsAfterSeasonNumber is not null) .ToList(); - if (lastWatchedEpisode != null) + if (lastWatchedEpisode is not null) { // Last watched episode is added, because there could be specials that aired before the last watched episode consideredEpisodes.Add(lastWatchedEpisode); } - if (nextEpisode != null) + if (nextEpisode is not null) { consideredEpisodes.Add(nextEpisode); } var sortedConsideredEpisodes = _libraryManager.Sort(consideredEpisodes, user, new[] { (ItemSortBy.AiredEpisodeOrder, SortOrder.Ascending) }) .Cast<Episode>(); - if (lastWatchedEpisode != null) + if (lastWatchedEpisode is not null) { sortedConsideredEpisodes = sortedConsideredEpisodes.SkipWhile(episode => !episode.Id.Equals(lastWatchedEpisode.Id)).Skip(1); } @@ -275,7 +269,7 @@ namespace Emby.Server.Implementations.TV nextEpisode = sortedConsideredEpisodes.FirstOrDefault(); } - if (nextEpisode != null) + if (nextEpisode is not null) { var userData = _userDataManager.GetUserData(user, nextEpisode); @@ -288,7 +282,7 @@ namespace Emby.Server.Implementations.TV return nextEpisode; } - if (lastWatchedEpisode != null) + if (lastWatchedEpisode is not null) { var userData = _userDataManager.GetUserData(user, lastWatchedEpisode); diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index 550f0e075..5e897833e 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -102,7 +102,7 @@ namespace Emby.Server.Implementations.Updates PackageInfo[]? packages = await _httpClientFactory.CreateClient(NamedClient.Default) .GetFromJsonAsync<PackageInfo[]>(new Uri(manifest), _jsonSerializerOptions, cancellationToken).ConfigureAwait(false); - if (packages == null) + if (packages is null) { return Array.Empty<PackageInfo>(); } @@ -168,7 +168,7 @@ namespace Emby.Server.Implementations.Updates var result = new List<PackageInfo>(); foreach (RepositoryInfo repository in _config.Configuration.PluginRepositories) { - if (repository.Enabled && repository.Url != null) + if (repository.Enabled && repository.Url is not null) { // Where repositories have the same content, the details from the first is taken. foreach (var package in await GetPackages(repository.Name ?? "Unnamed Repo", repository.Url, true, cancellationToken).ConfigureAwait(true)) @@ -181,7 +181,7 @@ namespace Emby.Server.Implementations.Updates var version = package.Versions[i]; var plugin = _pluginManager.GetPlugin(package.Id, version.VersionNumber); - if (plugin != null) + if (plugin is not null) { await _pluginManager.GenerateManifest(package, version.VersionNumber, plugin.Path, plugin.Manifest.Status).ConfigureAwait(false); } @@ -199,7 +199,7 @@ namespace Emby.Server.Implementations.Updates continue; } - if (existing != null) + if (existing is not null) { // Assumption is both lists are ordered, so slot these into the correct place. MergeSortedList(existing.Versions, package.Versions); @@ -222,7 +222,7 @@ namespace Emby.Server.Implementations.Updates Guid id = default, Version? specificVersion = null) { - if (name != null) + if (name is not null) { availablePackages = availablePackages.Where(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); } @@ -232,7 +232,7 @@ namespace Emby.Server.Implementations.Updates availablePackages = availablePackages.Where(x => x.Id.Equals(id)); } - if (specificVersion != null) + if (specificVersion is not null) { availablePackages = availablePackages.Where(x => x.Versions.Any(y => y.VersionNumber.Equals(specificVersion))); } @@ -251,7 +251,7 @@ namespace Emby.Server.Implementations.Updates var package = FilterPackages(availablePackages, name, id, specificVersion).FirstOrDefault(); // Package not found in repository - if (package == null) + if (package is null) { yield break; } @@ -260,11 +260,11 @@ namespace Emby.Server.Implementations.Updates var availableVersions = package.Versions .Where(x => string.IsNullOrEmpty(x.TargetAbi) || Version.Parse(x.TargetAbi) <= appVer); - if (specificVersion != null) + if (specificVersion is not null) { availableVersions = availableVersions.Where(x => x.VersionNumber.Equals(specificVersion)); } - else if (minVersion != null) + else if (minVersion is not null) { availableVersions = availableVersions.Where(x => x.VersionNumber >= minVersion); } @@ -370,7 +370,7 @@ namespace Emby.Server.Implementations.Updates /// <param name="plugin">The <see cref="LocalPlugin"/> to uninstall.</param> public void UninstallPlugin(LocalPlugin plugin) { - if (plugin == null) + if (plugin is null) { return; } @@ -495,7 +495,7 @@ namespace Emby.Server.Implementations.Updates var compatibleVersions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, minVersion: plugin.Version); var version = compatibleVersions.FirstOrDefault(y => y.Version > plugin.Version); - if (version != null && CompletedInstallations.All(x => !x.Id.Equals(version.Id))) + if (version is not null && CompletedInstallations.All(x => !x.Id.Equals(version.Id))) { yield return version; } @@ -565,9 +565,9 @@ namespace Emby.Server.Implementations.Updates ?? _pluginManager.Plugins.FirstOrDefault(p => p.Name.Equals(package.Name, StringComparison.OrdinalIgnoreCase) && p.Version.Equals(package.Version)); await PerformPackageInstallation(package, plugin?.Manifest.Status ?? PluginStatus.Active, cancellationToken).ConfigureAwait(false); - _logger.LogInformation("Plugin {Action}: {PluginName} {PluginVersion}", plugin == null ? "installed" : "updated", package.Name, package.Version); + _logger.LogInformation("Plugin {Action}: {PluginName} {PluginVersion}", plugin is null ? "installed" : "updated", package.Name, package.Version); - return plugin != null; + return plugin is not null; } } } |
