aboutsummaryrefslogtreecommitdiff
path: root/Emby.Server.Implementations
diff options
context:
space:
mode:
authorRonan Charles-Lorel <roro.roronoa@gmail.com>2023-06-29 15:08:52 +0200
committerGitHub <noreply@github.com>2023-06-29 15:08:52 +0200
commite108183b138552013bbfd13c36937481228eb9e6 (patch)
tree8e374adf35d64b157ac88e5b84a25d186bd4ccf1 /Emby.Server.Implementations
parent31ac861b8560547c7e0c46513077abf76e6bc618 (diff)
parentb5bbb98175e0542d43c01f80c15e8dce04e58b53 (diff)
Merge branch 'jellyfin:master' into master
Diffstat (limited to 'Emby.Server.Implementations')
-rw-r--r--Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs52
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs97
-rw-r--r--Emby.Server.Implementations/Channels/ChannelManager.cs39
-rw-r--r--Emby.Server.Implementations/Collections/CollectionManager.cs21
-rw-r--r--Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs4
-rw-r--r--Emby.Server.Implementations/ConfigurationOptions.cs7
-rw-r--r--Emby.Server.Implementations/Data/BaseSqliteRepository.cs125
-rw-r--r--Emby.Server.Implementations/Data/ConnectionPool.cs79
-rw-r--r--Emby.Server.Implementations/Data/ManagedConnection.cs13
-rw-r--r--Emby.Server.Implementations/Data/SqliteItemRepository.cs487
-rw-r--r--Emby.Server.Implementations/Data/SqliteUserDataRepository.cs38
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs37
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj30
-rw-r--r--Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs41
-rw-r--r--Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs13
-rw-r--r--Emby.Server.Implementations/HttpServer/WebSocketConnection.cs16
-rw-r--r--Emby.Server.Implementations/IO/ExtendedFileSystemInfo.cs13
-rw-r--r--Emby.Server.Implementations/IO/ManagedFileSystem.cs46
-rw-r--r--Emby.Server.Implementations/Images/BaseFolderImageProvider.cs2
-rw-r--r--Emby.Server.Implementations/Images/FolderImageProvider.cs2
-rw-r--r--Emby.Server.Implementations/Images/GenreImageProvider.cs2
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs62
-rw-r--r--Emby.Server.Implementations/Library/MediaSourceManager.cs4
-rw-r--r--Emby.Server.Implementations/Library/PathExtensions.cs98
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs6
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs7
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs15
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs26
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs17
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs6
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs22
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs18
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs11
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs6
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs26
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs6
-rw-r--r--Emby.Server.Implementations/Library/UserViewManager.cs26
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs17
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs25
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs23
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvManager.cs32
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs4
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs3
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs21
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs47
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs47
-rw-r--r--Emby.Server.Implementations/Localization/Core/be.json127
-rw-r--r--Emby.Server.Implementations/Localization/Core/bn.json38
-rw-r--r--Emby.Server.Implementations/Localization/Core/ca.json84
-rw-r--r--Emby.Server.Implementations/Localization/Core/cy.json5
-rw-r--r--Emby.Server.Implementations/Localization/Core/da.json104
-rw-r--r--Emby.Server.Implementations/Localization/Core/es-AR.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/es.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/es_419.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/fa.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/fi.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/fil.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/gsw.json12
-rw-r--r--Emby.Server.Implementations/Localization/Core/hi.json58
-rw-r--r--Emby.Server.Implementations/Localization/Core/id.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/is.json11
-rw-r--r--Emby.Server.Implementations/Localization/Core/ja.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/lt-LT.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/lv.json7
-rw-r--r--Emby.Server.Implementations/Localization/Core/lzh.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/ml.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/mr.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/ms.json21
-rw-r--r--Emby.Server.Implementations/Localization/Core/ne.json16
-rw-r--r--Emby.Server.Implementations/Localization/Core/nl.json18
-rw-r--r--Emby.Server.Implementations/Localization/Core/or.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/pa.json42
-rw-r--r--Emby.Server.Implementations/Localization/Core/pr.json7
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/ru.json10
-rw-r--r--Emby.Server.Implementations/Localization/Core/sl-SI.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/sn.json28
-rw-r--r--Emby.Server.Implementations/Localization/Core/sv.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/te.json21
-rw-r--r--Emby.Server.Implementations/Localization/Core/tr.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/uk.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/ur_PK.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-CN.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-HK.json110
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-TW.json6
-rw-r--r--Emby.Server.Implementations/Localization/LocalizationManager.cs144
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/0-prefer.csv11
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/au.csv20
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/be.csv17
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/br.csv14
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/ca.csv26
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/co.csv15
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/de.csv22
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/dk.csv11
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/es.csv30
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/fi.csv20
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/fr.csv17
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/gb.csv29
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/ie.csv15
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/jp.csv15
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/kz.csv13
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/mx.csv12
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/nl.csv14
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/no.csv15
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/nz.csv26
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/ro.csv7
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/ru.csv11
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/se.csv15
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/uk.csv29
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/us.csv73
-rw-r--r--Emby.Server.Implementations/MediaEncoder/EncodingManager.cs24
-rw-r--r--Emby.Server.Implementations/Playlists/PlaylistManager.cs57
-rw-r--r--Emby.Server.Implementations/Playlists/PlaylistsFolder.cs8
-rw-r--r--Emby.Server.Implementations/Plugins/PluginManager.cs202
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs9
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/TaskManager.cs13
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs1
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs119
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs4
-rw-r--r--Emby.Server.Implementations/Session/SessionWebSocketListener.cs13
-rw-r--r--Emby.Server.Implementations/Session/WebSocketController.cs8
-rw-r--r--Emby.Server.Implementations/Sorting/RuntimeComparer.cs5
-rw-r--r--Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs7
-rw-r--r--Emby.Server.Implementations/Sorting/SortNameComparer.cs5
-rw-r--r--Emby.Server.Implementations/Sorting/StartDateComparer.cs6
-rw-r--r--Emby.Server.Implementations/Sorting/StudioComparer.cs5
-rw-r--r--Emby.Server.Implementations/SyncPlay/Group.cs12
-rw-r--r--Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs6
-rw-r--r--Emby.Server.Implementations/TV/TVSeriesManager.cs12
-rw-r--r--Emby.Server.Implementations/Updates/InstallationManager.cs7
131 files changed, 2278 insertions, 1401 deletions
diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
index 985a127d5..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 is not null;
}
}
@@ -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)
{
@@ -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/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 07b0807b7..7969577bc 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -11,7 +11,6 @@ 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;
@@ -81,11 +80,13 @@ using MediaBrowser.Controller.Subtitles;
using MediaBrowser.Controller.SyncPlay;
using MediaBrowser.Controller.TV;
using MediaBrowser.LocalMetadata.Savers;
+using MediaBrowser.MediaEncoding.BdInfo;
using MediaBrowser.MediaEncoding.Subtitles;
using MediaBrowser.Model.Cryptography;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
+using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.System;
@@ -114,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;
@@ -130,7 +127,6 @@ namespace Emby.Server.Implementations
private readonly IPluginManager _pluginManager;
private List<Type> _creatingInstances;
- private IMediaEncoder _mediaEncoder;
private ISessionManager _sessionManager;
/// <summary>
@@ -139,8 +135,6 @@ namespace Emby.Server.Implementations
/// <value>All concrete types.</value>
private Type[] _allConcreteTypes;
- private DeviceId _deviceId;
-
private bool _disposed = false;
/// <summary>
@@ -164,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,23 +186,9 @@ namespace Emby.Server.Implementations
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.
@@ -284,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;
@@ -445,7 +418,7 @@ namespace Emby.Server.Implementations
ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
ConfigurationManager.NamedConfigurationUpdated += OnConfigurationUpdated;
- _mediaEncoder.SetFFmpegPath();
+ Resolve<IMediaEncoder>().SetFFmpegPath();
Logger.LogInformation("ServerId: {ServerId}", SystemId);
@@ -558,6 +531,8 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
+ serviceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
+
serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
@@ -652,50 +627,19 @@ namespace Emby.Server.Implementations
}
}
+ ((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}", RuntimeInformation.OSDescription);
- 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,10 +726,6 @@ 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>());
}
@@ -1248,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 85ccbc028..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);
@@ -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)
{
@@ -229,6 +226,7 @@ namespace Emby.Server.Implementations.Channels
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
{
@@ -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)
@@ -404,7 +405,7 @@ namespace Emby.Server.Implementations.Channels
}
else
{
- results = new List<MediaSourceInfo>();
+ results = Enumerable.Empty<MediaSourceInfo>();
}
return results
diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs
index b53c8ca51..b34d0f21e 100644
--- a/Emby.Server.Implementations/Collections/CollectionManager.cs
+++ b/Emby.Server.Implementations/Collections/CollectionManager.cs
@@ -112,7 +112,8 @@ namespace Emby.Server.Implementations.Collections
return Path.Combine(_appPaths.DataPath, "collections");
}
- private Task<Folder?> GetCollectionsFolder(bool createIfNeeded)
+ /// <inheritdoc />
+ public Task<Folder?> GetCollectionsFolder(bool createIfNeeded)
{
return EnsureLibraryFolder(GetCollectionsFolderPath(), createIfNeeded);
}
@@ -206,8 +207,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();
@@ -223,18 +223,23 @@ namespace Emby.Server.Implementations.Collections
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)
{
- LinkedChild[] newChildren = new LinkedChild[collection.LinkedChildren.Length + list.Count];
+ var originalLen = collection.LinkedChildren.Length;
+ var newItemCount = itemList.Count;
+ LinkedChild[] newChildren = new LinkedChild[originalLen + newItemCount];
collection.LinkedChildren.CopyTo(newChildren, 0);
- list.CopyTo(newChildren, collection.LinkedChildren.Length);
+ for (int i = 0; i < newItemCount; i++)
+ {
+ newChildren[originalLen + i] = LinkedChild.Create(itemList[i]);
+ }
+
collection.LinkedChildren = newChildren;
collection.UpdateRatingToItems(linkedChildrenList);
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 f0a4c8ffb..f0c267627 100644
--- a/Emby.Server.Implementations/ConfigurationOptions.cs
+++ b/Emby.Server.Implementations/ConfigurationOptions.cs
@@ -11,14 +11,15 @@ 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()
{
{ HostWebClientKey, bool.TrueString },
- { DefaultRedirectKey, "web/index.html" },
+ { DefaultRedirectKey, "web/" },
{ FfmpegProbeSizeKey, "1G" },
{ FfmpegAnalyzeDurationKey, "200M" },
{ PlaylistsAllowDuplicatesKey, bool.FalseString },
- { BindToUnixSocketKey, bool.FalseString }
+ { BindToUnixSocketKey, bool.FalseString },
+ { SqliteCacheSizeKey, "20000" }
};
}
}
diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
index 1d61667f8..d05534ee7 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>
@@ -63,7 +72,7 @@ namespace Emby.Server.Implementations.Data
/// <summary>
/// Gets the locking mode. <see href="https://www.sqlite.org/pragma.html#pragma_locking_mode" />.
/// </summary>
- protected virtual string LockingMode => "EXCLUSIVE";
+ protected virtual string LockingMode => "NORMAL";
/// <summary>
/// Gets the journal mode. <see href="https://www.sqlite.org/pragma.html#pragma_journal_mode" />.
@@ -73,9 +82,10 @@ namespace Emby.Server.Implementations.Data
/// <summary>
/// Gets the journal size limit. <see href="https://www.sqlite.org/pragma.html#pragma_journal_size_limit" />.
+ /// The default (-1) is overriden to prevent unconstrained WAL size, as reported by users.
/// </summary>
/// <value>The journal size limit.</value>
- protected virtual int? JournalSizeLimit => 0;
+ protected virtual int? JournalSizeLimit => 134_217_728; // 128MiB
/// <summary>
/// Gets the page size.
@@ -88,7 +98,7 @@ 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.
@@ -101,83 +111,114 @@ namespace Emby.Server.Implementations.Data
/// 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 is not 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);
+ 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=" + (int)JournalSizeLimit.Value);
+ 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(
@@ -252,22 +293,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/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/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index bc703fe90..ca8f605a0 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -25,6 +25,7 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Persistence;
@@ -34,6 +35,7 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.Querying;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using SQLitePCL.pretty;
@@ -49,8 +51,8 @@ namespace Emby.Server.Implementations.Data
private const string SaveItemCommandText =
@"replace into TypedBaseItems
- (guid,type,data,Path,StartDate,EndDate,ChannelId,IsMovie,IsSeries,EpisodeTitle,IsRepeat,CommunityRating,CustomRating,IndexNumber,IsLocked,Name,OfficialRating,MediaType,Overview,ParentIndexNumber,PremiereDate,ProductionYear,ParentId,Genres,InheritedParentalRatingValue,SortName,ForcedSortName,RunTimeTicks,Size,DateCreated,DateModified,PreferredMetadataLanguage,PreferredMetadataCountryCode,Width,Height,DateLastRefreshed,DateLastSaved,IsInMixedFolder,LockedFields,Studios,Audio,ExternalServiceId,Tags,IsFolder,UnratedType,TopParentId,TrailerTypes,CriticRating,CleanName,PresentationUniqueKey,OriginalTitle,PrimaryVersionId,DateLastMediaAdded,Album,IsVirtualItem,SeriesName,UserDataKey,SeasonName,SeasonId,SeriesId,ExternalSeriesId,Tagline,ProviderIds,Images,ProductionLocations,ExtraIds,TotalBitrate,ExtraType,Artists,AlbumArtists,ExternalId,SeriesPresentationUniqueKey,ShowId,OwnerId)
- values (@guid,@type,@data,@Path,@StartDate,@EndDate,@ChannelId,@IsMovie,@IsSeries,@EpisodeTitle,@IsRepeat,@CommunityRating,@CustomRating,@IndexNumber,@IsLocked,@Name,@OfficialRating,@MediaType,@Overview,@ParentIndexNumber,@PremiereDate,@ProductionYear,@ParentId,@Genres,@InheritedParentalRatingValue,@SortName,@ForcedSortName,@RunTimeTicks,@Size,@DateCreated,@DateModified,@PreferredMetadataLanguage,@PreferredMetadataCountryCode,@Width,@Height,@DateLastRefreshed,@DateLastSaved,@IsInMixedFolder,@LockedFields,@Studios,@Audio,@ExternalServiceId,@Tags,@IsFolder,@UnratedType,@TopParentId,@TrailerTypes,@CriticRating,@CleanName,@PresentationUniqueKey,@OriginalTitle,@PrimaryVersionId,@DateLastMediaAdded,@Album,@IsVirtualItem,@SeriesName,@UserDataKey,@SeasonName,@SeasonId,@SeriesId,@ExternalSeriesId,@Tagline,@ProviderIds,@Images,@ProductionLocations,@ExtraIds,@TotalBitrate,@ExtraType,@Artists,@AlbumArtists,@ExternalId,@SeriesPresentationUniqueKey,@ShowId,@OwnerId)";
+ (guid,type,data,Path,StartDate,EndDate,ChannelId,IsMovie,IsSeries,EpisodeTitle,IsRepeat,CommunityRating,CustomRating,IndexNumber,IsLocked,Name,OfficialRating,MediaType,Overview,ParentIndexNumber,PremiereDate,ProductionYear,ParentId,Genres,InheritedParentalRatingValue,SortName,ForcedSortName,RunTimeTicks,Size,DateCreated,DateModified,PreferredMetadataLanguage,PreferredMetadataCountryCode,Width,Height,DateLastRefreshed,DateLastSaved,IsInMixedFolder,LockedFields,Studios,Audio,ExternalServiceId,Tags,IsFolder,UnratedType,TopParentId,TrailerTypes,CriticRating,CleanName,PresentationUniqueKey,OriginalTitle,PrimaryVersionId,DateLastMediaAdded,Album,LUFS,IsVirtualItem,SeriesName,UserDataKey,SeasonName,SeasonId,SeriesId,ExternalSeriesId,Tagline,ProviderIds,Images,ProductionLocations,ExtraIds,TotalBitrate,ExtraType,Artists,AlbumArtists,ExternalId,SeriesPresentationUniqueKey,ShowId,OwnerId)
+ values (@guid,@type,@data,@Path,@StartDate,@EndDate,@ChannelId,@IsMovie,@IsSeries,@EpisodeTitle,@IsRepeat,@CommunityRating,@CustomRating,@IndexNumber,@IsLocked,@Name,@OfficialRating,@MediaType,@Overview,@ParentIndexNumber,@PremiereDate,@ProductionYear,@ParentId,@Genres,@InheritedParentalRatingValue,@SortName,@ForcedSortName,@RunTimeTicks,@Size,@DateCreated,@DateModified,@PreferredMetadataLanguage,@PreferredMetadataCountryCode,@Width,@Height,@DateLastRefreshed,@DateLastSaved,@IsInMixedFolder,@LockedFields,@Studios,@Audio,@ExternalServiceId,@Tags,@IsFolder,@UnratedType,@TopParentId,@TrailerTypes,@CriticRating,@CleanName,@PresentationUniqueKey,@OriginalTitle,@PrimaryVersionId,@DateLastMediaAdded,@Album,@LUFS,@IsVirtualItem,@SeriesName,@UserDataKey,@SeasonName,@SeasonId,@SeriesId,@ExternalSeriesId,@Tagline,@ProviderIds,@Images,@ProductionLocations,@ExtraIds,@TotalBitrate,@ExtraType,@Artists,@AlbumArtists,@ExternalId,@SeriesPresentationUniqueKey,@ShowId,@OwnerId)";
private readonly IServerConfigurationManager _config;
private readonly IServerApplicationHost _appHost;
@@ -110,6 +112,7 @@ namespace Emby.Server.Implementations.Data
"PrimaryVersionId",
"DateLastMediaAdded",
"Album",
+ "LUFS",
"CriticRating",
"IsVirtualItem",
"SeriesName",
@@ -318,13 +321,15 @@ namespace Emby.Server.Implementations.Data
/// <param name="logger">Instance of the <see cref="ILogger{SqliteItemRepository}"/> interface.</param>
/// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
/// <param name="imageProcessor">Instance of the <see cref="IImageProcessor"/> interface.</param>
+ /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
/// <exception cref="ArgumentNullException">config is null.</exception>
public SqliteItemRepository(
IServerConfigurationManager config,
IServerApplicationHost appHost,
ILogger<SqliteItemRepository> logger,
ILocalizationManager localization,
- IImageProcessor imageProcessor)
+ IImageProcessor imageProcessor,
+ IConfiguration configuration)
: base(logger)
{
_config = config;
@@ -336,10 +341,13 @@ namespace Emby.Server.Implementations.Data
_jsonOptions = JsonDefaults.Options;
DbFilePath = Path.Combine(_config.ApplicationPaths.DataPath, "library.db");
+
+ CacheSize = configuration.GetSqliteCacheSize();
+ ReadConnectionsCount = Environment.ProcessorCount * 2;
}
/// <inheritdoc />
- protected override int? CacheSize => 20000;
+ protected override int? CacheSize { get; }
/// <inheritdoc />
protected override TempStoreMode TempStore => TempStoreMode.Memory;
@@ -347,10 +355,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))";
@@ -488,6 +496,7 @@ namespace Emby.Server.Implementations.Data
AddColumn(db, "TypedBaseItems", "PrimaryVersionId", "Text", existingColumnNames);
AddColumn(db, "TypedBaseItems", "DateLastMediaAdded", "DATETIME", existingColumnNames);
AddColumn(db, "TypedBaseItems", "Album", "Text", existingColumnNames);
+ AddColumn(db, "TypedBaseItems", "LUFS", "Float", existingColumnNames);
AddColumn(db, "TypedBaseItems", "IsVirtualItem", "BIT", existingColumnNames);
AddColumn(db, "TypedBaseItems", "SeriesName", "Text", existingColumnNames);
AddColumn(db, "TypedBaseItems", "UserDataKey", "Text", existingColumnNames);
@@ -551,8 +560,6 @@ namespace Emby.Server.Implementations.Data
connection.RunQueries(postQueries);
}
-
- userDataRepo.Initialize(userManager, WriteLock, WriteConnection);
}
public void SaveImages(BaseItem item)
@@ -586,7 +593,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);
@@ -594,9 +601,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;
@@ -606,7 +615,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())
@@ -622,14 +631,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)
@@ -911,6 +914,7 @@ namespace Emby.Server.Implementations.Data
}
saveItemStatement.TryBind("@Album", item.Album);
+ saveItemStatement.TryBind("@LUFS", item.LUFS);
saveItemStatement.TryBind("@IsVirtualItem", item.IsVirtualItem);
if (item is IHasSeries hasSeriesName)
@@ -1195,7 +1199,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)
{
@@ -1284,15 +1288,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());
}
}
@@ -1307,7 +1309,8 @@ namespace Emby.Server.Implementations.Data
{
return false;
}
- else if (type == typeof(UserRootFolder))
+
+ if (type == typeof(UserRootFolder))
{
return false;
}
@@ -1317,55 +1320,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;
}
@@ -1749,6 +1765,11 @@ namespace Emby.Server.Implementations.Data
item.Album = album;
}
+ if (reader.TryGetSingle(index++, out var lUFS))
+ {
+ item.LUFS = lUFS;
+ }
+
if (reader.TryGetSingle(index++, out var criticRating))
{
item.CriticRating = criticRating;
@@ -1956,22 +1977,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 />
@@ -1980,16 +1998,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);
}
}
@@ -2376,7 +2392,7 @@ namespace Emby.Server.Implementations.Data
else
{
builder.Append(
- @"(SELECT CASE WHEN InheritedParentalRatingValue=0
+ @"(SELECT CASE WHEN COALESCE(InheritedParentalRatingValue, 0)=0
THEN 0
ELSE 10.0 / (1.0 + ABS(InheritedParentalRatingValue - @InheritedParentalRatingValue))
END)");
@@ -2390,6 +2406,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)
{
@@ -2841,13 +2858,10 @@ namespace Emby.Server.Implementations.Data
connection.RunInTransaction(
db =>
{
- var itemQueryStatement = PrepareStatement(db, itemQuery);
- var totalRecordCountQueryStatement = PrepareStatement(db, totalRecordCountQuery);
-
if (!isReturningZeroItems)
{
using (new QueryTimeLogger(Logger, itemQuery, "GetItems.ItemQuery"))
- using (var statement = itemQueryStatement)
+ using (var statement = PrepareStatement(db, itemQuery))
{
if (EnableJoinUserData(query))
{
@@ -2882,7 +2896,7 @@ namespace Emby.Server.Implementations.Data
if (query.EnableTotalRecordCount)
{
using (new QueryTimeLogger(Logger, totalRecordCountQuery, "GetItems.TotalRecordCount"))
- using (var statement = totalRecordCountQueryStatement)
+ using (var statement = PrepareStatement(db, totalRecordCountQuery))
{
if (EnableJoinUserData(query))
{
@@ -3202,7 +3216,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)
{
@@ -3677,7 +3692,6 @@ namespace Emby.Server.Implementations.Data
if (statement is not null)
{
nameContains = FixUnicodeChars(nameContains);
-
statement.TryBind("@NameContains", "%" + GetCleanValue(nameContains) + "%");
}
}
@@ -3803,13 +3817,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 is not null)
- {
- statement.TryBind(paramName, artistId);
- }
-
+ statement?.TryBind(paramName, artistId);
index++;
}
@@ -3824,13 +3833,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 is not null)
- {
- statement.TryBind(paramName, artistId);
- }
-
+ statement?.TryBind(paramName, artistId);
index++;
}
@@ -3845,13 +3849,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 is not null)
- {
- statement.TryBind(paramName, artistId);
- }
-
+ statement?.TryBind(paramName, artistId);
index++;
}
@@ -3866,13 +3865,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 is not null)
- {
- statement.TryBind(paramName, albumId);
- }
-
+ statement?.TryBind(paramName, albumId);
index++;
}
@@ -3887,13 +3881,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 is not null)
- {
- statement.TryBind(paramName, artistId);
- }
-
+ statement?.TryBind(paramName, artistId);
index++;
}
@@ -3908,13 +3897,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 is not null)
- {
- statement.TryBind(paramName, genreId);
- }
-
+ statement?.TryBind(paramName, genreId);
index++;
}
@@ -3929,11 +3913,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 is not null)
- {
- statement.TryBind("@Genre" + index, GetCleanValue(item));
- }
-
+ statement?.TryBind("@Genre" + index, GetCleanValue(item));
index++;
}
@@ -3948,11 +3928,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 is not null)
- {
- statement.TryBind("@Tag" + index, GetCleanValue(item));
- }
-
+ statement?.TryBind("@Tag" + index, GetCleanValue(item));
index++;
}
@@ -3967,11 +3943,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 is not null)
- {
- statement.TryBind("@ExcludeTag" + index, GetCleanValue(item));
- }
-
+ statement?.TryBind("@ExcludeTag" + index, GetCleanValue(item));
index++;
}
@@ -3986,14 +3958,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 is not null)
- {
- statement.TryBind(paramName, studioId);
- }
-
+ statement?.TryBind(paramName, studioId);
index++;
}
@@ -4008,11 +3974,7 @@ namespace Emby.Server.Implementations.Data
foreach (var item in query.OfficialRatings)
{
clauses.Add("OfficialRating=@OfficialRating" + index);
- if (statement is not null)
- {
- statement.TryBind("@OfficialRating" + index, item);
- }
-
+ statement?.TryBind("@OfficialRating" + index, item);
index++;
}
@@ -4020,35 +3982,97 @@ 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 is not 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)
+ if (query.MaxParentalRating.HasValue)
+ {
+ ratingClauseBuilder.Append(" AND InheritedParentalRatingValue <= @MaxParentalRating");
+ statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value);
+ }
+ }
+ else if (query.BlockUnratedItems.Length > 0)
{
- whereClauses.Add("InheritedParentalRatingValue<=@MaxParentalRating");
+ 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)
{
- statement.TryBind("@MaxParentalRating", query.MaxParentalRating.Value);
+ for (var ind = 0; ind < query.BlockUnratedItems.Length; ind++)
+ {
+ statement.TryBind(paramName + ind, query.BlockUnratedItems[ind].ToString());
+ }
}
- }
- if (query.HasParentalRating.HasValue)
- {
- if (query.HasParentalRating.Value)
+ 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))
{
- whereClauses.Add("InheritedParentalRatingValue = 0");
+ 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)
+ {
+ 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)
{
@@ -4089,37 +4113,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 is not 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 is not 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 is not 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 is not null)
- {
- statement.TryBind("@HasNoSubtitleTrackWithLanguage", query.HasNoSubtitleTrackWithLanguage);
- }
+ statement?.TryBind("@HasNoSubtitleTrackWithLanguage", query.HasNoSubtitleTrackWithLanguage);
}
if (query.HasSubtitles.HasValue)
@@ -4169,15 +4181,11 @@ namespace Emby.Server.Implementations.Data
if (query.Years.Length == 1)
{
whereClauses.Add("ProductionYear=@Years");
- if (statement is not 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 + ")");
}
@@ -4185,10 +4193,7 @@ namespace Emby.Server.Implementations.Data
if (isVirtualItem.HasValue)
{
whereClauses.Add("IsVirtualItem=@IsVirtualItem");
- if (statement is not null)
- {
- statement.TryBind("@IsVirtualItem", isVirtualItem.Value);
- }
+ statement?.TryBind("@IsVirtualItem", isVirtualItem.Value);
}
if (query.IsSpecialSeason.HasValue)
@@ -4219,31 +4224,22 @@ namespace Emby.Server.Implementations.Data
if (queryMediaTypes.Length == 1)
{
whereClauses.Add("MediaType=@MediaTypes");
- if (statement is not 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 is not null)
- {
- statement.TryBind("@IncludeId" + index, id);
- }
-
+ statement?.TryBind("@IncludeId" + index, id);
index++;
}
@@ -4253,16 +4249,11 @@ 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 is not null)
- {
- statement.TryBind("@ExcludeId" + index, id);
- }
-
+ statement?.TryBind("@ExcludeId" + index, id);
index++;
}
@@ -4283,11 +4274,7 @@ namespace Emby.Server.Implementations.Data
var paramName = "@ExcludeProviderId" + index;
excludeIds.Add("(ProviderIds is null or ProviderIds not like " + paramName + ")");
- if (statement is not null)
- {
- statement.TryBind(paramName, "%" + pair.Key + "=" + pair.Value + "%");
- }
-
+ statement?.TryBind(paramName, "%" + pair.Key + "=" + pair.Value + "%");
index++;
break;
@@ -4312,7 +4299,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:
@@ -4326,11 +4313,7 @@ namespace Emby.Server.Implementations.Data
hasProviderIds.Add("ProviderIds like " + paramName);
// this replaces the placeholder with a value, here: %key=val%
- if (statement is not null)
- {
- statement.TryBind(paramName, "%" + pair.Key + "=" + pair.Value + "%");
- }
-
+ statement?.TryBind(paramName, "%" + pair.Key + "=" + pair.Value + "%");
index++;
break;
@@ -4407,11 +4390,7 @@ namespace Emby.Server.Implementations.Data
if (query.AncestorIds.Length == 1)
{
whereClauses.Add("Guid in (select itemId from AncestorIds where AncestorId=@AncestorId)");
-
- if (statement is not null)
- {
- statement.TryBind("@AncestorId", query.AncestorIds[0]);
- }
+ statement?.TryBind("@AncestorId", query.AncestorIds[0]);
}
if (query.AncestorIds.Length > 1)
@@ -4424,39 +4403,13 @@ 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 is not null)
- {
- statement.TryBind("@AncestorWithPresentationUniqueKey", query.AncestorWithPresentationUniqueKey);
- }
+ statement?.TryBind("@AncestorWithPresentationUniqueKey", query.AncestorWithPresentationUniqueKey);
}
if (!string.IsNullOrWhiteSpace(query.SeriesPresentationUniqueKey))
{
whereClauses.Add("SeriesPresentationUniqueKey=@SeriesPresentationUniqueKey");
-
- if (statement is not null)
- {
- statement.TryBind("@SeriesPresentationUniqueKey", query.SeriesPresentationUniqueKey);
- }
- }
-
- if (query.BlockUnratedItems.Length == 1)
- {
- whereClauses.Add("(InheritedParentalRatingValue > 0 or UnratedType <> @UnratedType)");
- if (statement is not 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)
@@ -4477,6 +4430,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>();
@@ -4587,6 +4558,7 @@ namespace Emby.Server.Implementations.Data
return whereClauses;
}
+#nullable disable
/// <summary>
/// Formats a where clause for the specified provider.
@@ -4793,22 +4765,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)
@@ -4833,23 +4803,20 @@ 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)
@@ -5440,6 +5407,9 @@ AND Type = @InternalPersonType)");
list.AddRange(inheritedTags.Select(i => (6, i)));
+ // Remove all invalid values.
+ list.RemoveAll(i => string.IsNullOrEmpty(i.Item2));
+
return list;
}
@@ -5577,7 +5547,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);
@@ -5606,9 +5576,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))
diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
index 5f2c3c9dc..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 =>
@@ -371,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/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index 5103b1fbf..7a6ed2cb8 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);
}
@@ -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;
}
@@ -571,9 +571,7 @@ namespace Emby.Server.Implementations.Dto
return null;
}
}).Where(i => i is not null)
- .Where(i => user is null ?
- true :
- i.IsVisible(user))
+ .Where(i => user is null || i.IsVisible(user))
.DistinctBy(x => x.Name, StringComparer.OrdinalIgnoreCase)
.ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase);
@@ -908,6 +906,7 @@ namespace Emby.Server.Implementations.Dto
// Add audio info
if (item is Audio audio)
{
+ dto.LUFS = audio.LUFS;
dto.Album = audio.Album;
if (audio.ExtraType.HasValue)
{
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index 1b5c879be..b8655c760 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -22,17 +22,17 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="DiscUtils.Udf" Version="0.16.13" />
- <PackageReference Include="Jellyfin.XmlTv" Version="10.8.0" />
- <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
- <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
- <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
- <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.2" />
- <PackageReference Include="Mono.Nat" Version="3.0.4" />
- <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.4.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>
@@ -53,13 +53,13 @@
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
+ <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 05d0a9b79..be36bbd2c 100644
--- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
@@ -12,6 +12,7 @@ using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Events;
using MediaBrowser.Controller.Channels;
+using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
@@ -26,12 +27,8 @@ namespace Emby.Server.Implementations.EntryPoints
{
public class LibraryChangedNotifier : IServerEntryPoint
{
- /// <summary>
- /// The library update duration.
- /// </summary>
- private const int LibraryUpdateDuration = 30000;
-
private readonly ILibraryManager _libraryManager;
+ private readonly IServerConfigurationManager _configurationManager;
private readonly IProviderManager _providerManager;
private readonly ISessionManager _sessionManager;
private readonly IUserManager _userManager;
@@ -51,12 +48,14 @@ namespace Emby.Server.Implementations.EntryPoints
public LibraryChangedNotifier(
ILibraryManager libraryManager,
+ IServerConfigurationManager configurationManager,
ISessionManager sessionManager,
IUserManager userManager,
ILogger<LibraryChangedNotifier> logger,
IProviderManager providerManager)
{
_libraryManager = libraryManager;
+ _configurationManager = configurationManager;
_sessionManager = sessionManager;
_userManager = userManager;
_logger = logger;
@@ -196,12 +195,12 @@ namespace Emby.Server.Implementations.EntryPoints
LibraryUpdateTimer = new Timer(
LibraryUpdateTimerCallback,
null,
- LibraryUpdateDuration,
- Timeout.Infinite);
+ TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration),
+ Timeout.InfiniteTimeSpan);
}
else
{
- LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite);
+ LibraryUpdateTimer.Change(TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan);
}
if (e.Item.GetParent() is Folder parent)
@@ -229,11 +228,11 @@ namespace Emby.Server.Implementations.EntryPoints
{
if (LibraryUpdateTimer is null)
{
- LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration, Timeout.Infinite);
+ LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan);
}
else
{
- LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite);
+ LibraryUpdateTimer.Change(TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan);
}
_itemsUpdated.Add(e.Item);
@@ -256,11 +255,11 @@ namespace Emby.Server.Implementations.EntryPoints
{
if (LibraryUpdateTimer is null)
{
- LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration, Timeout.Infinite);
+ LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan);
}
else
{
- LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite);
+ LibraryUpdateTimer.Change(TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan);
}
if (e.Parent is Folder parent)
@@ -276,25 +275,31 @@ 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
+ foldersAddedTo = _foldersAddedTo
.DistinctBy(x => x.Id)
.ToList();
- var foldersRemovedFrom = _foldersRemovedFrom
+ foldersRemovedFrom = _foldersRemovedFrom
.DistinctBy(x => x.Id)
.ToList();
- var itemsUpdated = _itemsUpdated
+ itemsUpdated = _itemsUpdated
.Where(i => !_itemsAdded.Contains(i))
.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 is not null)
{
@@ -308,6 +313,8 @@ namespace Emby.Server.Implementations.EntryPoints
_foldersAddedTo.Clear();
_foldersRemovedFrom.Clear();
}
+
+ await SendChangeNotifications(itemsAdded, itemsUpdated, itemsRemoved, foldersAddedTo, foldersRemovedFrom, CancellationToken.None).ConfigureAwait(false);
}
/// <summary>
diff --git a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
index e724618b3..d32759017 100644
--- a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
@@ -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 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);
}
}
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
index b1a99853a..af79c18c4 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
@@ -9,7 +9,7 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Extensions.Json;
using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Net;
+using MediaBrowser.Controller.Net.WebSocketMessages;
using MediaBrowser.Model.Session;
using Microsoft.Extensions.Logging;
@@ -88,6 +88,18 @@ namespace Emby.Server.Implementations.HttpServer
/// <summary>
/// Sends a message asynchronously.
/// </summary>
+ /// <param name="message">The message.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public Task SendAsync(WebSocketMessage message, CancellationToken cancellationToken)
+ {
+ var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions);
+ return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken);
+ }
+
+ /// <summary>
+ /// Sends a message asynchronously.
+ /// </summary>
/// <typeparam name="T">The type of the message.</typeparam>
/// <param name="message">The message.</param>
/// <param name="cancellationToken">The cancellation token.</param>
@@ -224,7 +236,7 @@ namespace Emby.Server.Implementations.HttpServer
{
LastKeepAliveDate = DateTime.UtcNow;
return SendAsync(
- new WebSocketMessage<string>
+ new OutboundWebSocketMessage
{
MessageId = Guid.NewGuid(),
MessageType = SessionMessageType.KeepAlive
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/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
index 7b8c79e8a..60ab668cd 100644
--- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs
+++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
@@ -267,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>
@@ -405,19 +386,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);
}
}
}
@@ -430,19 +410,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)
{
@@ -450,7 +431,7 @@ namespace Emby.Server.Implementations.IO
}
else
{
- attributes = RemoveAttribute(attributes, FileAttributes.ReadOnly);
+ attributes &= ~FileAttributes.ReadOnly;
}
if (isHidden)
@@ -459,17 +440,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>
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/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/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index a3c66dc79..ea45bf0ba 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;
@@ -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))
{
@@ -537,7 +539,7 @@ namespace Emby.Server.Implementations.Library
collectionType = GetContentTypeOverride(fullPath, true);
}
- var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, directoryService)
+ var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, this)
{
Parent = parent,
FileInfo = fileInfo,
@@ -1253,7 +1255,7 @@ namespace Emby.Server.Implementations.Library
var parent = GetItemById(query.ParentId);
if (parent is not null)
{
- SetTopParentIdsOrAncestors(query, new List<BaseItem> { parent });
+ SetTopParentIdsOrAncestors(query, new[] { parent });
}
}
@@ -1262,7 +1264,14 @@ namespace Emby.Server.Implementations.Library
AddUserToQuery(query, query.User, allowExternalContent);
}
- return _itemRepository.GetItemList(query);
+ var itemList = _itemRepository.GetItemList(query);
+ var user = query.User;
+ if (user is not null)
+ {
+ return itemList.Where(i => i.IsVisible(user)).ToList();
+ }
+
+ return itemList;
}
public List<BaseItem> GetItemList(InternalItemsQuery query)
@@ -1277,7 +1286,7 @@ namespace Emby.Server.Implementations.Library
var parent = GetItemById(query.ParentId);
if (parent is not null)
{
- SetTopParentIdsOrAncestors(query, new List<BaseItem> { parent });
+ SetTopParentIdsOrAncestors(query, new[] { parent });
}
}
@@ -1435,7 +1444,7 @@ namespace Emby.Server.Implementations.Library
var parent = GetItemById(query.ParentId);
if (parent is not null)
{
- SetTopParentIdsOrAncestors(query, new List<BaseItem> { parent });
+ SetTopParentIdsOrAncestors(query, new[] { parent });
}
}
@@ -1455,7 +1464,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))
{
@@ -1501,6 +1510,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() };
+ }
}
}
@@ -1602,7 +1617,7 @@ namespace Emby.Server.Implementations.Library
{
_logger.LogError(ex, "Error getting intros");
- return new List<IntroInfo>();
+ return Enumerable.Empty<IntroInfo>();
}
}
@@ -1877,7 +1892,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;
}
@@ -2741,9 +2756,7 @@ namespace Emby.Server.Implementations.Library
}
})
.Where(i => i is not null)
- .Where(i => query.User is null ?
- true :
- i.IsVisible(query.User))
+ .Where(i => query.User is null || i.IsVisible(query.User))
.ToList();
}
@@ -2876,7 +2889,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)
{
@@ -2918,12 +2931,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);
}
@@ -3085,22 +3098,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)
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index eadfa5dfe..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)
diff --git a/Emby.Server.Implementations/Library/PathExtensions.cs b/Emby.Server.Implementations/Library/PathExtensions.cs
index 64e7d5446..c4b6b3756 100644
--- a/Emby.Server.Implementations/Library/PathExtensions.cs
+++ b/Emby.Server.Implementations/Library/PathExtensions.cs
@@ -1,5 +1,6 @@
using System;
using System.Diagnostics.CodeAnalysis;
+using System.IO;
using MediaBrowser.Common.Providers;
namespace Emby.Server.Implementations.Library
@@ -86,24 +87,8 @@ namespace Emby.Server.Implementations.Library
return false;
}
- char oldDirectorySeparatorChar;
- char newDirectorySeparatorChar;
- // True normalization is still not possible https://github.com/dotnet/runtime/issues/2162
- // The reasoning behind this is that a forward slash likely means it's a Linux path and
- // so the whole path should be normalized to use / and vice versa for Windows (although Windows doesn't care much).
- if (newSubPath.Contains('/', StringComparison.Ordinal))
- {
- oldDirectorySeparatorChar = '\\';
- newDirectorySeparatorChar = '/';
- }
- else
- {
- oldDirectorySeparatorChar = '/';
- newDirectorySeparatorChar = '\\';
- }
-
- path = path.Replace(oldDirectorySeparatorChar, newDirectorySeparatorChar);
- subPath = subPath.Replace(oldDirectorySeparatorChar, newDirectorySeparatorChar);
+ subPath = subPath.NormalizePath(out var newDirectorySeparatorChar);
+ path = path.NormalizePath(newDirectorySeparatorChar);
// We have to ensure that the sub path ends with a directory separator otherwise we'll get weird results
// when the sub path matches a similar but in-complete subpath
@@ -127,5 +112,82 @@ namespace Emby.Server.Implementations.Library
return true;
}
+
+ /// <summary>
+ /// Retrieves the full resolved path and normalizes path separators to the <see cref="Path.DirectorySeparatorChar"/>.
+ /// </summary>
+ /// <param name="path">The path to canonicalize.</param>
+ /// <returns>The fully expanded, normalized path.</returns>
+ public static string Canonicalize(this string path)
+ {
+ return Path.GetFullPath(path).NormalizePath();
+ }
+
+ /// <summary>
+ /// Normalizes the path's directory separator character to the currently defined <see cref="Path.DirectorySeparatorChar"/>.
+ /// </summary>
+ /// <param name="path">The path to normalize.</param>
+ /// <returns>The normalized path string or <see langword="null"/> if the input path is null or empty.</returns>
+ [return: NotNullIfNotNull(nameof(path))]
+ public static string? NormalizePath(this string? path)
+ {
+ return path.NormalizePath(Path.DirectorySeparatorChar);
+ }
+
+ /// <summary>
+ /// Normalizes the path's directory separator character.
+ /// </summary>
+ /// <param name="path">The path to normalize.</param>
+ /// <param name="separator">The separator character the path now uses or <see langword="null"/>.</param>
+ /// <returns>The normalized path string or <see langword="null"/> if the input path is null or empty.</returns>
+ [return: NotNullIfNotNull(nameof(path))]
+ public static string? NormalizePath(this string? path, out char separator)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ separator = default;
+ return path;
+ }
+
+ var newSeparator = '\\';
+
+ // True normalization is still not possible https://github.com/dotnet/runtime/issues/2162
+ // The reasoning behind this is that a forward slash likely means it's a Linux path and
+ // so the whole path should be normalized to use / and vice versa for Windows (although Windows doesn't care much).
+ if (path.Contains('/', StringComparison.Ordinal))
+ {
+ newSeparator = '/';
+ }
+
+ separator = newSeparator;
+
+ return path.NormalizePath(newSeparator);
+ }
+
+ /// <summary>
+ /// Normalizes the path's directory separator character to the specified character.
+ /// </summary>
+ /// <param name="path">The path to normalize.</param>
+ /// <param name="newSeparator">The replacement directory separator character. Must be a valid directory separator.</param>
+ /// <returns>The normalized path.</returns>
+ /// <exception cref="ArgumentException">Thrown if the new separator character is not a directory separator.</exception>
+ [return: NotNullIfNotNull(nameof(path))]
+ public static string? NormalizePath(this string? path, char newSeparator)
+ {
+ const char Bs = '\\';
+ const char Fs = '/';
+
+ if (!(newSeparator == Bs || newSeparator == Fs))
+ {
+ throw new ArgumentException("The character must be a directory separator.");
+ }
+
+ if (string.IsNullOrEmpty(path))
+ {
+ return path;
+ }
+
+ return newSeparator == Bs ? path.Replace(Fs, newSeparator) : path.Replace(Bs, newSeparator);
+ }
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
index 06621700a..a74f82475 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
@@ -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,7 +179,7 @@ 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 is not null && parent.IsTopParent);
@@ -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 e8615e7db..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))
diff --git a/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs b/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs
index 30c52e19d..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>
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 1522cd3ae..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;
}
@@ -97,12 +98,12 @@ 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))
@@ -118,12 +119,12 @@ 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
@@ -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)
{
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 7a2b3da3a..5d569009d 100644
--- a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
@@ -30,7 +30,7 @@ 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
+ // It's a boxset if the path is a directory with [playlist] in its name
var filename = Path.GetFileName(Path.TrimEndingDirectorySeparator(args.Path));
if (string.IsNullOrEmpty(filename))
{
@@ -42,7 +42,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
return new Playlist
{
Path = args.Path,
- Name = filename.Replace("[playlist]", string.Empty, StringComparison.OrdinalIgnoreCase).Trim()
+ Name = filename.Replace("[playlist]", string.Empty, StringComparison.OrdinalIgnoreCase).Trim(),
+ OpenAccess = true
};
}
@@ -53,7 +54,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
return new Playlist
{
Path = args.Path,
- Name = filename
+ Name = filename,
+ OpenAccess = true
};
}
}
@@ -70,7 +72,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
Path = args.Path,
Name = Path.GetFileNameWithoutExtension(args.Path),
IsInMixedFolder = true,
- PlaylistMediaType = MediaType.Audio
+ PlaylistMediaType = MediaType.Audio,
+ OpenAccess = true
};
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
index 0fcc5070b..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)
{
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
index 62a524d2e..e9538a5c9 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
@@ -81,14 +81,24 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
if (season.IndexNumber.HasValue)
{
var seasonNumber = season.IndexNumber.Value;
-
- season.Name = seasonNumber == 0 ?
- args.LibraryOptions.SeasonZeroDisplayName :
- string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("NameSeasonNumber"),
- seasonNumber,
- args.LibraryOptions.PreferredMetadataLanguage);
+ if (string.IsNullOrEmpty(season.Name))
+ {
+ var seasonNames = series.SeasonNames;
+ if (seasonNames.TryGetValue(seasonNumber, out var seasonName))
+ {
+ season.Name = seasonName;
+ }
+ else
+ {
+ season.Name = seasonNumber == 0 ?
+ args.LibraryOptions.SeasonZeroDisplayName :
+ string.Format(
+ CultureInfo.InvariantCulture,
+ _localization.GetLocalizedString("NameSeasonNumber"),
+ seasonNumber,
+ args.LibraryOptions.PreferredMetadataLanguage);
+ }
+ }
}
return season;
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
index 8f69175d0..d4f275bed 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
@@ -184,6 +184,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
{
var justName = Path.GetFileName(path.AsSpan());
+ var imdbId = justName.GetAttributeValue("imdbid");
+ if (!string.IsNullOrEmpty(imdbId))
+ {
+ item.SetProviderId(MetadataProvider.Imdb, imdbId);
+ }
+
var tvdbId = justName.GetAttributeValue("tvdbid");
if (!string.IsNullOrEmpty(tvdbId))
{
diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs
index 1137625f4..2c3dc1857 100644
--- a/Emby.Server.Implementations/Library/UserViewManager.cs
+++ b/Emby.Server.Implementations/Library/UserViewManager.cs
@@ -46,10 +46,9 @@ namespace Emby.Server.Implementations.Library
public Folder[] GetUserViews(UserViewQuery query)
{
var user = _userManager.GetUserById(query.UserId);
-
if (user is null)
{
- throw new ArgumentException("User Id specified in the query does not exist.", nameof(query));
+ throw new ArgumentException("User id specified in the query does not exist.", nameof(query));
}
var folders = _libraryManager.GetUserRootFolder()
@@ -58,7 +57,6 @@ namespace Emby.Server.Implementations.Library
.ToList();
var groupedFolders = new List<ICollectionFolder>();
-
var list = new List<Folder>();
foreach (var folder in folders)
@@ -66,6 +64,20 @@ namespace Emby.Server.Implementations.Library
var collectionFolder = folder as ICollectionFolder;
var folderViewType = collectionFolder?.CollectionType;
+ // Playlist library requires special handling because the folder only refrences user playlists
+ if (string.Equals(folderViewType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
+ {
+ var items = folder.GetItemList(new InternalItemsQuery(user)
+ {
+ ParentId = folder.ParentId
+ });
+
+ if (!items.Any(item => item.IsVisible(user)))
+ {
+ continue;
+ }
+ }
+
if (UserView.IsUserSpecific(folder))
{
list.Add(_libraryManager.GetNamedView(user, folder.Name, folder.Id, folderViewType, null));
@@ -111,10 +123,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;
@@ -132,14 +144,12 @@ namespace Emby.Server.Implementations.Library
}
var sorted = _libraryManager.Sort(list, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).ToList();
-
var orders = user.GetPreferenceValues<Guid>(PreferenceKind.OrderedViews);
return list
.OrderBy(i =>
{
var index = Array.IndexOf(orders, i.Id);
-
if (index == -1
&& i is UserView view
&& !view.DisplayParentId.Equals(default))
@@ -286,7 +296,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/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
index 8edd8f66a..b9d0f170a 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
@@ -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);
@@ -1866,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);
}
@@ -2032,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();
@@ -2042,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();
@@ -2122,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
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
index 3f7914d3b..7645c6c52 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
@@ -415,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 + "?token=" + token;
- }
+
+ return apiUrl + "/image/" + uri + "?token=" + token;
}
private static double GetAspectRatio(ImageDataDto i)
@@ -463,10 +462,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings
}
StringBuilder str = new StringBuilder("[", 1 + (programIds.Count * 13));
- foreach (ReadOnlySpan<char> i in programIds)
+ foreach (var i in programIds)
{
str.Append('"')
- .Append(i.Slice(0, 10))
+ .Append(i[..10])
.Append("\",");
}
@@ -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;
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
index e874990da..066afb956 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
@@ -137,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 is not null,
+ SeasonNumber = program.Episode.Series,
+ IsSeries = program.Episode.Series is not null,
IsRepeat = program.IsPreviouslyShown && !program.IsNew,
IsPremiere = program.Premiere is not 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)),
+ 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 is 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))
@@ -243,7 +244,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{
Id = c.Id,
Name = c.DisplayName,
- ImageUrl = string.IsNullOrEmpty(c.Icon.Source) ? null : c.Icon.Source,
+ 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/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
index 4003468d0..ee039ff0f 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
@@ -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 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>();
}
@@ -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,
@@ -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);
@@ -2379,12 +2378,11 @@ 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)
@@ -2396,14 +2394,16 @@ namespace Emby.Server.Implementations.LiveTv
.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/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
index 5327b3d74..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";
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
index 81eb083f6..7bc209d6b 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
@@ -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);
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/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
index bcb42e162..acf3964c8 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
@@ -30,12 +30,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
public class M3UTunerHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
{
- private static readonly string[] _disallowedSharedStreamExtensions =
+ private static readonly string[] _disallowedMimeTypes =
{
- ".mkv",
- ".mp4",
- ".m3u8",
- ".mpd"
+ "video/x-matroska",
+ "video/mp4",
+ "application/vnd.apple.mpegurl",
+ "application/mpegurl",
+ "application/x-mpegurl",
+ "video/vnd.mpeg.dash.mpd"
};
private readonly IHttpClientFactory _httpClientFactory;
@@ -118,9 +120,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
if (mediaSource.Protocol == MediaProtocol.Http && !mediaSource.RequiresLooping)
{
- var extension = Path.GetExtension(mediaSource.Path) ?? string.Empty;
+ using var message = new HttpRequestMessage(HttpMethod.Head, mediaSource.Path);
+ using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+ .SendAsync(message, cancellationToken)
+ .ConfigureAwait(false);
- if (!_disallowedSharedStreamExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
+ response.EnsureSuccessStatusCode();
+
+ if (!_disallowedMimeTypes.Contains(response.Content.Headers.ContentType?.ToString(), StringComparison.OrdinalIgnoreCase))
{
return new SharedHttpStream(mediaSource, tunerHost, streamId, FileSystem, _httpClientFactory, Logger, Config, _appHost, _streamHelper);
}
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/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
index e84e1e074..51f46f4da 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
@@ -38,7 +38,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
_httpClientFactory = httpClientFactory;
_appHost = appHost;
OriginalStreamId = originalStreamId;
- EnableStreamSharing = true;
}
public override async Task Open(CancellationToken openCancellationToken)
@@ -59,39 +58,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None)
.ConfigureAwait(false);
- var contentType = response.Content.Headers.ContentType?.ToString() ?? string.Empty;
- if (contentType.Contains("matroska", StringComparison.OrdinalIgnoreCase)
- || contentType.Contains("mp4", StringComparison.OrdinalIgnoreCase)
- || contentType.Contains("dash", StringComparison.OrdinalIgnoreCase)
- || contentType.Contains("mpegURL", StringComparison.OrdinalIgnoreCase)
- || contentType.Contains("text/", StringComparison.OrdinalIgnoreCase))
- {
- // Close the stream without any sharing features
- response.Dispose();
- return;
- }
-
- SetTempFilePath("ts");
-
var taskCompletionSource = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
_ = StartStreaming(response, taskCompletionSource, LiveStreamCancellationTokenSource.Token);
- // OpenedMediaSource.Protocol = MediaProtocol.File;
- // OpenedMediaSource.Path = tempFile;
- // OpenedMediaSource.ReadAtNativeFramerate = true;
-
MediaSource.Path = _appHost.GetApiUrlForLocalAccess() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
MediaSource.Protocol = MediaProtocol.Http;
- // OpenedMediaSource.Path = TempFilePath;
- // OpenedMediaSource.Protocol = MediaProtocol.File;
-
- // OpenedMediaSource.Path = _tempFilePath;
- // OpenedMediaSource.Protocol = MediaProtocol.File;
- // OpenedMediaSource.SupportsDirectPlay = false;
- // OpenedMediaSource.SupportsDirectStream = true;
- // OpenedMediaSource.SupportsTranscoding = true;
var res = await taskCompletionSource.Task.ConfigureAwait(false);
if (!res)
{
@@ -108,15 +81,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
try
{
Logger.LogInformation("Beginning {StreamType} stream to {FilePath}", GetType().Name, TempFilePath);
- using var message = response;
- await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
- await StreamHelper.CopyToAsync(
- stream,
- fileStream,
- IODefaults.CopyToBufferSize,
- () => Resolve(openTaskCompletionSource),
- cancellationToken).ConfigureAwait(false);
+ using (response)
+ {
+ await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
+ await StreamHelper.CopyToAsync(
+ stream,
+ fileStream,
+ IODefaults.CopyToBufferSize,
+ () => Resolve(openTaskCompletionSource),
+ cancellationToken).ConfigureAwait(false);
+ }
}
catch (OperationCanceledException ex)
{
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/bn.json b/Emby.Server.Implementations/Localization/Core/bn.json
index c3fbe2408..005926231 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 1966f6968..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,65 +16,65 @@
"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",
+ "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",
+ "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": "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 de Jellyfin està carregant. Si et plau, prova de nou ben aviat.",
+ "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": "Sèries de TV",
@@ -82,11 +82,11 @@
"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 biblioteca",
@@ -94,14 +94,14 @@
"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.",
+ "TaskRefreshChannelsDescription": "Actualitza la informació dels canals d'internet.",
+ "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.",
@@ -110,12 +110,12 @@
"TaskRefreshChapterImages": "Extreure les imatges dels capítols",
"TaskCleanCacheDescription": "Elimina els arxius temporals que ja no són necessaris per al servidor.",
"TaskCleanCache": "Elimina arxius temporals",
- "TasksChannelsCategory": "Canals d'Internet",
+ "TasksChannelsCategory": "Canals d'internet",
"TasksApplicationCategory": "Aplicació",
"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": "Per defecte",
@@ -124,5 +124,5 @@
"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",
- "HearingImpaired": "Discapacitat Auditiva"
+ "HearingImpaired": "Discapacitat auditiva"
}
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 0d0d0c813..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": "Albumkunstner",
+ "HeaderAlbumArtists": "Albums kunstnere",
"HeaderContinueWatching": "Fortsæt afspilning",
- "HeaderFavoriteAlbums": "Favoritalbummer",
- "HeaderFavoriteArtists": "Favoritkunstnere",
- "HeaderFavoriteEpisodes": "Favoritepisoder",
- "HeaderFavoriteShows": "Favoritserier",
- "HeaderFavoriteSongs": "Favoritsange",
+ "HeaderFavoriteAlbums": "Favorit albummer",
+ "HeaderFavoriteArtists": "Favorit kunstnere",
+ "HeaderFavoriteEpisodes": "Favorit afsnit",
+ "HeaderFavoriteShows": "Favorit serier",
+ "HeaderFavoriteSongs": "Favorit sange",
"HeaderLiveTV": "Live-TV",
"HeaderNextUp": "Næste",
"HeaderRecordingGroups": "Optagelsesgrupper",
@@ -39,90 +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",
+ "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",
+ "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/es-AR.json b/Emby.Server.Implementations/Localization/Core/es-AR.json
index 8ad9e8c71..8bd3c5def 100644
--- a/Emby.Server.Implementations/Localization/Core/es-AR.json
+++ b/Emby.Server.Implementations/Localization/Core/es-AR.json
@@ -118,11 +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",
- "HearingImpaired": "Personas con discapacidad auditiva"
+ "HearingImpaired": "Discapacidad Auditiva"
}
diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json
index afffdf3bf..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",
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/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 ec72d58dd..8672cfb9f 100644
--- a/Emby.Server.Implementations/Localization/Core/fi.json
+++ b/Emby.Server.Implementations/Localization/Core/fi.json
@@ -118,7 +118,7 @@
"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",
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/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/hi.json b/Emby.Server.Implementations/Localization/Core/hi.json
index 182b43ffc..47d3eeac5 100644
--- a/Emby.Server.Implementations/Localization/Core/hi.json
+++ b/Emby.Server.Implementations/Localization/Core/hi.json
@@ -67,5 +67,61 @@
"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": "लोगोकी जानकारी ताज़ी करें",
+ "TaskKeyframeExtractor": "कीफ़्रेम एक्सट्रैक्टर",
+ "TaskCleanActivityLogDescription": "कॉन्फ़िगर की गई आयु से पुरानी गतिविधि लॉग प्रविष्टियां हटाता है।",
+ "TaskRefreshChapterImagesDescription": "अध्याय वाले वीडियो के लिए थंबनेल बनाता है।",
+ "TaskRefreshLibraryDescription": "नई फ़ाइलों के लिए आपकी मीडिया लाइब्रेरी को स्कैन करता है और मेटाडेटा को ताज़ा करता है।",
+ "TaskCleanLogs": "स्वच्छ लॉग निर्देशिका",
+ "TaskUpdatePluginsDescription": "प्लगइन्स के लिए अपडेट डाउनलोड और इंस्टॉल करें जो स्वचालित रूप से अपडेट करने के लिए कॉन्फ़िगर किए गए हैं।",
+ "TaskCleanTranscode": "स्वच्छ ट्रांसकोड निर्देशिका",
+ "TaskCleanTranscodeDescription": "एक दिन से अधिक पुरानी ट्रांसकोड फ़ाइलें हटाता है.",
+ "TaskRefreshChannelsDescription": "इंटरनेट चैनल की जानकारी को ताज़ा करता है।",
+ "TaskOptimizeDatabaseDescription": "डेटाबेस को कॉम्पैक्ट करता है और मुक्त स्थान को छोटा करता है। लाइब्रेरी को स्कैन करने के बाद इस कार्य को चलाने या अन्य परिवर्तन करने से जो डेटाबेस संशोधनों को लागू करते हैं, प्रदर्शन में सुधार कर सकते हैं।",
+ "TaskRefreshChannels": "इंटरनेट चैनल की जानकारी को ताज़ा करता है",
+ "TaskRefreshChapterImages": "अध्याय छवियाँ निकालें",
+ "TaskCleanLogsDescription": "{0} दिन से अधिक पुरानी लॉग फ़ाइलें हटाता है।",
+ "TaskCleanCacheDescription": "उन कैश फ़ाइलों को हटाता है जिनकी अब सिस्टम को आवश्यकता नहीं है।",
+ "TaskUpdatePlugins": "अद्यतन प्लगइन्स",
+ "TaskRefreshPeopleDescription": "आपकी मीडिया लाइब्रेरी में अभिनेताओं और निर्देशकों के लिए मेटाडेटा अपडेट करता है।",
+ "TaskCleanCache": "स्वच्छ कैश निर्देशिका",
+ "TaskDownloadMissingSubtitlesDescription": "मेटाडेटा कॉन्फ़िगरेशन के आधार पर लापता उपशीर्षक के लिए इंटरनेट खोजता है।",
+ "TaskKeyframeExtractorDescription": "अधिक सटीक एचएलएस प्लेलिस्ट बनाने के लिए वीडियो फ़ाइलों से मुख्य-फ़्रेम निकालता है। यह कार्य लंबे समय तक चल सकता है।"
}
diff --git a/Emby.Server.Implementations/Localization/Core/id.json b/Emby.Server.Implementations/Localization/Core/id.json
index 695c0f404..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",
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/ja.json b/Emby.Server.Implementations/Localization/Core/ja.json
index 7f616c35a..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}",
diff --git a/Emby.Server.Implementations/Localization/Core/lt-LT.json b/Emby.Server.Implementations/Localization/Core/lt-LT.json
index e1c937b6c..ce8d8fc32 100644
--- a/Emby.Server.Implementations/Localization/Core/lt-LT.json
+++ b/Emby.Server.Implementations/Localization/Core/lt-LT.json
@@ -20,9 +20,9 @@
"HeaderFavoriteAlbums": "Mėgstami Albumai",
"HeaderFavoriteArtists": "Mėgstami Atlikėjai",
"HeaderFavoriteEpisodes": "Mėgstamiausios serijos",
- "HeaderFavoriteShows": "Mėgstamiausi serialai",
- "HeaderFavoriteSongs": "Mėgstamos dainos",
- "HeaderLiveTV": "TV gyvai",
+ "HeaderFavoriteShows": "Mėgstamiausios TV Laidos",
+ "HeaderFavoriteSongs": "Mėgstamos Dainos",
+ "HeaderLiveTV": "Tiesioginė TV",
"HeaderNextUp": "Toliau eilėje",
"HeaderRecordingGroups": "Įrašų grupės",
"HomeVideos": "Namų vaizdo įrašai",
diff --git a/Emby.Server.Implementations/Localization/Core/lv.json b/Emby.Server.Implementations/Localization/Core/lv.json
index e460fd719..f7b24412a 100644
--- a/Emby.Server.Implementations/Localization/Core/lv.json
+++ b/Emby.Server.Implementations/Localization/Core/lv.json
@@ -84,7 +84,7 @@
"CameraImageUploadedFrom": "Jauns kameras attēls ir ticis augšupielādēts no {0}",
"Books": "Grāmatas",
"Artists": "Izpildītāji",
- "Albums": "Albūmi",
+ "Albums": "Albumi",
"ProviderValue": "Provider: {0}",
"HeaderFavoriteSongs": "Dziesmu Favorīti",
"HeaderFavoriteShows": "Raidījumu Favorīti",
@@ -120,5 +120,8 @@
"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",
+ "TaskKeyframeExtractor": "Atslēgkadru Ekstraktors",
+ "TaskKeyframeExtractorDescription": "Ekstraktē atslēgkadrus no video failiem lai izveidotu precīzākus HLS atskaņošanas sarakstus. Šis process var būt ilgs."
}
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/ml.json b/Emby.Server.Implementations/Localization/Core/ml.json
index acc7746c1..0620fbcdb 100644
--- a/Emby.Server.Implementations/Localization/Core/ml.json
+++ b/Emby.Server.Implementations/Localization/Core/ml.json
@@ -119,5 +119,7 @@
"Genres": "വിഭാഗങ്ങൾ",
"Channels": "ചാനലുകൾ",
"TaskOptimizeDatabaseDescription": "ഡാറ്റാബേസ് ചുരുക്കുകയും സ്വതന്ത്ര ഇടം വെട്ടിച്ചുരുക്കുകയും ചെയ്യുന്നു. ലൈബ്രറി സ്‌കാൻ ചെയ്‌തതിനുശേഷം അല്ലെങ്കിൽ ഡാറ്റാബേസ് പരിഷ്‌ക്കരണങ്ങളെ സൂചിപ്പിക്കുന്ന മറ്റ് മാറ്റങ്ങൾ ചെയ്‌തതിന് ശേഷം ഈ ടാസ്‌ക് പ്രവർത്തിപ്പിക്കുന്നത് പ്രകടനം മെച്ചപ്പെടുത്തും.",
- "TaskOptimizeDatabase": "ഡാറ്റാബേസ് ഒപ്റ്റിമൈസ് ചെയ്യുക"
+ "TaskOptimizeDatabase": "ഡാറ്റാബേസ് ഒപ്റ്റിമൈസ് ചെയ്യുക",
+ "HearingImpaired": "കേൾവി തകരാറുകൾ",
+ "External": "പുറമേയുള്ള"
}
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/ne.json b/Emby.Server.Implementations/Localization/Core/ne.json
index 4c8e820a5..7c6b08fb3 100644
--- a/Emby.Server.Implementations/Localization/Core/ne.json
+++ b/Emby.Server.Implementations/Localization/Core/ne.json
@@ -109,5 +109,19 @@
"Sync": "समकालीन",
"SubtitleDownloadFailureFromForItem": "उपशीर्षकहरू {0} बाट {1} को लागि डाउनलोड गर्न असफल",
"PluginUpdatedWithName": "{0} अद्यावधिक गरिएको थियो",
- "PluginUninstalledWithName": "{0} को स्थापना रद्द गरिएको थियो"
+ "PluginUninstalledWithName": "{0} को स्थापना रद्द गरिएको थियो",
+ "HearingImpaired": "सुन्न नसक्ने",
+ "TaskUpdatePluginsDescription": "स्वचालित रूपमा अद्यावधिक गर्न कन्फिगर गरिएका प्लगइनहरूका लागि अद्यावधिकहरू डाउनलोड र स्थापना गर्दछ।",
+ "TaskCleanTranscode": "सफा ट्रान्सकोड निर्देशिका",
+ "TaskCleanTranscodeDescription": "एक दिन भन्दा पुराना ट्रान्सकोड फाइलहरू मेटाउँछ।",
+ "TaskRefreshChannels": "च्यानलहरू ताजा गर्नुहोस्",
+ "TaskDownloadMissingSubtitlesDescription": "मेटाडेटा कन्फिगरेसनमा आधारित हराइरहेको उपशीर्षकहरूको लागि इन्टरनेट खोज्छ।",
+ "TaskOptimizeDatabase": "डेटाबेस अप्टिमाइज गर्नुहोस्",
+ "TaskOptimizeDatabaseDescription": "डाटाबेस कम्प्याक्ट र खाली ठाउँ काट्छ। पुस्तकालय स्क्यान गरेपछि वा डाटाबेस परिमार्जनलाई संकेत गर्ने अन्य परिवर्तनहरू गरेपछि यो कार्य चलाउँदा कार्यसम्पादनमा सुधार हुन सक्छ।",
+ "TaskKeyframeExtractorDescription": "थप सटीक एचएलएस प्लेलिस्टहरू सिर्जना गर्न भिडियो फाइलहरूबाट कीफ्रेमहरू निकाल्छ। यो कार्य लामो समय सम्म चल्न सक्छ।",
+ "TaskUpdatePlugins": "प्लगइनहरू अपडेट गर्नुहोस्",
+ "TaskRefreshPeopleDescription": "तपाईंको मिडिया लाइब्रेरीमा अभिनेता र निर्देशकहरूको लागि मेटाडेटा अपडेट गर्दछ।",
+ "TaskRefreshChannelsDescription": "इन्टरनेट च्यानल जानकारी ताजा गर्दछ।",
+ "TaskDownloadMissingSubtitles": "छुटेका उपशीर्षकहरू डाउनलोड गर्नुहोस्",
+ "TaskKeyframeExtractor": "कीफ्रेम एक्स्ट्रक्टर"
}
diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json
index e03747cbe..4eb00d289 100644
--- a/Emby.Server.Implementations/Localization/Core/nl.json
+++ b/Emby.Server.Implementations/Localization/Core/nl.json
@@ -8,7 +8,7 @@
"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}",
@@ -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",
@@ -95,26 +95,26 @@
"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": "Logboekmap opschonen",
"TaskCleanTranscode": "Transcoderingsmap opschonen",
"TaskUpdatePluginsDescription": "Downloadt en installeert updates van plug-ins waarvoor automatisch bijwerken is ingeschakeld.",
"TaskUpdatePlugins": "Plug-ins bijwerken",
- "TaskRefreshPeopleDescription": "Update metadata for acteurs en regisseurs in de media bibliotheek.",
+ "TaskRefreshPeopleDescription": "Updatet metadata voor acteurs en regisseurs in je mediabibliotheek.",
"TaskRefreshPeople": "Personen vernieuwen",
"TaskCleanLogsDescription": "Verwijdert log bestanden ouder dan {0} dagen.",
"TaskRefreshLibraryDescription": "Scant de mediabibliotheek op nieuwe bestanden en vernieuwt de metadata.",
"TaskRefreshLibrary": "Mediabibliotheek scannen",
- "TaskRefreshChapterImagesDescription": "Maakt thumbnails aan voor videos met hoofdstukken.",
- "TaskRefreshChapterImages": "Hoofdstukafbeeldingen uitpakken",
+ "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-map opschonen",
- "TasksChannelsCategory": "Internet Kanalen",
+ "TasksChannelsCategory": "Internetkanalen",
"TasksApplicationCategory": "Toepassing",
"TasksLibraryCategory": "Bibliotheek",
"TasksMaintenanceCategory": "Onderhoud",
- "TaskCleanActivityLogDescription": "Verwijdert activiteiten logs ouder dan de ingestelde tijd.",
+ "TaskCleanActivityLogDescription": "Verwijdert activiteitenlogs ouder dan de ingestelde leeftijd.",
"TaskCleanActivityLog": "Activiteitenlogboek legen",
"Undefined": "Niet gedefinieerd",
"Forced": "Geforceerd",
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/pr.json b/Emby.Server.Implementations/Localization/Core/pr.json
index 466c8a990..87800a2fe 100644
--- a/Emby.Server.Implementations/Localization/Core/pr.json
+++ b/Emby.Server.Implementations/Localization/Core/pr.json
@@ -19,5 +19,10 @@
"FailedLoginAttemptWithUserName": "Ye failed to get in, try from {0}",
"Favorites": "Finest Loot",
"ItemRemovedWithName": "{0} was taken from yer treasure",
- "LabelIpAddressValue": "Ship's coordinates: {0}"
+ "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.json b/Emby.Server.Implementations/Localization/Core/pt.json
index 39229f45f..2281e80c8 100644
--- a/Emby.Server.Implementations/Localization/Core/pt.json
+++ b/Emby.Server.Implementations/Localization/Core/pt.json
@@ -121,5 +121,7 @@
"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",
- "HearingImpaired": "Problemas auditivos"
+ "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/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json
index 65cf29e80..421513341 100644
--- a/Emby.Server.Implementations/Localization/Core/ru.json
+++ b/Emby.Server.Implementations/Localization/Core/ru.json
@@ -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": "Обновление приложения установлено",
@@ -70,7 +70,7 @@
"ScheduledTaskFailedWithName": "{0} - неудачна",
"ScheduledTaskStartedWithName": "{0} - запущена",
"ServerNameNeedsToBeRestarted": "Необходим перезапуск {0}",
- "Shows": "Передачи",
+ "Shows": "Телешоу",
"Songs": "Композиции",
"StartupEmbyServerIsLoading": "Jellyfin Server загружается. Повторите попытку в ближайшее время.",
"SubtitleDownloadFailureForItem": "Субтитры к {0} не удалось загрузить",
@@ -96,7 +96,7 @@
"TaskRefreshChannels": "Обновление каналов",
"TaskCleanTranscode": "Очистка каталога перекодировки",
"TaskUpdatePlugins": "Обновление плагинов",
- "TaskRefreshPeople": "Подновление людей",
+ "TaskRefreshPeople": "Обновление информации о персонах",
"TaskCleanLogs": "Очистка каталога журналов",
"TaskRefreshLibrary": "Сканирование медиатеки",
"TaskRefreshChapterImages": "Извлечение изображений сцен",
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/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json
index 318a0f3cf..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",
diff --git a/Emby.Server.Implementations/Localization/Core/te.json b/Emby.Server.Implementations/Localization/Core/te.json
index a9a8ceae0..24168b611 100644
--- a/Emby.Server.Implementations/Localization/Core/te.json
+++ b/Emby.Server.Implementations/Localization/Core/te.json
@@ -19,5 +19,24 @@
"Channels": "ఛానెల్‌లు",
"Books": "పుస్తకాలు",
"Artists": "కళాకారులు",
- "Albums": "ఆల్బమ్‌లు"
+ "Albums": "ఆల్బమ్‌లు",
+ "HearingImpaired": "వినికిడి లోపం",
+ "HomeVideos": "హోమ్ వీడియోలు",
+ "AppDeviceValues": "అప్లికేషన్ : {0}, పరికరం: {1}",
+ "Application": "అప్లికేషన్",
+ "AuthenticationSucceededWithUserName": "విజయవంతంగా ఆమోదించబడింది",
+ "CameraImageUploadedFrom": "{0} నుండి కొత్త కెమెరా చిత్రం అప్‌లోడ్ చేయబడింది",
+ "ChapterNameValue": "అధ్యాయం",
+ "DeviceOfflineWithName": "{0} డిస్‌కనెక్ట్ చేయబడింది",
+ "DeviceOnlineWithName": "{0} కనెక్ట్ చేయబడింది",
+ "External": "బాహ్య",
+ "FailedLoginAttemptWithUserName": "{0} నుండి విఫలమైన లాగిన్ ప్రయత్నం",
+ "HeaderFavoriteAlbums": "ఇష్టమైన ఆల్బమ్‌లు",
+ "HeaderFavoriteArtists": "ఇష్టమైన కళాకారులు",
+ "HeaderFavoriteEpisodes": "ఇష్టమైన ఎపిసోడ్‌లు",
+ "HeaderFavoriteShows": "ఇష్టమైన ప్రదర్శనలు",
+ "HeaderFavoriteSongs": "ఇష్టమైన పాటలు",
+ "HeaderLiveTV": "ప్రత్యక్ష TV",
+ "HeaderNextUp": "తదుపరి",
+ "HeaderRecordingGroups": "రికార్డింగ్ గుంపులు"
}
diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json
index b802db982..9a140f871 100644
--- a/Emby.Server.Implementations/Localization/Core/tr.json
+++ b/Emby.Server.Implementations/Localization/Core/tr.json
@@ -123,5 +123,6 @@
"TaskOptimizeDatabase": "Veritabanını optimize et",
"TaskKeyframeExtractorDescription": "Daha hassas HLS çalma listeleri oluşturmak için video dosyalarından kareleri çıkarır. Bu görev uzun bir süre çalışabilir.",
"TaskKeyframeExtractor": "Kare Ayırt Edici",
- "External": "Harici"
+ "External": "Harici",
+ "HearingImpaired": "Duyma engelli"
}
diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json
index 92ce616f2..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} видалено",
diff --git a/Emby.Server.Implementations/Localization/Core/ur_PK.json b/Emby.Server.Implementations/Localization/Core/ur_PK.json
index 7fe0c4c4b..5d3f19432 100644
--- a/Emby.Server.Implementations/Localization/Core/ur_PK.json
+++ b/Emby.Server.Implementations/Localization/Core/ur_PK.json
@@ -12,7 +12,7 @@
"HeaderContinueWatching": "دیکھنا جاری رکھیں",
"Playlists": "پلے لسٹس",
"ValueSpecialEpisodeName": "خصوصی - {0}",
- "Shows": "دکھاتا ہے۔",
+ "Shows": "دکھاتا ہے",
"Genres": "انواع",
"Artists": "فنکار",
"Sync": "مطابقت پذیری",
@@ -123,5 +123,5 @@
"TaskCleanActivityLogDescription": "تشکیل شدہ عمر سے زیادہ پرانی سرگرمی لاگ اندراجات کو حذف کرتا ہے۔",
"External": "بیرونی",
"HearingImpaired": "قوت سماعت سے محروم",
- "TaskCleanActivityLog": "سرگرمی لاگ کو صاف کریں۔"
+ "TaskCleanActivityLog": "سرگرمی لاگ کو صاف کریں"
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json
index ccfbeef0c..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": "收藏的专辑",
diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json
index cdc25ec7c..e8b8c2c5f 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-HK.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json
@@ -4,18 +4,18 @@
"Application": "應用程式",
"Artists": "藝人",
"AuthenticationSucceededWithUserName": "{0} 授權成功",
- "Books": "圖書",
- "CameraImageUploadedFrom": "{0} 成功上傳一張新相片",
+ "Books": "書籍",
+ "CameraImageUploadedFrom": "{0} 成功上傳一張新照片",
"Channels": "頻道",
- "ChapterNameValue": "章節 {0}",
- "Collections": "合輯",
- "DeviceOfflineWithName": "{0} 已經斷開連接",
- "DeviceOnlineWithName": "{0} 已經連接",
+ "ChapterNameValue": "第 {0} 章",
+ "Collections": "系列",
+ "DeviceOfflineWithName": "{0} 已斷開連接",
+ "DeviceOnlineWithName": "{0} 已連接",
"FailedLoginAttemptWithUserName": "{0} 登入失敗",
"Favorites": "我的最愛",
"Folders": "資料夾",
"Genres": "風格",
- "HeaderAlbumArtists": "專輯藝人",
+ "HeaderAlbumArtists": "專輯歌手",
"HeaderContinueWatching": "繼續觀看",
"HeaderFavoriteAlbums": "最愛的專輯",
"HeaderFavoriteArtists": "最愛的藝人",
@@ -23,105 +23,105 @@
"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 伺服器可供下載。",
+ "NameSeasonUnknown": "未知的季度",
+ "NewVersionIsAvailable": "有較新版本的 Jellyfin 可供下載。",
"NotificationOptionApplicationUpdateAvailable": "有可用的更新",
- "NotificationOptionApplicationUpdateInstalled": "應用程式已更新",
+ "NotificationOptionApplicationUpdateInstalled": "應用程式已被更新",
"NotificationOptionAudioPlayback": "開始播放音訊",
- "NotificationOptionAudioPlaybackStopped": "已停止播放音訊",
- "NotificationOptionCameraImageUploaded": "相片已上傳",
+ "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": "外部",
"HearingImpaired": "聽力障礙"
diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json
index 4949c5ab6..36f4df93d 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-TW.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json
@@ -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": "應用程式",
diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs
index b418c7877..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,80 +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))
+ {
+ ratings.Add(new ParentalRating("XXX", 1000));
+ }
+
+ // A lot of countries don't excplicitly have a seperate rating for banned content
+ if (ratings.All(x => x.Value != 1001))
{
- countryCode = "us";
+ ratings.Add(new ParentalRating("Banned", 1001));
}
- return GetRatings(countryCode)
- ?? GetRatings("us")
- ?? throw new InvalidOperationException($"Invalid resource path: '{CountriesPath}'");
+ 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;
+ }
- return value;
+ if (_allParentalRatings.TryGetValue(countryCode, out var countryValue))
+ {
+ return countryValue;
+ }
+
+ return null;
}
/// <inheritdoc />
- public int? GetRatingLevel(string rating)
+ public int? GetRatingLevel(string rating, string? countryCode = null)
{
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;
}
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/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
index 9fe51f083..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 is not 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/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
index 2717c392b..702f8d45b 100644
--- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs
+++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
@@ -67,9 +67,8 @@ namespace Emby.Server.Implementations.Playlists
public async Task<PlaylistCreationResult> CreatePlaylist(PlaylistCreationRequest options)
{
var name = options.Name;
-
var folderName = _fileSystem.GetValidFilename(name);
- var parentFolder = GetPlaylistsFolder(Guid.Empty);
+ var parentFolder = GetPlaylistsFolder(options.UserId);
if (parentFolder is null)
{
throw new ArgumentException(nameof(parentFolder));
@@ -80,7 +79,6 @@ namespace Emby.Server.Implementations.Playlists
foreach (var itemId in options.ItemIdList)
{
var item = _libraryManager.GetItemById(itemId);
-
if (item is null)
{
throw new ArgumentException("No item exists with the supplied Id");
@@ -121,7 +119,6 @@ namespace Emby.Server.Implementations.Playlists
}
var user = _userManager.GetUserById(options.UserId);
-
var path = Path.Combine(parentFolder.Path, folderName);
path = GetTargetPath(path);
@@ -130,25 +127,15 @@ namespace Emby.Server.Implementations.Playlists
try
{
Directory.CreateDirectory(path);
-
var playlist = new Playlist
{
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);
-
parentFolder.AddChild(playlist);
await playlist.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { ForceSave = true }, CancellationToken.None)
@@ -334,7 +321,8 @@ namespace Emby.Server.Implementations.Playlists
}
}
- private void SavePlaylistFile(Playlist item)
+ /// <inheritdoc />
+ public void SavePlaylistFile(Playlist item)
{
// this is probably best done as a metadata provider
// saving a file over itself will require some work to prevent this from happening when not needed
@@ -537,5 +525,40 @@ 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 if (!playlist.OpenAccess)
+ {
+ // Remove playlist if not shared
+ _libraryManager.DeleteItem(
+ playlist,
+ new DeleteOptions
+ {
+ DeleteFileLocation = false,
+ DeleteFromExternalProvider = false
+ },
+ playlist.GetParent(),
+ false);
+ }
+ }
+ }
}
}
diff --git a/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs b/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs
index e2f2e436f..d67caa52d 100644
--- a/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs
+++ b/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs
@@ -27,11 +27,6 @@ namespace Emby.Server.Implementations.Playlists
[JsonIgnore]
public override string CollectionType => MediaBrowser.Model.Entities.CollectionType.Playlists;
- public override bool IsVisible(User user)
- {
- return base.IsVisible(user) && GetChildren(user, true).Any();
- }
-
protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
{
return base.GetEligibleChildrenForRecursiveChildren(user).OfType<Playlist>();
@@ -47,8 +42,7 @@ namespace Emby.Server.Implementations.Playlists
query.Recursive = true;
query.IncludeItemTypes = new[] { BaseItemKind.Playlist };
- query.Parent = null;
- return LibraryManager.GetItemsResult(query);
+ return QueryWithPostFiltering2(query);
}
public override string GetClientTypeName()
diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs
index f2212f4dc..48584ae0c 100644
--- a/Emby.Server.Implementations/Plugins/PluginManager.cs
+++ b/Emby.Server.Implementations/Plugins/PluginManager.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Data;
using System.Globalization;
using System.IO;
using System.Linq;
@@ -9,6 +10,8 @@ using System.Runtime.Loader;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
+using Emby.Server.Implementations.Library;
+using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
using Jellyfin.Extensions.Json.Converters;
using MediaBrowser.Common;
@@ -29,6 +32,8 @@ namespace Emby.Server.Implementations.Plugins
/// </summary>
public class PluginManager : IPluginManager
{
+ private const string MetafileName = "meta.json";
+
private readonly string _pluginsPath;
private readonly Version _appVersion;
private readonly List<AssemblyLoadContext> _assemblyLoadContexts;
@@ -44,7 +49,7 @@ namespace Emby.Server.Implementations.Plugins
/// <summary>
/// Initializes a new instance of the <see cref="PluginManager"/> class.
/// </summary>
- /// <param name="logger">The <see cref="ILogger"/>.</param>
+ /// <param name="logger">The <see cref="ILogger{PluginManager}"/>.</param>
/// <param name="appHost">The <see cref="IApplicationHost"/>.</param>
/// <param name="config">The <see cref="ServerConfiguration"/>.</param>
/// <param name="pluginsPath">The plugin path.</param>
@@ -123,41 +128,64 @@ 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
{
- var assemblyLoadContext = new PluginLoadContext(file);
- _assemblyLoadContexts.Add(assemblyLoadContext);
-
- assembly = assemblyLoadContext.LoadFromAssemblyPath(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;
}
}
@@ -281,7 +309,7 @@ namespace Emby.Server.Implementations.Plugins
// 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 is not null) ?? plugins.OrderByDescending(p => p.Version).FirstOrDefault();
+ plugin = plugins.FirstOrDefault(p => p.Instance is not null) ?? plugins.MaxBy(p => p.Version);
}
else
{
@@ -348,7 +376,7 @@ namespace Emby.Server.Implementations.Plugins
try
{
var data = JsonSerializer.Serialize(manifest, _jsonOptions);
- File.WriteAllText(Path.Combine(path, "meta.json"), data);
+ File.WriteAllText(Path.Combine(path, MetafileName), data);
return true;
}
catch (ArgumentException e)
@@ -359,7 +387,7 @@ namespace Emby.Server.Implementations.Plugins
}
/// <inheritdoc/>
- public async Task<bool> GenerateManifest(PackageInfo packageInfo, Version version, string path, PluginStatus status)
+ public async Task<bool> PopulateManifest(PackageInfo packageInfo, Version version, string path, PluginStatus status)
{
var versionInfo = packageInfo.Versions.First(v => v.Version == version.ToString());
var imagePath = string.Empty;
@@ -404,10 +432,72 @@ namespace Emby.Server.Implementations.Plugins
ImagePath = imagePath
};
+ if (!await ReconcileManifest(manifest, path))
+ {
+ // An error occurred during reconciliation and saving could be undesirable.
+ return false;
+ }
+
return SaveManifest(manifest, path);
}
/// <summary>
+ /// Reconciles the manifest against any properties that exist locally in a pre-packaged meta.json found at the path.
+ /// If no file is found, no reconciliation occurs.
+ /// </summary>
+ /// <param name="manifest">The <see cref="PluginManifest"/> to reconcile against.</param>
+ /// <param name="path">The plugin path.</param>
+ /// <returns>The reconciled <see cref="PluginManifest"/>.</returns>
+ private async Task<bool> ReconcileManifest(PluginManifest manifest, string path)
+ {
+ try
+ {
+ var metafile = Path.Combine(path, MetafileName);
+ if (!File.Exists(metafile))
+ {
+ _logger.LogInformation("No local manifest exists for plugin {Plugin}. Skipping manifest reconciliation.", manifest.Name);
+ return true;
+ }
+
+ using var metaStream = File.OpenRead(metafile);
+ var localManifest = await JsonSerializer.DeserializeAsync<PluginManifest>(metaStream, _jsonOptions);
+ localManifest ??= new PluginManifest();
+
+ if (!Equals(localManifest.Id, manifest.Id))
+ {
+ _logger.LogError("The manifest ID {LocalUUID} did not match the package info ID {PackageUUID}.", localManifest.Id, manifest.Id);
+ manifest.Status = PluginStatus.Malfunctioned;
+ }
+
+ if (localManifest.Version != manifest.Version)
+ {
+ // Package information provides the version and is the source of truth. Pre-packages meta.json is assumed to be a mistake in this regard.
+ _logger.LogWarning("The version of the local manifest was {LocalVersion}, but {PackageVersion} was expected. The value will be replaced.", localManifest.Version, manifest.Version);
+ }
+
+ // Explicitly mapping properties instead of using reflection is preferred here.
+ manifest.Category = string.IsNullOrEmpty(localManifest.Category) ? manifest.Category : localManifest.Category;
+ manifest.AutoUpdate = localManifest.AutoUpdate; // Preserve whatever is local. Package info does not have this property.
+ manifest.Changelog = string.IsNullOrEmpty(localManifest.Changelog) ? manifest.Changelog : localManifest.Changelog;
+ manifest.Description = string.IsNullOrEmpty(localManifest.Description) ? manifest.Description : localManifest.Description;
+ manifest.Name = string.IsNullOrEmpty(localManifest.Name) ? manifest.Name : localManifest.Name;
+ manifest.Overview = string.IsNullOrEmpty(localManifest.Overview) ? manifest.Overview : localManifest.Overview;
+ manifest.Owner = string.IsNullOrEmpty(localManifest.Owner) ? manifest.Owner : localManifest.Owner;
+ manifest.TargetAbi = string.IsNullOrEmpty(localManifest.TargetAbi) ? manifest.TargetAbi : localManifest.TargetAbi;
+ manifest.Timestamp = localManifest.Timestamp.Equals(default) ? manifest.Timestamp : localManifest.Timestamp;
+ manifest.ImagePath = string.IsNullOrEmpty(localManifest.ImagePath) ? manifest.ImagePath : localManifest.ImagePath;
+ manifest.Assemblies = localManifest.Assemblies;
+
+ return true;
+ }
+ catch (Exception e)
+ {
+ _logger.LogWarning(e, "Unable to reconcile plugin manifest due to an error. {Path}", path);
+ return false;
+ }
+ }
+
+ /// <summary>
/// Changes a plugin's load status.
/// </summary>
/// <param name="plugin">The <see cref="LocalPlugin"/> instance.</param>
@@ -571,7 +661,7 @@ namespace Emby.Server.Implementations.Plugins
{
Version? version;
PluginManifest? manifest = null;
- var metafile = Path.Combine(dir, "meta.json");
+ var metafile = Path.Combine(dir, MetafileName);
if (File.Exists(metafile))
{
// Only path where this stays null is when File.ReadAllBytes throws an IOException
@@ -665,7 +755,15 @@ namespace Emby.Server.Implementations.Plugins
var entry = versions[x];
if (!string.Equals(lastName, entry.Name, StringComparison.OrdinalIgnoreCase))
{
- entry.DllFiles = Directory.GetFiles(entry.Path, "*.dll", SearchOption.AllDirectories);
+ if (!TryGetPluginDlls(entry, out var allowedDlls))
+ {
+ _logger.LogError("One or more assembly paths was invalid. Marking plugin {Plugin} as \"Malfunctioned\".", entry.Name);
+ ChangePluginState(entry, PluginStatus.Malfunctioned);
+ continue;
+ }
+
+ entry.DllFiles = allowedDlls;
+
if (entry.IsEnabledAndSupported)
{
lastName = entry.Name;
@@ -712,6 +810,68 @@ namespace Emby.Server.Implementations.Plugins
}
/// <summary>
+ /// Attempts to retrieve valid DLLs from the plugin path. This method will consider the assembly whitelist
+ /// from the manifest.
+ /// </summary>
+ /// <remarks>
+ /// Loading DLLs from externally supplied paths introduces a path traversal risk. This method
+ /// uses a safelisting tactic of considering DLLs from the plugin directory and only using
+ /// the plugin's canonicalized assembly whitelist for comparison. See
+ /// <see href="https://owasp.org/www-community/attacks/Path_Traversal"/> for more details.
+ /// </remarks>
+ /// <param name="plugin">The plugin.</param>
+ /// <param name="whitelistedDlls">The whitelisted DLLs. If the method returns <see langword="false"/>, this will be empty.</param>
+ /// <returns>
+ /// <see langword="true"/> if all assemblies listed in the manifest were available in the plugin directory.
+ /// <see langword="false"/> if any assemblies were invalid or missing from the plugin directory.
+ /// </returns>
+ /// <exception cref="ArgumentNullException">If the <see cref="LocalPlugin"/> is null.</exception>
+ private bool TryGetPluginDlls(LocalPlugin plugin, out IReadOnlyList<string> whitelistedDlls)
+ {
+ ArgumentNullException.ThrowIfNull(nameof(plugin));
+
+ IReadOnlyList<string> pluginDlls = Directory.GetFiles(plugin.Path, "*.dll", SearchOption.AllDirectories);
+
+ whitelistedDlls = Array.Empty<string>();
+ if (pluginDlls.Count > 0 && plugin.Manifest.Assemblies.Count > 0)
+ {
+ _logger.LogInformation("Registering whitelisted assemblies for plugin \"{Plugin}\"...", plugin.Name);
+
+ var canonicalizedPaths = new List<string>();
+ foreach (var path in plugin.Manifest.Assemblies)
+ {
+ var canonicalized = Path.Combine(plugin.Path, path).Canonicalize();
+
+ // Ensure we stay in the plugin directory.
+ if (!canonicalized.StartsWith(plugin.Path.NormalizePath(), StringComparison.Ordinal))
+ {
+ _logger.LogError("Assembly path {Path} is not inside the plugin directory.", path);
+ return false;
+ }
+
+ canonicalizedPaths.Add(canonicalized);
+ }
+
+ var intersected = pluginDlls.Intersect(canonicalizedPaths).ToList();
+
+ if (intersected.Count != canonicalizedPaths.Count)
+ {
+ _logger.LogError("Plugin {Plugin} contained assembly paths that were not found in the directory.", plugin.Name);
+ return false;
+ }
+
+ whitelistedDlls = intersected;
+ }
+ else
+ {
+ // No whitelist, default to loading all DLLs in plugin directory.
+ whitelistedDlls = pluginDlls;
+ }
+
+ return true;
+ }
+
+ /// <summary>
/// Changes the status of the other versions of the plugin to "Superceded".
/// </summary>
/// <param name="plugin">The <see cref="LocalPlugin"/> that's master.</param>
diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
index ee9aa8569..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;
@@ -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();
@@ -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);
@@ -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;
diff --git a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
index 63f0beb10..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.
@@ -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)
{
@@ -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 abc203618..6ad6c4cbd 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
@@ -100,7 +100,6 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
EnableImages = false
},
SourceTypes = new SourceType[] { SourceType.Library },
- HasChapterImages = false,
IsVirtualItem = false
})
.OfType<Video>()
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs
new file mode 100644
index 000000000..f78fc6f97
--- /dev/null
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs
@@ -0,0 +1,119 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Collections;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
+
+/// <summary>
+/// Deletes Path references from collections that no longer exists.
+/// </summary>
+public class CleanupCollectionPathsTask : IScheduledTask
+{
+ private readonly ILocalizationManager _localization;
+ private readonly ICollectionManager _collectionManager;
+ private readonly ILogger<CleanupCollectionPathsTask> _logger;
+ private readonly IProviderManager _providerManager;
+ private readonly IFileSystem _fileSystem;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CleanupCollectionPathsTask"/> class.
+ /// </summary>
+ /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
+ /// <param name="collectionManager">Instance of the <see cref="ICollectionManager"/> interface.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="providerManager">The provider manager.</param>
+ /// <param name="fileSystem">The filesystem.</param>
+ public CleanupCollectionPathsTask(
+ ILocalizationManager localization,
+ ICollectionManager collectionManager,
+ ILogger<CleanupCollectionPathsTask> logger,
+ IProviderManager providerManager,
+ IFileSystem fileSystem)
+ {
+ _localization = localization;
+ _collectionManager = collectionManager;
+ _logger = logger;
+ _providerManager = providerManager;
+ _fileSystem = fileSystem;
+ }
+
+ /// <inheritdoc />
+ public string Name => _localization.GetLocalizedString("TaskCleanCollections");
+
+ /// <inheritdoc />
+ public string Key => "CleanCollections";
+
+ /// <inheritdoc />
+ public string Description => _localization.GetLocalizedString("TaskCleanCollectionsDescription");
+
+ /// <inheritdoc />
+ public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
+
+ /// <inheritdoc />
+ public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var collectionsFolder = await _collectionManager.GetCollectionsFolder(false).ConfigureAwait(false);
+ if (collectionsFolder is null)
+ {
+ _logger.LogDebug("There is no collection folder to be found");
+ return;
+ }
+
+ var collections = collectionsFolder.Children.OfType<BoxSet>().ToArray();
+ _logger.LogDebug("Found {CollectionLength} Boxsets", collections.Length);
+
+ var itemsToRemove = new List<LinkedChild>();
+ for (var index = 0; index < collections.Length; index++)
+ {
+ var collection = collections[index];
+ _logger.LogDebug("Check Boxset {CollectionName}", collection.Name);
+
+ foreach (var collectionLinkedChild in collection.LinkedChildren)
+ {
+ if (!File.Exists(collectionLinkedChild.Path))
+ {
+ _logger.LogInformation("Item in boxset {CollectionName} cannot be found at {ItemPath}", collection.Name, collectionLinkedChild.Path);
+ itemsToRemove.Add(collectionLinkedChild);
+ }
+ }
+
+ if (itemsToRemove.Count != 0)
+ {
+ _logger.LogDebug("Update Boxset {CollectionName}", collection.Name);
+ collection.LinkedChildren = collection.LinkedChildren.Except(itemsToRemove).ToArray();
+ await collection.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken)
+ .ConfigureAwait(false);
+
+ _providerManager.QueueRefresh(
+ collection.Id,
+ new MetadataRefreshOptions(new DirectoryService(_fileSystem))
+ {
+ ForceSave = true
+ },
+ RefreshPriority.High);
+
+ itemsToRemove.Clear();
+ }
+
+ progress.Report(100D / collections.Length * (index + 1));
+ }
+ }
+
+ /// <inheritdoc />
+ public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
+ {
+ return new[] { new TaskTriggerInfo() { Type = TaskTriggerInfo.TriggerStartup } };
+ }
+}
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index afa3721b8..5f6dc93fb 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -606,7 +606,7 @@ namespace Emby.Server.Implementations.Session
}
catch (Exception ex)
{
- _logger.LogDebug("Error calling OnPlaybackStopped", ex);
+ _logger.LogDebug(ex, "Error calling OnPlaybackStopped");
}
}
@@ -953,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");
}
}
diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
index aebb55907..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.
@@ -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);
diff --git a/Emby.Server.Implementations/Session/WebSocketController.cs b/Emby.Server.Implementations/Session/WebSocketController.cs
index 051fa5b3c..cf8e0fb00 100644
--- a/Emby.Server.Implementations/Session/WebSocketController.cs
+++ b/Emby.Server.Implementations/Session/WebSocketController.cs
@@ -7,8 +7,8 @@ using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.Net.WebSocketMessages;
using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Net;
using MediaBrowser.Model.Session;
using Microsoft.Extensions.Logging;
@@ -69,9 +69,7 @@ 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 is null)
{
@@ -79,7 +77,7 @@ namespace Emby.Server.Implementations.Session
}
return socket.SendAsync(
- new WebSocketMessage<T>
+ new OutboundWebSocketMessage<T>
{
Data = data,
MessageType = name,
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 7d7ea5810..da8f94932 100644
--- a/Emby.Server.Implementations/SyncPlay/Group.cs
+++ b/Emby.Server.Implementations/SyncPlay/Group.cs
@@ -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 63c4a1556..00c655634 100644
--- a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
+++ b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
@@ -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 967f90b55..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;
@@ -42,7 +40,7 @@ namespace Emby.Server.Implementations.TV
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)
@@ -91,7 +89,7 @@ namespace Emby.Server.Implementations.TV
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 is not null);
+ .Where(i => i is not null)!;
}
private static string GetUniqueSeriesKey(Episode episode)
@@ -185,7 +183,7 @@ 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)
{
@@ -209,7 +207,7 @@ namespace Emby.Server.Implementations.TV
var lastWatchedEpisode = _libraryManager.GetItemList(lastQuery).Cast<Episode>().FirstOrDefault();
- Episode GetEpisode()
+ Episode? GetEpisode()
{
var nextQuery = new InternalItemsQuery(user)
{
diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs
index 5e897833e..6c198b6f9 100644
--- a/Emby.Server.Implementations/Updates/InstallationManager.cs
+++ b/Emby.Server.Implementations/Updates/InstallationManager.cs
@@ -183,7 +183,7 @@ namespace Emby.Server.Implementations.Updates
var plugin = _pluginManager.GetPlugin(package.Id, version.VersionNumber);
if (plugin is not null)
{
- await _pluginManager.GenerateManifest(package, version.VersionNumber, plugin.Path, plugin.Manifest.Status).ConfigureAwait(false);
+ await _pluginManager.PopulateManifest(package, version.VersionNumber, plugin.Path, plugin.Manifest.Status).ConfigureAwait(false);
}
// Remove versions with a target ABI greater then the current application version.
@@ -555,7 +555,10 @@ namespace Emby.Server.Implementations.Updates
stream.Position = 0;
using var reader = new ZipArchive(stream);
reader.ExtractToDirectory(targetDir, true);
- await _pluginManager.GenerateManifest(package.PackageInfo, package.Version, targetDir, status).ConfigureAwait(false);
+
+ // Ensure we create one or populate existing ones with missing data.
+ await _pluginManager.PopulateManifest(package.PackageInfo, package.Version, targetDir, status);
+
_pluginManager.ImportPluginFrom(targetDir);
}