diff options
Diffstat (limited to 'Emby.Server.Implementations')
87 files changed, 838 insertions, 675 deletions
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 6fd152a42..903c31133 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -3,6 +3,7 @@ #pragma warning disable CS1591 using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; @@ -18,6 +19,7 @@ using Emby.Dlna; using Emby.Dlna.Main; using Emby.Dlna.Ssdp; using Emby.Drawing; +using Emby.Naming.Common; using Emby.Notifications; using Emby.Photos; using Emby.Server.Implementations.Archiving; @@ -56,6 +58,7 @@ using MediaBrowser.Common.Updates; using MediaBrowser.Controller; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Chapters; +using MediaBrowser.Controller.ClientEvent; using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dlna; @@ -117,7 +120,7 @@ namespace Emby.Server.Implementations /// <summary> /// The disposable parts. /// </summary> - private readonly List<IDisposable> _disposableParts = new List<IDisposable>(); + private readonly ConcurrentDictionary<IDisposable, byte> _disposableParts = new (); private readonly IFileSystem _fileSystemManager; private readonly IConfiguration _startupConfig; @@ -128,7 +131,6 @@ namespace Emby.Server.Implementations private List<Type> _creatingInstances; private IMediaEncoder _mediaEncoder; private ISessionManager _sessionManager; - private string[] _urlPrefixes; /// <summary> /// Gets or sets all concrete types. @@ -147,25 +149,20 @@ namespace Emby.Server.Implementations /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param> /// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param> /// <param name="startupConfig">The <see cref="IConfiguration" /> interface.</param> - /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> - /// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param> public ApplicationHost( IServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IStartupOptions options, - IConfiguration startupConfig, - IFileSystem fileSystem, - IServiceCollection serviceCollection) + IConfiguration startupConfig) { ApplicationPaths = applicationPaths; LoggerFactory = loggerFactory; _startupOptions = options; _startupConfig = startupConfig; - _fileSystemManager = fileSystem; - ServiceCollection = serviceCollection; + _fileSystemManager = new ManagedFileSystem(LoggerFactory.CreateLogger<ManagedFileSystem>(), applicationPaths); Logger = LoggerFactory.CreateLogger<ApplicationHost>(); - fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem)); + _fileSystemManager.AddShortcutHandler(new MbLinkShortcutHandler(_fileSystemManager)); ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version; ApplicationVersionString = ApplicationVersion.ToString(3); @@ -214,7 +211,7 @@ namespace Emby.Server.Implementations /// <summary> /// Gets the <see cref="INetworkManager"/> singleton instance. /// </summary> - public INetworkManager NetManager { get; internal set; } + public INetworkManager NetManager { get; private set; } /// <summary> /// Gets a value indicating whether this instance has changes that require the entire application to restart. @@ -230,24 +227,22 @@ namespace Emby.Server.Implementations /// </summary> protected ILogger<ApplicationHost> Logger { get; } - protected IServiceCollection ServiceCollection { get; } - /// <summary> /// Gets the logger factory. /// </summary> protected ILoggerFactory LoggerFactory { get; } /// <summary> - /// Gets or sets the application paths. + /// Gets the application paths. /// </summary> /// <value>The application paths.</value> - protected IServerApplicationPaths ApplicationPaths { get; set; } + protected IServerApplicationPaths ApplicationPaths { get; } /// <summary> - /// Gets or sets the configuration manager. + /// Gets the configuration manager. /// </summary> /// <value>The configuration manager.</value> - public ServerConfigurationManager ConfigurationManager { get; set; } + public ServerConfigurationManager ConfigurationManager { get; } /// <summary> /// Gets or sets the service provider. @@ -351,22 +346,6 @@ namespace Emby.Server.Implementations } /// <summary> - /// Creates an instance of type and resolves all constructor dependencies. - /// </summary> - /// <param name="type">The type.</param> - /// <returns>System.Object.</returns> - public object CreateInstance(Type type) - => ActivatorUtilities.CreateInstance(ServiceProvider, type); - - /// <summary> - /// Creates an instance of type and resolves all constructor dependencies. - /// </summary> - /// <typeparam name="T">The type.</typeparam> - /// <returns>T.</returns> - public T CreateInstance<T>() - => ActivatorUtilities.CreateInstance<T>(ServiceProvider); - - /// <summary> /// Creates the instance safe. /// </summary> /// <param name="type">The type.</param> @@ -375,7 +354,7 @@ namespace Emby.Server.Implementations { _creatingInstances ??= new List<Type>(); - if (_creatingInstances.IndexOf(type) != -1) + if (_creatingInstances.Contains(type)) { Logger.LogError("DI Loop detected in the attempted creation of {Type}", type.FullName); foreach (var entry in _creatingInstances) @@ -385,7 +364,7 @@ namespace Emby.Server.Implementations _pluginManager.FailPlugin(type.Assembly); - throw new ExternalException("DI Loop detected."); + throw new TypeLoadException("DI Loop detected"); } try @@ -418,8 +397,15 @@ namespace Emby.Server.Implementations public IEnumerable<Type> GetExportTypes<T>() { var currentType = typeof(T); - - return _allConcreteTypes.Where(i => currentType.IsAssignableFrom(i)); + var numberOfConcreteTypes = _allConcreteTypes.Length; + for (var i = 0; i < numberOfConcreteTypes; i++) + { + var type = _allConcreteTypes[i]; + if (currentType.IsAssignableFrom(type)) + { + yield return type; + } + } } /// <inheritdoc /> @@ -434,9 +420,9 @@ namespace Emby.Server.Implementations if (manageLifetime) { - lock (_disposableParts) + foreach (var part in parts.OfType<IDisposable>()) { - _disposableParts.AddRange(parts.OfType<IDisposable>()); + _disposableParts.TryAdd(part, byte.MinValue); } } @@ -455,9 +441,9 @@ namespace Emby.Server.Implementations if (manageLifetime) { - lock (_disposableParts) + foreach (var part in parts.OfType<IDisposable>()) { - _disposableParts.AddRange(parts.OfType<IDisposable>()); + _disposableParts.TryAdd(part, byte.MinValue); } } @@ -521,7 +507,7 @@ namespace Emby.Server.Implementations } /// <inheritdoc/> - public void Init() + public void Init(IServiceCollection serviceCollection) { DiscoverTypes(); @@ -551,128 +537,130 @@ namespace Emby.Server.Implementations CertificatePath = networkConfiguration.CertificatePath; Certificate = GetCertificate(CertificatePath, networkConfiguration.CertificatePassword); - RegisterServices(); + RegisterServices(serviceCollection); - _pluginManager.RegisterServices(ServiceCollection); + _pluginManager.RegisterServices(serviceCollection); } /// <summary> /// Registers services/resources with the service collection that will be available via DI. /// </summary> - protected virtual void RegisterServices() + /// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param> + protected virtual void RegisterServices(IServiceCollection serviceCollection) { - ServiceCollection.AddSingleton(_startupOptions); + serviceCollection.AddSingleton(_startupOptions); - ServiceCollection.AddMemoryCache(); + serviceCollection.AddMemoryCache(); - ServiceCollection.AddSingleton<IServerConfigurationManager>(ConfigurationManager); - ServiceCollection.AddSingleton<IConfigurationManager>(ConfigurationManager); - ServiceCollection.AddSingleton<IApplicationHost>(this); - ServiceCollection.AddSingleton<IPluginManager>(_pluginManager); - ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths); + serviceCollection.AddSingleton<IServerConfigurationManager>(ConfigurationManager); + serviceCollection.AddSingleton<IConfigurationManager>(ConfigurationManager); + serviceCollection.AddSingleton<IApplicationHost>(this); + serviceCollection.AddSingleton(_pluginManager); + serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths); - ServiceCollection.AddSingleton(_fileSystemManager); - ServiceCollection.AddSingleton<TmdbClientManager>(); + serviceCollection.AddSingleton(_fileSystemManager); + serviceCollection.AddSingleton<TmdbClientManager>(); - ServiceCollection.AddSingleton(NetManager); + serviceCollection.AddSingleton(NetManager); - ServiceCollection.AddSingleton<ITaskManager, TaskManager>(); + serviceCollection.AddSingleton<ITaskManager, TaskManager>(); - ServiceCollection.AddSingleton(_xmlSerializer); + serviceCollection.AddSingleton(_xmlSerializer); - ServiceCollection.AddSingleton<IStreamHelper, StreamHelper>(); + serviceCollection.AddSingleton<IStreamHelper, StreamHelper>(); - ServiceCollection.AddSingleton<ICryptoProvider, CryptographyProvider>(); + serviceCollection.AddSingleton<ICryptoProvider, CryptographyProvider>(); - ServiceCollection.AddSingleton<ISocketFactory, SocketFactory>(); + serviceCollection.AddSingleton<ISocketFactory, SocketFactory>(); - ServiceCollection.AddSingleton<IInstallationManager, InstallationManager>(); + serviceCollection.AddSingleton<IInstallationManager, InstallationManager>(); - ServiceCollection.AddSingleton<IZipClient, ZipClient>(); + serviceCollection.AddSingleton<IZipClient, ZipClient>(); - ServiceCollection.AddSingleton<IServerApplicationHost>(this); - ServiceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths); + serviceCollection.AddSingleton<IServerApplicationHost>(this); + serviceCollection.AddSingleton(ApplicationPaths); - ServiceCollection.AddSingleton<ILocalizationManager, LocalizationManager>(); + serviceCollection.AddSingleton<ILocalizationManager, LocalizationManager>(); - ServiceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>(); + serviceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>(); - ServiceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>(); - ServiceCollection.AddSingleton<IUserDataManager, UserDataManager>(); + serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>(); + serviceCollection.AddSingleton<IUserDataManager, UserDataManager>(); - ServiceCollection.AddSingleton<IItemRepository, SqliteItemRepository>(); + serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>(); - ServiceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>(); - ServiceCollection.AddSingleton<EncodingHelper>(); + serviceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>(); + serviceCollection.AddSingleton<EncodingHelper>(); // TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required - ServiceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>)); - ServiceCollection.AddTransient(provider => new Lazy<IProviderManager>(provider.GetRequiredService<IProviderManager>)); - ServiceCollection.AddTransient(provider => new Lazy<IUserViewManager>(provider.GetRequiredService<IUserViewManager>)); - ServiceCollection.AddSingleton<ILibraryManager, LibraryManager>(); + serviceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>)); + serviceCollection.AddTransient(provider => new Lazy<IProviderManager>(provider.GetRequiredService<IProviderManager>)); + serviceCollection.AddTransient(provider => new Lazy<IUserViewManager>(provider.GetRequiredService<IUserViewManager>)); + serviceCollection.AddSingleton<ILibraryManager, LibraryManager>(); + serviceCollection.AddSingleton<NamingOptions>(); - ServiceCollection.AddSingleton<IMusicManager, MusicManager>(); + serviceCollection.AddSingleton<IMusicManager, MusicManager>(); - ServiceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>(); + serviceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>(); - ServiceCollection.AddSingleton<ISearchEngine, SearchEngine>(); + serviceCollection.AddSingleton<ISearchEngine, SearchEngine>(); - ServiceCollection.AddSingleton<IWebSocketManager, WebSocketManager>(); + serviceCollection.AddSingleton<IWebSocketManager, WebSocketManager>(); - ServiceCollection.AddSingleton<IImageProcessor, ImageProcessor>(); + serviceCollection.AddSingleton<IImageProcessor, ImageProcessor>(); - ServiceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>(); + serviceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>(); - ServiceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>(); + serviceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>(); - ServiceCollection.AddSingleton<ISubtitleManager, SubtitleManager>(); + serviceCollection.AddSingleton<ISubtitleManager, SubtitleManager>(); - ServiceCollection.AddSingleton<IProviderManager, ProviderManager>(); + serviceCollection.AddSingleton<IProviderManager, ProviderManager>(); // TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required - ServiceCollection.AddTransient(provider => new Lazy<ILiveTvManager>(provider.GetRequiredService<ILiveTvManager>)); - ServiceCollection.AddSingleton<IDtoService, DtoService>(); - - ServiceCollection.AddSingleton<IChannelManager, ChannelManager>(); + serviceCollection.AddTransient(provider => new Lazy<ILiveTvManager>(provider.GetRequiredService<ILiveTvManager>)); + serviceCollection.AddSingleton<IDtoService, DtoService>(); - ServiceCollection.AddSingleton<ISessionManager, SessionManager>(); + serviceCollection.AddSingleton<IChannelManager, ChannelManager>(); - ServiceCollection.AddSingleton<IDlnaManager, DlnaManager>(); + serviceCollection.AddSingleton<ISessionManager, SessionManager>(); - ServiceCollection.AddSingleton<ICollectionManager, CollectionManager>(); + serviceCollection.AddSingleton<IDlnaManager, DlnaManager>(); - ServiceCollection.AddSingleton<IPlaylistManager, PlaylistManager>(); + serviceCollection.AddSingleton<ICollectionManager, CollectionManager>(); - ServiceCollection.AddSingleton<ISyncPlayManager, SyncPlayManager>(); + serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>(); - ServiceCollection.AddSingleton<LiveTvDtoService>(); - ServiceCollection.AddSingleton<ILiveTvManager, LiveTvManager>(); + serviceCollection.AddSingleton<ISyncPlayManager, SyncPlayManager>(); - ServiceCollection.AddSingleton<IUserViewManager, UserViewManager>(); + serviceCollection.AddSingleton<LiveTvDtoService>(); + serviceCollection.AddSingleton<ILiveTvManager, LiveTvManager>(); - ServiceCollection.AddSingleton<INotificationManager, NotificationManager>(); + serviceCollection.AddSingleton<IUserViewManager, UserViewManager>(); - ServiceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>(); + serviceCollection.AddSingleton<INotificationManager, NotificationManager>(); - ServiceCollection.AddSingleton<IChapterManager, ChapterManager>(); + serviceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>(); - ServiceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>(); + serviceCollection.AddSingleton<IChapterManager, ChapterManager>(); - ServiceCollection.AddScoped<ISessionContext, SessionContext>(); + serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>(); - ServiceCollection.AddSingleton<IAuthService, AuthService>(); - ServiceCollection.AddSingleton<IQuickConnect, QuickConnectManager>(); + serviceCollection.AddScoped<ISessionContext, SessionContext>(); - ServiceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>(); + serviceCollection.AddSingleton<IAuthService, AuthService>(); + serviceCollection.AddSingleton<IQuickConnect, QuickConnectManager>(); - ServiceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>(); + serviceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>(); - ServiceCollection.AddSingleton<TranscodingJobHelper>(); - ServiceCollection.AddScoped<MediaInfoHelper>(); - ServiceCollection.AddScoped<AudioHelper>(); - ServiceCollection.AddScoped<DynamicHlsHelper>(); + serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>(); - ServiceCollection.AddSingleton<IDirectoryService, DirectoryService>(); + serviceCollection.AddSingleton<TranscodingJobHelper>(); + serviceCollection.AddScoped<MediaInfoHelper>(); + serviceCollection.AddScoped<AudioHelper>(); + serviceCollection.AddScoped<DynamicHlsHelper>(); + serviceCollection.AddScoped<IClientEventLogger, ClientEventLogger>(); + serviceCollection.AddSingleton<IDirectoryService, DirectoryService>(); } /// <summary> @@ -795,8 +783,6 @@ namespace Emby.Server.Implementations _pluginManager.CreatePlugins(); - _urlPrefixes = GetUrlPrefixes().ToArray(); - Resolve<ILibraryManager>().AddParts( GetExports<IResolverIgnoreRule>(), GetExports<IItemResolver>(), @@ -864,32 +850,12 @@ namespace Emby.Server.Implementations } } - private IEnumerable<string> GetUrlPrefixes() - { - var hosts = new[] { "+" }; - - return hosts.SelectMany(i => - { - var prefixes = new List<string> - { - "http://" + i + ":" + HttpPort + "/" - }; - - if (Certificate != null) - { - prefixes.Add("https://" + i + ":" + HttpsPort + "/"); - } - - return prefixes; - }); - } - /// <summary> /// Called when [configuration updated]. /// </summary> /// <param name="sender">The sender.</param> /// <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param> - protected void OnConfigurationUpdated(object sender, EventArgs e) + private void OnConfigurationUpdated(object sender, EventArgs e) { var requiresRestart = false; var networkConfiguration = ConfigurationManager.GetNetworkConfiguration(); @@ -898,8 +864,8 @@ namespace Emby.Server.Implementations if (HttpPort != 0 && HttpsPort != 0) { // Need to restart if ports have changed - if (networkConfiguration.HttpServerPortNumber != HttpPort || - networkConfiguration.HttpsPortNumber != HttpsPort) + if (networkConfiguration.HttpServerPortNumber != HttpPort + || networkConfiguration.HttpsPortNumber != HttpsPort) { if (ConfigurationManager.Configuration.IsPortAuthorized) { @@ -911,11 +877,6 @@ namespace Emby.Server.Implementations } } - if (!_urlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase)) - { - requiresRestart = true; - } - if (ValidateSslCertificate(networkConfiguration)) { requiresRestart = true; @@ -957,7 +918,7 @@ namespace Emby.Server.Implementations } /// <summary> - /// Notifies that the kernel that a change has been made that requires a restart. + /// Notifies the kernel that a change has been made that requires a restart. /// </summary> public void NotifyPendingRestart() { @@ -1098,11 +1059,6 @@ namespace Emby.Server.Implementations }; } - public IEnumerable<WakeOnLanInfo> GetWakeOnLanInfo() - => NetManager.GetMacAddresses() - .Select(i => new WakeOnLanInfo(i)) - .ToList(); - public PublicSystemInfo GetPublicSystemInfo(HttpRequest request) { return new PublicSystemInfo @@ -1118,7 +1074,7 @@ namespace Emby.Server.Implementations } /// <inheritdoc/> - public string GetSmartApiUrl(IPAddress remoteAddr, int? port = null) + public string GetSmartApiUrl(IPAddress remoteAddr) { // Published server ends with a / if (!string.IsNullOrEmpty(PublishedServerUrl)) @@ -1127,18 +1083,12 @@ namespace Emby.Server.Implementations return PublishedServerUrl.Trim('/'); } - string smart = NetManager.GetBindInterface(remoteAddr, out port); - // If the smartAPI doesn't start with http then treat it as a host or ip. - if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase)) - { - return smart.Trim('/'); - } - + string smart = NetManager.GetBindInterface(remoteAddr, out var port); return GetLocalApiUrl(smart.Trim('/'), null, port); } /// <inheritdoc/> - public string GetSmartApiUrl(HttpRequest request, int? port = null) + public string GetSmartApiUrl(HttpRequest request) { // Return the host in the HTTP request as the API url if (ConfigurationManager.GetNetworkConfiguration().EnablePublishedServerUriByRequest) @@ -1159,18 +1109,12 @@ namespace Emby.Server.Implementations return PublishedServerUrl.Trim('/'); } - string smart = NetManager.GetBindInterface(request, out port); - // If the smartAPI doesn't start with http then treat it as a host or ip. - if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase)) - { - return smart.Trim('/'); - } - + string smart = NetManager.GetBindInterface(request, out var port); return GetLocalApiUrl(smart.Trim('/'), request.Scheme, port); } /// <inheritdoc/> - public string GetSmartApiUrl(string hostname, int? port = null) + public string GetSmartApiUrl(string hostname) { // Published server ends with a / if (!string.IsNullOrEmpty(PublishedServerUrl)) @@ -1179,31 +1123,29 @@ namespace Emby.Server.Implementations return PublishedServerUrl.Trim('/'); } - string smart = NetManager.GetBindInterface(hostname, out port); - - // If the smartAPI doesn't start with http then treat it as a host or ip. - if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase)) - { - return smart.Trim('/'); - } - + string smart = NetManager.GetBindInterface(hostname, out var port); return GetLocalApiUrl(smart.Trim('/'), null, port); } /// <inheritdoc/> - public string GetLoopbackHttpApiUrl() + public string GetApiUrlForLocalAccess(bool allowHttps = true) { - if (NetManager.IsIP6Enabled) - { - return GetLocalApiUrl("::1", Uri.UriSchemeHttp, HttpPort); - } - - return GetLocalApiUrl("127.0.0.1", Uri.UriSchemeHttp, HttpPort); + // With an empty source, the port will be null + string smart = NetManager.GetBindInterface(string.Empty, out _); + var scheme = !allowHttps ? Uri.UriSchemeHttp : null; + int? port = !allowHttps ? HttpPort : null; + return GetLocalApiUrl(smart.Trim('/'), scheme, port); } /// <inheritdoc/> public string GetLocalApiUrl(string hostname, string scheme = null, int? port = null) { + // If the smartAPI doesn't start with http then treat it as a host or ip. + if (hostname.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + { + return hostname.TrimEnd('/'); + } + // NOTE: If no BaseUrl is set then UriBuilder appends a trailing slash, but if there is no BaseUrl it does // not. For consistency, always trim the trailing slash. return new UriBuilder @@ -1277,12 +1219,15 @@ namespace Emby.Server.Implementations Logger.LogInformation("Disposing {Type}", type.Name); - var parts = _disposableParts.Distinct().Where(i => i.GetType() != type).ToList(); - _disposableParts.Clear(); - - foreach (var part in parts) + foreach (var (part, _) in _disposableParts) { - Logger.LogInformation("Disposing {Type}", part.GetType().Name); + var partType = part.GetType(); + if (partType == type) + { + continue; + } + + Logger.LogInformation("Disposing {Type}", partType.Name); try { @@ -1290,9 +1235,11 @@ namespace Emby.Server.Implementations } catch (Exception ex) { - Logger.LogError(ex, "Error disposing {Type}", part.GetType().Name); + Logger.LogError(ex, "Error disposing {Type}", partType.Name); } } + + _disposableParts.Clear(); } _disposed = true; diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs index 09aee602a..f65eaec1c 100644 --- a/Emby.Server.Implementations/Channels/ChannelManager.cs +++ b/Emby.Server.Implementations/Channels/ChannelManager.cs @@ -1075,14 +1075,6 @@ namespace Emby.Server.Implementations.Channels forceUpdate = true; } - // was used for status - // if (!string.Equals(item.ExternalEtag ?? string.Empty, info.Etag ?? string.Empty, StringComparison.Ordinal)) - // { - // item.ExternalEtag = info.Etag; - // forceUpdate = true; - // _logger.LogDebug("Forcing update due to ExternalEtag {0}", item.Name); - // } - if (!internalChannelId.Equals(item.ChannelId)) { forceUpdate = true; diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs index 673810c49..e9c005cea 100644 --- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs +++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Security.Cryptography; +using System.Text; using MediaBrowser.Common.Extensions; using MediaBrowser.Model.Cryptography; -using static MediaBrowser.Common.Cryptography.Constants; +using static MediaBrowser.Model.Cryptography.Constants; namespace Emby.Server.Implementations.Cryptography { @@ -12,10 +14,7 @@ namespace Emby.Server.Implementations.Cryptography /// </summary> public class CryptographyProvider : ICryptoProvider { - // FIXME: When we get DotNet Standard 2.1 we need to revisit how we do the crypto - // Currently supported hash methods from https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.cryptoconfig?view=netcore-2.1 - // there might be a better way to autogenerate this list as dotnet updates, but I couldn't find one - // Please note the default method of PBKDF2 is not included, it cannot be used to generate hashes cleanly as it is actually a pbkdf with sha1 + // TODO: remove when not needed for backwards compat private static readonly HashSet<string> _supportedHashMethods = new HashSet<string>() { "MD5", @@ -35,60 +34,81 @@ namespace Emby.Server.Implementations.Cryptography }; /// <inheritdoc /> - public string DefaultHashMethod => "PBKDF2"; + public string DefaultHashMethod => "PBKDF2-SHA512"; /// <inheritdoc /> - public IEnumerable<string> GetSupportedHashMethods() - => _supportedHashMethods; - - private byte[] PBKDF2(string method, byte[] bytes, byte[] salt, int iterations) + public PasswordHash CreatePasswordHash(ReadOnlySpan<char> password) { - // downgrading for now as we need this library to be dotnetstandard compliant - // with this downgrade we'll add a check to make sure we're on the downgrade method at the moment - if (method != DefaultHashMethod) - { - throw new CryptographicException($"Cannot currently use PBKDF2 with requested hash method: {method}"); - } - - using var r = new Rfc2898DeriveBytes(bytes, salt, iterations); - return r.GetBytes(32); + byte[] salt = GenerateSalt(); + return new PasswordHash( + DefaultHashMethod, + Rfc2898DeriveBytes.Pbkdf2( + password, + salt, + DefaultIterations, + HashAlgorithmName.SHA512, + DefaultOutputLength), + salt, + new Dictionary<string, string> + { + { "iterations", DefaultIterations.ToString(CultureInfo.InvariantCulture) } + }); } /// <inheritdoc /> - public byte[] ComputeHash(string hashMethod, byte[] bytes, byte[] salt) + public bool Verify(PasswordHash hash, ReadOnlySpan<char> password) { - if (hashMethod == DefaultHashMethod) + if (string.Equals(hash.Id, "PBKDF2", StringComparison.Ordinal)) { - return PBKDF2(hashMethod, bytes, salt, DefaultIterations); + return hash.Hash.SequenceEqual( + Rfc2898DeriveBytes.Pbkdf2( + password, + hash.Salt, + int.Parse(hash.Parameters["iterations"], CultureInfo.InvariantCulture), + HashAlgorithmName.SHA1, + 32)); } - if (!_supportedHashMethods.Contains(hashMethod)) + if (string.Equals(hash.Id, "PBKDF2-SHA512", StringComparison.Ordinal)) { - throw new CryptographicException($"Requested hash method is not supported: {hashMethod}"); + return hash.Hash.SequenceEqual( + Rfc2898DeriveBytes.Pbkdf2( + password, + hash.Salt, + int.Parse(hash.Parameters["iterations"], CultureInfo.InvariantCulture), + HashAlgorithmName.SHA512, + DefaultOutputLength)); } - using var h = HashAlgorithm.Create(hashMethod) ?? throw new ResourceNotFoundException($"Unknown hash method: {hashMethod}."); - if (salt.Length == 0) + if (!_supportedHashMethods.Contains(hash.Id)) { - return h.ComputeHash(bytes); + throw new CryptographicException($"Requested hash method is not supported: {hash.Id}"); } - byte[] salted = new byte[bytes.Length + salt.Length]; + using var h = HashAlgorithm.Create(hash.Id) ?? throw new ResourceNotFoundException($"Unknown hash method: {hash.Id}."); + var bytes = Encoding.UTF8.GetBytes(password.ToArray()); + if (hash.Salt.Length == 0) + { + return hash.Hash.SequenceEqual(h.ComputeHash(bytes)); + } + + byte[] salted = new byte[bytes.Length + hash.Salt.Length]; Array.Copy(bytes, salted, bytes.Length); - Array.Copy(salt, 0, salted, bytes.Length, salt.Length); - return h.ComputeHash(salted); + hash.Salt.CopyTo(salted.AsSpan(bytes.Length)); + return hash.Hash.SequenceEqual(h.ComputeHash(salted)); } /// <inheritdoc /> - public byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt) - => PBKDF2(DefaultHashMethod, bytes, salt, DefaultIterations); - - /// <inheritdoc /> public byte[] GenerateSalt() => GenerateSalt(DefaultSaltLength); /// <inheritdoc /> public byte[] GenerateSalt(int length) - => RandomNumberGenerator.GetBytes(length); + { + var salt = new byte[length]; + using var rng = RandomNumberGenerator.Create(); + rng.GetNonZeroBytes(salt); + return salt; + } } } diff --git a/Emby.Server.Implementations/Data/ManagedConnection.cs b/Emby.Server.Implementations/Data/ManagedConnection.cs index 44dad5b17..11e33278d 100644 --- a/Emby.Server.Implementations/Data/ManagedConnection.cs +++ b/Emby.Server.Implementations/Data/ManagedConnection.cs @@ -7,7 +7,7 @@ using SQLitePCL.pretty; namespace Emby.Server.Implementations.Data { - public class ManagedConnection : IDisposable + public sealed class ManagedConnection : IDisposable { private readonly SemaphoreSlim _writeLock; diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index ad76f3d6d..b91ff6408 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -134,14 +134,11 @@ namespace Emby.Server.Implementations.Dto var dto = GetBaseItemDtoInternal(item, options, user, owner); if (item is LiveTvChannel tvChannel) { - var list = new List<(BaseItemDto, LiveTvChannel)>(1) { (dto, tvChannel) }; - LivetvManager.AddChannelInfo(list, options, user); + LivetvManager.AddChannelInfo(new[] { (dto, tvChannel) }, options, user); } else if (item is LiveTvProgram) { - var list = new List<(BaseItem, BaseItemDto)>(1) { (item, dto) }; - var task = LivetvManager.AddInfoToProgramDto(list, options.Fields, user); - Task.WaitAll(task); + LivetvManager.AddInfoToProgramDto(new[] { (item, dto) }, options.Fields, user).GetAwaiter().GetResult(); } if (item is IItemByName itemByName @@ -373,6 +370,12 @@ namespace Emby.Server.Implementations.Dto if (item is MusicAlbum || item is Season || item is Playlist) { dto.ChildCount = dto.RecursiveItemCount; + var folderChildCount = folder.LinkedChildren.Length; + // The default is an empty array, so we can't reliably use the count when it's empty + if (folderChildCount > 0) + { + dto.ChildCount ??= folderChildCount; + } } if (options.ContainsField(ItemFields.ChildCount)) @@ -497,7 +500,7 @@ namespace Emby.Server.Implementations.Dto } catch (Exception ex) { - _logger.LogError(ex, "Error getting {imageType} image info for {path}", image.Type, image.Path); + _logger.LogError(ex, "Error getting {ImageType} image info for {Path}", image.Type, image.Path); return null; } } @@ -755,15 +758,6 @@ namespace Emby.Server.Implementations.Dto dto.BackdropImageTags = GetTagsAndFillBlurhashes(dto, item, ImageType.Backdrop, backdropLimit); } - if (options.ContainsField(ItemFields.ScreenshotImageTags)) - { - var screenshotLimit = options.GetImageLimit(ImageType.Screenshot); - if (screenshotLimit > 0) - { - dto.ScreenshotImageTags = GetTagsAndFillBlurhashes(dto, item, ImageType.Screenshot, screenshotLimit); - } - } - if (options.ContainsField(ItemFields.Genres)) { dto.Genres = item.Genres; diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index c1ce4b557..95acd216d 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -25,14 +25,14 @@ <ItemGroup> <PackageReference Include="DiscUtils.Udf" Version="0.16.13" /> <PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" /> - <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0-rc.2*" /> - <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.0-rc.2*" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0-rc.2*" /> - <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0-rc.2*" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.0-rc.2*" /> - <PackageReference Include="Mono.Nat" Version="3.0.1" /> + <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" /> + <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.0" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" /> + <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.0" /> + <PackageReference Include="Mono.Nat" Version="3.0.2" /> <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.2" /> - <PackageReference Include="sharpcompress" Version="0.30.0" /> + <PackageReference Include="sharpcompress" Version="0.30.1" /> <PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" /> <PackageReference Include="DotNet.Glob" Version="3.1.3" /> </ItemGroup> diff --git a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs index 640754af4..d325fa14f 100644 --- a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs +++ b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs @@ -27,7 +27,6 @@ namespace Emby.Server.Implementations.EntryPoints private readonly IServerApplicationHost _appHost; private readonly ILogger<ExternalPortForwarding> _logger; private readonly IServerConfigurationManager _config; - private readonly IDeviceDiscovery _deviceDiscovery; private readonly ConcurrentDictionary<IPEndPoint, byte> _createdRules = new ConcurrentDictionary<IPEndPoint, byte>(); @@ -42,17 +41,14 @@ namespace Emby.Server.Implementations.EntryPoints /// <param name="logger">The logger.</param> /// <param name="appHost">The application host.</param> /// <param name="config">The configuration manager.</param> - /// <param name="deviceDiscovery">The device discovery.</param> public ExternalPortForwarding( ILogger<ExternalPortForwarding> logger, IServerApplicationHost appHost, - IServerConfigurationManager config, - IDeviceDiscovery deviceDiscovery) + IServerConfigurationManager config) { _logger = logger; _appHost = appHost; _config = config; - _deviceDiscovery = deviceDiscovery; } private string GetConfigIdentifier() diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs index e2ad07177..e7103ec95 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs @@ -24,7 +24,7 @@ namespace Emby.Server.Implementations.HttpServer.Security if (!auth.HasToken) { - throw new AuthenticationException("Request does not contain a token."); + return auth; } if (!auth.IsAuthenticated) diff --git a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs index f86bfd755..e99876dce 100644 --- a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs +++ b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs @@ -35,7 +35,12 @@ namespace Emby.Server.Implementations.HttpServer /// <inheritdoc /> public async Task WebSocketRequestHandler(HttpContext context) { - _ = await _authService.Authenticate(context.Request).ConfigureAwait(false); + var authorizationInfo = await _authService.Authenticate(context.Request).ConfigureAwait(false); + if (!authorizationInfo.IsAuthenticated) + { + throw new SecurityException("Token is required"); + } + try { _logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress); diff --git a/Emby.Server.Implementations/IO/FileRefresher.cs b/Emby.Server.Implementations/IO/FileRefresher.cs index 47a83d77c..e62361c1e 100644 --- a/Emby.Server.Implementations/IO/FileRefresher.cs +++ b/Emby.Server.Implementations/IO/FileRefresher.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -14,7 +12,7 @@ using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.IO { - public class FileRefresher : IDisposable + public sealed class FileRefresher : IDisposable { private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; @@ -22,7 +20,7 @@ namespace Emby.Server.Implementations.IO private readonly List<string> _affectedPaths = new List<string>(); private readonly object _timerLock = new object(); - private Timer _timer; + private Timer? _timer; private bool _disposed; public FileRefresher(string path, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger logger) @@ -36,7 +34,7 @@ namespace Emby.Server.Implementations.IO AddPath(path); } - public event EventHandler<EventArgs> Completed; + public event EventHandler<EventArgs>? Completed; public string Path { get; private set; } @@ -111,7 +109,7 @@ namespace Emby.Server.Implementations.IO RestartTimer(); } - private void OnTimerCallback(object state) + private void OnTimerCallback(object? state) { List<string> paths; @@ -127,7 +125,7 @@ namespace Emby.Server.Implementations.IO try { - ProcessPathChanges(paths.ToList()); + ProcessPathChanges(paths); } catch (Exception ex) { @@ -137,12 +135,12 @@ namespace Emby.Server.Implementations.IO private void ProcessPathChanges(List<string> paths) { - var itemsToRefresh = paths + IEnumerable<BaseItem> itemsToRefresh = paths .Distinct(StringComparer.OrdinalIgnoreCase) .Select(GetAffectedBaseItem) .Where(item => item != null) - .GroupBy(x => x.Id) - .Select(x => x.First()); + .GroupBy(x => x!.Id) // Removed null values in the previous .Where() + .Select(x => x.First())!; foreach (var item in itemsToRefresh) { @@ -176,15 +174,15 @@ namespace Emby.Server.Implementations.IO /// </summary> /// <param name="path">The path.</param> /// <returns>BaseItem.</returns> - private BaseItem GetAffectedBaseItem(string path) + private BaseItem? GetAffectedBaseItem(string path) { - BaseItem item = null; + BaseItem? item = null; while (item == null && !string.IsNullOrEmpty(path)) { item = _libraryManager.FindByPath(path, null); - path = System.IO.Path.GetDirectoryName(path); + path = System.IO.Path.GetDirectoryName(path) ?? string.Empty; } if (item != null) diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs index e9d069cd3..9fcc7fe59 100644 --- a/Emby.Server.Implementations/IO/LibraryMonitor.cs +++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs @@ -267,7 +267,7 @@ namespace Emby.Server.Implementations.IO if (_fileSystemWatchers.TryAdd(path, newWatcher)) { newWatcher.EnableRaisingEvents = true; - _logger.LogInformation("Watching directory " + path); + _logger.LogInformation("Watching directory {Path}", path); } else { @@ -276,7 +276,7 @@ namespace Emby.Server.Implementations.IO } catch (Exception ex) { - _logger.LogError(ex, "Error watching path: {path}", path); + _logger.LogError(ex, "Error watching path: {Path}", path); } }); } @@ -449,12 +449,12 @@ namespace Emby.Server.Implementations.IO } var newRefresher = new FileRefresher(path, _configurationManager, _libraryManager, _logger); - newRefresher.Completed += NewRefresher_Completed; + newRefresher.Completed += OnNewRefresherCompleted; _activeRefreshers.Add(newRefresher); } } - private void NewRefresher_Completed(object sender, EventArgs e) + private void OnNewRefresherCompleted(object sender, EventArgs e) { var refresher = (FileRefresher)sender; DisposeRefresher(refresher); @@ -481,6 +481,7 @@ namespace Emby.Server.Implementations.IO { lock (_activeRefreshers) { + refresher.Completed -= OnNewRefresherCompleted; refresher.Dispose(); _activeRefreshers.Remove(refresher); } @@ -492,6 +493,7 @@ namespace Emby.Server.Implementations.IO { foreach (var refresher in _activeRefreshers.ToList()) { + refresher.Completed -= OnNewRefresherCompleted; refresher.Dispose(); } diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index eeee28842..777cd2cd4 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -1,5 +1,3 @@ -#pragma warning disable CS1591 - using System; using System.Collections.Generic; using System.Globalization; @@ -23,6 +21,11 @@ namespace Emby.Server.Implementations.IO private readonly string _tempPath; private static readonly bool _isEnvironmentCaseInsensitive = OperatingSystem.IsWindows(); + /// <summary> + /// Initializes a new instance of the <see cref="ManagedFileSystem"/> class. + /// </summary> + /// <param name="logger">The <see cref="ILogger"/> instance to use.</param> + /// <param name="applicationPaths">The <see cref="IApplicationPaths"/> instance to use.</param> public ManagedFileSystem( ILogger<ManagedFileSystem> logger, IApplicationPaths applicationPaths) @@ -31,6 +34,7 @@ namespace Emby.Server.Implementations.IO _tempPath = applicationPaths.TempDirectory; } + /// <inheritdoc /> public virtual void AddShortcutHandler(IShortcutHandler handler) { _shortcutHandlers.Add(handler); @@ -72,6 +76,7 @@ namespace Emby.Server.Implementations.IO return handler?.Resolve(filename); } + /// <inheritdoc /> public virtual string MakeAbsolutePath(string folderPath, string filePath) { // path is actually a stream @@ -358,11 +363,13 @@ namespace Emby.Server.Implementations.IO return GetCreationTimeUtc(GetFileSystemInfo(path)); } + /// <inheritdoc /> public virtual DateTime GetCreationTimeUtc(FileSystemMetadata info) { return info.CreationTimeUtc; } + /// <inheritdoc /> public virtual DateTime GetLastWriteTimeUtc(FileSystemMetadata info) { return info.LastWriteTimeUtc; @@ -397,6 +404,7 @@ namespace Emby.Server.Implementations.IO return GetLastWriteTimeUtc(GetFileSystemInfo(path)); } + /// <inheritdoc /> public virtual void SetHidden(string path, bool isHidden) { if (!OperatingSystem.IsWindows()) @@ -421,6 +429,7 @@ namespace Emby.Server.Implementations.IO } } + /// <inheritdoc /> public virtual void SetAttributes(string path, bool isHidden, bool readOnly) { if (!OperatingSystem.IsWindows()) @@ -444,7 +453,7 @@ namespace Emby.Server.Implementations.IO if (readOnly) { - attributes = attributes | FileAttributes.ReadOnly; + attributes |= FileAttributes.ReadOnly; } else { @@ -453,7 +462,7 @@ namespace Emby.Server.Implementations.IO if (isHidden) { - attributes = attributes | FileAttributes.Hidden; + attributes |= FileAttributes.Hidden; } else { @@ -498,6 +507,7 @@ namespace Emby.Server.Implementations.IO File.Copy(temp1, file2, true); } + /// <inheritdoc /> public virtual bool ContainsSubPath(string parentPath, string path) { if (string.IsNullOrEmpty(parentPath)) @@ -515,6 +525,7 @@ namespace Emby.Server.Implementations.IO _isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); } + /// <inheritdoc /> public virtual string NormalizePath(string path) { if (string.IsNullOrEmpty(path)) @@ -530,6 +541,7 @@ namespace Emby.Server.Implementations.IO return Path.TrimEndingDirectorySeparator(path); } + /// <inheritdoc /> public virtual bool AreEqual(string path1, string path2) { if (path1 == null && path2 == null) @@ -548,6 +560,7 @@ namespace Emby.Server.Implementations.IO _isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); } + /// <inheritdoc /> public virtual string GetFileNameWithoutExtension(FileSystemMetadata info) { if (info.IsDirectory) @@ -558,11 +571,11 @@ namespace Emby.Server.Implementations.IO return Path.GetFileNameWithoutExtension(info.FullName); } + /// <inheritdoc /> public virtual bool IsPathFile(string path) { - // Cannot use Path.IsPathRooted because it returns false under mono when using windows-based paths, e.g. C:\\ - if (path.IndexOf("://", StringComparison.OrdinalIgnoreCase) != -1 && - !path.StartsWith("file://", StringComparison.OrdinalIgnoreCase)) + if (path.Contains("://", StringComparison.OrdinalIgnoreCase) + && !path.StartsWith("file://", StringComparison.OrdinalIgnoreCase)) { return false; } @@ -570,17 +583,23 @@ namespace Emby.Server.Implementations.IO return true; } + /// <inheritdoc /> public virtual void DeleteFile(string path) { SetAttributes(path, false, false); File.Delete(path); } + /// <inheritdoc /> public virtual List<FileSystemMetadata> GetDrives() { // check for ready state to avoid waiting for drives to timeout // some drives on linux have no actual size or are used for other purposes - return DriveInfo.GetDrives().Where(d => d.IsReady && d.TotalSize != 0 && d.DriveType != DriveType.Ram) + return DriveInfo.GetDrives() + .Where( + d => (d.DriveType == DriveType.Fixed || d.DriveType == DriveType.Network || d.DriveType == DriveType.Removable) + && d.IsReady + && d.TotalSize != 0) .Select(d => new FileSystemMetadata { Name = d.Name, @@ -589,16 +608,19 @@ namespace Emby.Server.Implementations.IO }).ToList(); } + /// <inheritdoc /> public virtual IEnumerable<FileSystemMetadata> GetDirectories(string path, bool recursive = false) { return ToMetadata(new DirectoryInfo(path).EnumerateDirectories("*", GetEnumerationOptions(recursive))); } + /// <inheritdoc /> public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, bool recursive = false) { return GetFiles(path, null, false, recursive); } + /// <inheritdoc /> public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive = false) { var enumerationOptions = GetEnumerationOptions(recursive); @@ -629,6 +651,7 @@ namespace Emby.Server.Implementations.IO return ToMetadata(files); } + /// <inheritdoc /> public virtual IEnumerable<FileSystemMetadata> GetFileSystemEntries(string path, bool recursive = false) { var directoryInfo = new DirectoryInfo(path); @@ -642,16 +665,19 @@ namespace Emby.Server.Implementations.IO return infos.Select(GetFileSystemMetadata); } + /// <inheritdoc /> public virtual IEnumerable<string> GetDirectoryPaths(string path, bool recursive = false) { return Directory.EnumerateDirectories(path, "*", GetEnumerationOptions(recursive)); } + /// <inheritdoc /> public virtual IEnumerable<string> GetFilePaths(string path, bool recursive = false) { return GetFilePaths(path, null, false, recursive); } + /// <inheritdoc /> public virtual IEnumerable<string> GetFilePaths(string path, string[]? extensions, bool enableCaseSensitiveExtensions, bool recursive = false) { var enumerationOptions = GetEnumerationOptions(recursive); @@ -682,6 +708,7 @@ namespace Emby.Server.Implementations.IO return files; } + /// <inheritdoc /> public virtual IEnumerable<string> GetFileSystemEntryPaths(string path, bool recursive = false) { return Directory.EnumerateFileSystemEntries(path, "*", GetEnumerationOptions(recursive)); diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs index bc5b4499f..29758a078 100644 --- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs @@ -1,8 +1,9 @@ using System; using System.IO; +using Emby.Naming.Audio; +using Emby.Naming.Common; using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Model.IO; @@ -13,17 +14,17 @@ namespace Emby.Server.Implementations.Library /// </summary> public class CoreResolutionIgnoreRule : IResolverIgnoreRule { - private readonly ILibraryManager _libraryManager; + private readonly NamingOptions _namingOptions; private readonly IServerApplicationPaths _serverApplicationPaths; /// <summary> /// Initializes a new instance of the <see cref="CoreResolutionIgnoreRule"/> class. /// </summary> - /// <param name="libraryManager">The library manager.</param> + /// <param name="namingOptions">The naming options.</param> /// <param name="serverApplicationPaths">The server application paths.</param> - public CoreResolutionIgnoreRule(ILibraryManager libraryManager, IServerApplicationPaths serverApplicationPaths) + public CoreResolutionIgnoreRule(NamingOptions namingOptions, IServerApplicationPaths serverApplicationPaths) { - _libraryManager = libraryManager; + _namingOptions = namingOptions; _serverApplicationPaths = serverApplicationPaths; } @@ -78,7 +79,7 @@ namespace Emby.Server.Implementations.Library { // Don't resolve these into audio files if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFileName, StringComparison.Ordinal) - && _libraryManager.IsAudioFile(filename)) + && AudioFileParser.IsAudioFile(filename, _namingOptions)) { return true; } diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 1326f60fe..778b6225e 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -79,6 +79,7 @@ namespace Emby.Server.Implementations.Library private readonly IFileSystem _fileSystem; private readonly IItemRepository _itemRepository; private readonly IImageProcessor _imageProcessor; + private readonly NamingOptions _namingOptions; /// <summary> /// The _root folder sync lock. @@ -88,9 +89,6 @@ namespace Emby.Server.Implementations.Library private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24); - private NamingOptions _namingOptions; - private string[] _videoFileExtensions; - /// <summary> /// The _root folder. /// </summary> @@ -116,6 +114,7 @@ namespace Emby.Server.Implementations.Library /// <param name="itemRepository">The item repository.</param> /// <param name="imageProcessor">The image processor.</param> /// <param name="memoryCache">The memory cache.</param> + /// <param name="namingOptions">The naming options.</param> public LibraryManager( IServerApplicationHost appHost, ILogger<LibraryManager> logger, @@ -130,7 +129,8 @@ namespace Emby.Server.Implementations.Library IMediaEncoder mediaEncoder, IItemRepository itemRepository, IImageProcessor imageProcessor, - IMemoryCache memoryCache) + IMemoryCache memoryCache, + NamingOptions namingOptions) { _appHost = appHost; _logger = logger; @@ -146,6 +146,7 @@ namespace Emby.Server.Implementations.Library _itemRepository = itemRepository; _imageProcessor = imageProcessor; _memoryCache = memoryCache; + _namingOptions = namingOptions; _configurationManager.ConfigurationUpdated += ConfigurationUpdated; @@ -333,8 +334,7 @@ namespace Emby.Server.Implementations.Library { try { - var task = BaseItem.ChannelManager.DeleteItem(item); - Task.WaitAll(task); + BaseItem.ChannelManager.DeleteItem(item).GetAwaiter().GetResult(); } catch (ArgumentException) { @@ -492,7 +492,7 @@ namespace Emby.Server.Implementations.Library } catch (Exception ex) { - _logger.LogError(ex, "Error in {resolver} resolving {path}", resolver.GetType().Name, args.Path); + _logger.LogError(ex, "Error in {Resolver} resolving {Path}", resolver.GetType().Name, args.Path); return null; } } @@ -799,7 +799,7 @@ namespace Emby.Server.Implementations.Library { var userRootPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath; - _logger.LogDebug("Creating userRootPath at {path}", userRootPath); + _logger.LogDebug("Creating userRootPath at {Path}", userRootPath); Directory.CreateDirectory(userRootPath); var newItemId = GetNewItemId(userRootPath, typeof(UserRootFolder)); @@ -810,7 +810,7 @@ namespace Emby.Server.Implementations.Library } catch (Exception ex) { - _logger.LogError(ex, "Error creating UserRootFolder {path}", newItemId); + _logger.LogError(ex, "Error creating UserRootFolder {Path}", newItemId); } if (tmpItem == null) @@ -827,7 +827,7 @@ namespace Emby.Server.Implementations.Library } _userRootFolder = tmpItem; - _logger.LogDebug("Setting userRootFolder: {folder}", _userRootFolder); + _logger.LogDebug("Setting userRootFolder: {Folder}", _userRootFolder); } } } @@ -1213,7 +1213,7 @@ namespace Emby.Server.Implementations.Library } catch (Exception ex) { - _logger.LogError(ex, "Error resolving shortcut file {file}", i); + _logger.LogError(ex, "Error resolving shortcut file {File}", i); return null; } }) @@ -1698,7 +1698,7 @@ namespace Emby.Server.Implementations.Library if (video == null) { - _logger.LogError("Intro resolver returned null for {path}.", info.Path); + _logger.LogError("Intro resolver returned null for {Path}.", info.Path); } else { @@ -1717,7 +1717,7 @@ namespace Emby.Server.Implementations.Library } catch (Exception ex) { - _logger.LogError(ex, "Error resolving path {path}.", info.Path); + _logger.LogError(ex, "Error resolving path {Path}.", info.Path); } } else @@ -2502,16 +2502,6 @@ namespace Emby.Server.Implementations.Library } /// <inheritdoc /> - public bool IsVideoFile(string path) - { - return VideoResolver.IsVideoFile(path, GetNamingOptions()); - } - - /// <inheritdoc /> - public bool IsAudioFile(string path) - => AudioFileParser.IsAudioFile(path, GetNamingOptions()); - - /// <inheritdoc /> public int? GetSeasonNumberFromPath(string path) => SeasonPathParser.Parse(path, true, true).SeasonNumber; @@ -2526,7 +2516,7 @@ namespace Emby.Server.Implementations.Library isAbsoluteNaming = null; } - var resolver = new EpisodeResolver(GetNamingOptions()); + var resolver = new EpisodeResolver(_namingOptions); var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd; @@ -2683,21 +2673,9 @@ namespace Emby.Server.Implementations.Library return changed; } - /// <inheritdoc /> - public NamingOptions GetNamingOptions() - { - if (_namingOptions == null) - { - _namingOptions = new NamingOptions(); - _videoFileExtensions = _namingOptions.VideoFileExtensions; - } - - return _namingOptions; - } - public ItemLookupInfo ParseName(string name) { - var namingOptions = GetNamingOptions(); + var namingOptions = _namingOptions; var result = VideoResolver.CleanDateTime(name, namingOptions); return new ItemLookupInfo @@ -2709,11 +2687,11 @@ namespace Emby.Server.Implementations.Library public IEnumerable<Video> FindTrailers(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService) { - var namingOptions = GetNamingOptions(); + var namingOptions = _namingOptions; var files = owner.IsInMixedFolder ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory) .Where(i => string.Equals(i.Name, BaseItem.TrailersFolderName, StringComparison.OrdinalIgnoreCase)) - .SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false)) + .SelectMany(i => _fileSystem.GetFiles(i.FullName, namingOptions.VideoFileExtensions, false, false)) .ToList(); var videos = VideoListResolver.Resolve(fileSystemChildren, namingOptions); @@ -2727,7 +2705,7 @@ namespace Emby.Server.Implementations.Library var resolvers = new IItemResolver[] { - new GenericVideoResolver<Trailer>(this) + new GenericVideoResolver<Trailer>(_namingOptions) }; return ResolvePaths(files, directoryService, null, new LibraryOptions(), null, resolvers) @@ -2753,11 +2731,11 @@ namespace Emby.Server.Implementations.Library public IEnumerable<Video> FindExtras(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService) { - var namingOptions = GetNamingOptions(); + var namingOptions = _namingOptions; var files = owner.IsInMixedFolder ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory) .Where(i => BaseItem.AllExtrasTypesFolderNames.ContainsKey(i.Name ?? string.Empty)) - .SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false)) + .SelectMany(i => _fileSystem.GetFiles(i.FullName, namingOptions.VideoFileExtensions, false, false)) .ToList(); var videos = VideoListResolver.Resolve(fileSystemChildren, namingOptions); @@ -2841,7 +2819,7 @@ namespace Emby.Server.Implementations.Library private void SetExtraTypeFromFilename(Video item) { - var resolver = new ExtraResolver(GetNamingOptions()); + var resolver = new ExtraResolver(_namingOptions); var result = resolver.GetExtraInfo(item.Path); diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index 351fced34..972d4ebbb 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -45,6 +45,7 @@ namespace Emby.Server.Implementations.Library private readonly IMediaEncoder _mediaEncoder; private readonly ILocalizationManager _localizationManager; private readonly IApplicationPaths _appPaths; + private readonly IDirectoryService _directoryService; private readonly ConcurrentDictionary<string, ILiveStream> _openStreams = new ConcurrentDictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase); private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1); @@ -61,7 +62,8 @@ namespace Emby.Server.Implementations.Library ILogger<MediaSourceManager> logger, IFileSystem fileSystem, IUserDataManager userDataManager, - IMediaEncoder mediaEncoder) + IMediaEncoder mediaEncoder, + IDirectoryService directoryService) { _itemRepo = itemRepo; _userManager = userManager; @@ -72,6 +74,7 @@ namespace Emby.Server.Implementations.Library _mediaEncoder = mediaEncoder; _localizationManager = localizationManager; _appPaths = applicationPaths; + _directoryService = directoryService; } public void AddParts(IEnumerable<IMediaSourceProvider> providers) @@ -106,16 +109,6 @@ namespace Emby.Server.Implementations.Library return false; } - public List<MediaStream> GetMediaStreams(string mediaSourceId) - { - var list = GetMediaStreams(new MediaStreamQuery - { - ItemId = new Guid(mediaSourceId) - }); - - return GetMediaStreamsForItem(list); - } - public List<MediaStream> GetMediaStreams(Guid itemId) { var list = GetMediaStreams(new MediaStreamQuery @@ -161,7 +154,7 @@ namespace Emby.Server.Implementations.Library if (allowMediaProbe && mediaSources[0].Type != MediaSourceType.Placeholder && !mediaSources[0].MediaStreams.Any(i => i.Type == MediaStreamType.Audio || i.Type == MediaStreamType.Video)) { await item.RefreshMetadata( - new MetadataRefreshOptions(new DirectoryService(_fileSystem)) + new MetadataRefreshOptions(_directoryService) { EnableRemoteContentProbe = true, MetadataRefreshMode = MetadataRefreshMode.FullRefresh @@ -212,6 +205,7 @@ namespace Emby.Server.Implementations.Library return SortMediaSources(list); } + /// <inheritdoc />> public MediaProtocol GetPathProtocol(string path) { if (path.StartsWith("Rtsp", StringComparison.OrdinalIgnoreCase)) @@ -258,7 +252,7 @@ namespace Emby.Server.Implementations.Library { if (path != null) { - if (path.IndexOf(".m3u", StringComparison.OrdinalIgnoreCase) != -1) + if (path.Contains(".m3u", StringComparison.OrdinalIgnoreCase)) { return false; } @@ -297,7 +291,7 @@ namespace Emby.Server.Implementations.Library catch (Exception ex) { _logger.LogError(ex, "Error getting media sources"); - return new List<MediaSourceInfo>(); + return Enumerable.Empty<MediaSourceInfo>(); } } @@ -494,14 +488,11 @@ namespace Emby.Server.Implementations.Library _liveStreamSemaphore.Release(); } - // TODO: Don't hardcode this - const bool isAudio = false; - try { if (mediaSource.MediaStreams.Any(i => i.Index != -1) || !mediaSource.SupportsProbing) { - AddMediaInfo(mediaSource, isAudio); + AddMediaInfo(mediaSource); } else { @@ -509,14 +500,14 @@ namespace Emby.Server.Implementations.Library string cacheKey = request.OpenToken; await new LiveStreamHelper(_mediaEncoder, _logger, _appPaths) - .AddMediaInfoWithProbe(mediaSource, isAudio, cacheKey, true, cancellationToken) + .AddMediaInfoWithProbe(mediaSource, false, cacheKey, true, cancellationToken) .ConfigureAwait(false); } } catch (Exception ex) { _logger.LogError(ex, "Error probing live tv stream"); - AddMediaInfo(mediaSource, isAudio); + AddMediaInfo(mediaSource); } // TODO: @bond Fix @@ -536,7 +527,7 @@ namespace Emby.Server.Implementations.Library return new Tuple<LiveStreamResponse, IDirectStreamProvider>(new LiveStreamResponse(clone), liveStream as IDirectStreamProvider); } - private static void AddMediaInfo(MediaSourceInfo mediaSource, bool isAudio) + private static void AddMediaInfo(MediaSourceInfo mediaSource) { mediaSource.DefaultSubtitleStreamIndex = null; @@ -855,9 +846,7 @@ namespace Emby.Server.Implementations.Library return (provider, keyId); } - /// <summary> - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// </summary> + /// <inheritdoc /> public void Dispose() { Dispose(true); diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs index fd9747b4b..7a6aea9c1 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs @@ -6,7 +6,10 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Emby.Naming.Audio; using Emby.Naming.AudioBook; +using Emby.Naming.Common; +using Emby.Naming.Video; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; @@ -21,11 +24,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio /// </summary> public class AudioResolver : ItemResolver<MediaBrowser.Controller.Entities.Audio.Audio>, IMultiItemResolver { - private readonly ILibraryManager _libraryManager; + private readonly NamingOptions _namingOptions; - public AudioResolver(ILibraryManager libraryManager) + public AudioResolver(NamingOptions namingOptions) { - _libraryManager = libraryManager; + _namingOptions = namingOptions; } /// <summary> @@ -40,7 +43,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio string collectionType, IDirectoryService directoryService) { - var result = ResolveMultipleInternal(parent, files, collectionType, directoryService); + var result = ResolveMultipleInternal(parent, files, collectionType); if (result != null) { @@ -56,12 +59,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio private MultiItemResolverResult ResolveMultipleInternal( Folder parent, List<FileSystemMetadata> files, - string collectionType, - IDirectoryService directoryService) + string collectionType) { if (string.Equals(collectionType, CollectionType.Books, StringComparison.OrdinalIgnoreCase)) { - return ResolveMultipleAudio<AudioBook>(parent, files, directoryService, false, collectionType, true); + return ResolveMultipleAudio(parent, files, true); } return null; @@ -87,14 +89,10 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio return null; } - var files = args.FileSystemChildren - .Where(i => !_libraryManager.IgnoreFile(i, args.Parent)) - .ToList(); - - return FindAudio<AudioBook>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false); + return FindAudioBook(args, false); } - if (_libraryManager.IsAudioFile(args.Path)) + if (AudioFileParser.IsAudioFile(args.Path, _namingOptions)) { var extension = Path.GetExtension(args.Path); @@ -107,7 +105,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio var isMixedCollectionType = string.IsNullOrEmpty(collectionType); // For conflicting extensions, give priority to videos - if (isMixedCollectionType && _libraryManager.IsVideoFile(args.Path)) + if (isMixedCollectionType && VideoResolver.IsVideoFile(args.Path, _namingOptions)) { return null; } @@ -141,29 +139,23 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio return null; } - private T FindAudio<T>(ItemResolveArgs args, string path, Folder parent, List<FileSystemMetadata> fileSystemEntries, IDirectoryService directoryService, string collectionType, bool parseName) - where T : MediaBrowser.Controller.Entities.Audio.Audio, new() + private AudioBook FindAudioBook(ItemResolveArgs args, bool parseName) { // TODO: Allow GetMultiDiscMovie in here - const bool supportsMultiVersion = false; + var result = ResolveMultipleAudio(args.Parent, args.GetActualFileSystemChildren(), parseName); - var result = ResolveMultipleAudio<T>(parent, fileSystemEntries, directoryService, supportsMultiVersion, collectionType, parseName) ?? - new MultiItemResolverResult(); - - if (result.Items.Count == 1) + if (result == null || result.Items.Count != 1 || result.Items[0] is not AudioBook item) { - // If we were supporting this we'd be checking filesFromOtherItems - var item = (T)result.Items[0]; - item.IsInMixedFolder = false; - item.Name = Path.GetFileName(item.ContainingFolderPath); - return item; + return null; } - return null; + // If we were supporting this we'd be checking filesFromOtherItems + item.IsInMixedFolder = false; + item.Name = Path.GetFileName(item.ContainingFolderPath); + return item; } - private MultiItemResolverResult ResolveMultipleAudio<T>(Folder parent, IEnumerable<FileSystemMetadata> fileSystemEntries, IDirectoryService directoryService, bool suppportMultiEditions, string collectionType, bool parseName) - where T : MediaBrowser.Controller.Entities.Audio.Audio, new() + private MultiItemResolverResult ResolveMultipleAudio(Folder parent, IEnumerable<FileSystemMetadata> fileSystemEntries, bool parseName) { var files = new List<FileSystemMetadata>(); var items = new List<BaseItem>(); @@ -176,15 +168,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio { leftOver.Add(child); } - else if (!IsIgnored(child.Name)) + else { files.Add(child); } } - var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions(); - - var resolver = new AudioBookListResolver(namingOptions); + var resolver = new AudioBookListResolver(_namingOptions); var resolverResult = resolver.Resolve(files).ToList(); var result = new MultiItemResolverResult @@ -210,7 +200,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio var firstMedia = resolvedItem.Files[0]; - var libraryItem = new T + var libraryItem = new AudioBook { Path = firstMedia.Path, IsInMixedFolder = isInMixedFolder, @@ -230,12 +220,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio return result; } - private bool ContainsFile(List<AudioBookInfo> result, FileSystemMetadata file) + private static bool ContainsFile(IEnumerable<AudioBookInfo> result, FileSystemMetadata file) { return result.Any(i => ContainsFile(i, file)); } - private bool ContainsFile(AudioBookInfo result, FileSystemMetadata file) + private static bool ContainsFile(AudioBookInfo result, FileSystemMetadata file) { return result.Files.Any(i => ContainsFile(i, file)) || result.AlternateVersions.Any(i => ContainsFile(i, file)) || @@ -246,10 +236,5 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio { return string.Equals(result.Path, file.FullName, StringComparison.OrdinalIgnoreCase); } - - private static bool IsIgnored(string filename) - { - return false; - } } } diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs index 60720dd2f..a9819a364 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Emby.Naming.Audio; +using Emby.Naming.Common; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; @@ -22,20 +23,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio public class MusicAlbumResolver : ItemResolver<MusicAlbum> { private readonly ILogger<MusicAlbumResolver> _logger; - private readonly IFileSystem _fileSystem; - private readonly ILibraryManager _libraryManager; + private readonly NamingOptions _namingOptions; /// <summary> /// Initializes a new instance of the <see cref="MusicAlbumResolver"/> class. /// </summary> /// <param name="logger">The logger.</param> - /// <param name="fileSystem">The file system.</param> - /// <param name="libraryManager">The library manager.</param> - public MusicAlbumResolver(ILogger<MusicAlbumResolver> logger, IFileSystem fileSystem, ILibraryManager libraryManager) + /// <param name="namingOptions">The naming options.</param> + public MusicAlbumResolver(ILogger<MusicAlbumResolver> logger, NamingOptions namingOptions) { _logger = logger; - _fileSystem = fileSystem; - _libraryManager = libraryManager; + _namingOptions = namingOptions; } /// <summary> @@ -87,7 +85,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio /// <returns><c>true</c> if the provided path points to a music album, <c>false</c> otherwise.</returns> public bool IsMusicAlbum(string path, IDirectoryService directoryService) { - return ContainsMusic(directoryService.GetFileSystemEntries(path), true, directoryService, _logger, _fileSystem, _libraryManager); + return ContainsMusic(directoryService.GetFileSystemEntries(path), true, directoryService); } /// <summary> @@ -101,7 +99,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio if (args.IsDirectory) { // if (args.Parent is MusicArtist) return true; // saves us from testing children twice - if (ContainsMusic(args.FileSystemChildren, true, args.DirectoryService, _logger, _fileSystem, _libraryManager)) + if (ContainsMusic(args.FileSystemChildren, true, args.DirectoryService)) { return true; } @@ -116,13 +114,10 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio private bool ContainsMusic( IEnumerable<FileSystemMetadata> list, bool allowSubfolders, - IDirectoryService directoryService, - ILogger<MusicAlbumResolver> logger, - IFileSystem fileSystem, - ILibraryManager libraryManager) + IDirectoryService directoryService) { // check for audio files before digging down into directories - var foundAudioFile = list.Any(fileSystemInfo => !fileSystemInfo.IsDirectory && libraryManager.IsAudioFile(fileSystemInfo.FullName)); + var foundAudioFile = list.Any(fileSystemInfo => !fileSystemInfo.IsDirectory && AudioFileParser.IsAudioFile(fileSystemInfo.FullName, _namingOptions)); if (foundAudioFile) { // at least one audio file exists @@ -137,21 +132,20 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio var discSubfolderCount = 0; - var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions(); - var parser = new AlbumParser(namingOptions); + var parser = new AlbumParser(_namingOptions); var directories = list.Where(fileSystemInfo => fileSystemInfo.IsDirectory); var result = Parallel.ForEach(directories, (fileSystemInfo, state) => { var path = fileSystemInfo.FullName; - var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryManager); + var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService); if (hasMusic) { if (parser.IsMultiPart(path)) { - logger.LogDebug("Found multi-disc folder: " + path); + _logger.LogDebug("Found multi-disc folder: {Path}", path); Interlocked.Increment(ref discSubfolderCount); } else diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs index 3d2ae95d2..27e18be42 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs @@ -3,6 +3,7 @@ using System; using System.Linq; using System.Threading.Tasks; +using Emby.Naming.Common; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; @@ -19,27 +20,19 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio public class MusicArtistResolver : ItemResolver<MusicArtist> { private readonly ILogger<MusicAlbumResolver> _logger; - private readonly IFileSystem _fileSystem; - private readonly ILibraryManager _libraryManager; - private readonly IServerConfigurationManager _config; + private NamingOptions _namingOptions; /// <summary> /// Initializes a new instance of the <see cref="MusicArtistResolver"/> class. /// </summary> /// <param name="logger">The logger for the created <see cref="MusicAlbumResolver"/> instances.</param> - /// <param name="fileSystem">The file system.</param> - /// <param name="libraryManager">The library manager.</param> - /// <param name="config">The configuration manager.</param> + /// <param name="namingOptions">The naming options.</param> public MusicArtistResolver( ILogger<MusicAlbumResolver> logger, - IFileSystem fileSystem, - ILibraryManager libraryManager, - IServerConfigurationManager config) + NamingOptions namingOptions) { _logger = logger; - _fileSystem = fileSystem; - _libraryManager = libraryManager; - _config = config; + _namingOptions = namingOptions; } /// <summary> @@ -89,7 +82,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio var directoryService = args.DirectoryService; - var albumResolver = new MusicAlbumResolver(_logger, _fileSystem, _libraryManager); + var albumResolver = new MusicAlbumResolver(_logger, _namingOptions); // If we contain an album assume we are an artist folder var directories = args.FileSystemChildren.Where(i => i.IsDirectory); diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs index 9ff99fa43..0ebf0e530 100644 --- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs @@ -6,6 +6,7 @@ using System; using System.IO; using System.Linq; using DiscUtils.Udf; +using Emby.Naming.Common; using Emby.Naming.Video; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -21,12 +22,12 @@ namespace Emby.Server.Implementations.Library.Resolvers public abstract class BaseVideoResolver<T> : MediaBrowser.Controller.Resolvers.ItemResolver<T> where T : Video, new() { - protected BaseVideoResolver(ILibraryManager libraryManager) + protected BaseVideoResolver(NamingOptions namingOptions) { - LibraryManager = libraryManager; + NamingOptions = namingOptions; } - protected ILibraryManager LibraryManager { get; } + protected NamingOptions NamingOptions { get; } /// <summary> /// Resolves the specified args. @@ -48,7 +49,7 @@ namespace Emby.Server.Implementations.Library.Resolvers protected virtual TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName) where TVideoType : Video, new() { - var namingOptions = LibraryManager.GetNamingOptions(); + var namingOptions = NamingOptions; // If the path is a file check for a matching extensions if (args.IsDirectory) @@ -138,7 +139,7 @@ namespace Emby.Server.Implementations.Library.Resolvers return null; } - if (LibraryManager.IsVideoFile(args.Path) || videoInfo.IsStub) + if (VideoResolver.IsVideoFile(args.Path, NamingOptions) || videoInfo.IsStub) { var path = args.Path; @@ -267,7 +268,7 @@ namespace Emby.Server.Implementations.Library.Resolvers protected void Set3DFormat(Video video) { - var result = Format3DParser.Parse(video.Path, LibraryManager.GetNamingOptions()); + var result = Format3DParser.Parse(video.Path, NamingOptions); Set3DFormat(video, result.Is3D, result.Format3D); } diff --git a/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs index 9599faea4..72341d9db 100644 --- a/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs @@ -2,16 +2,16 @@ #pragma warning disable CS1591 +using Emby.Naming.Common; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; namespace Emby.Server.Implementations.Library.Resolvers { public class GenericVideoResolver<T> : BaseVideoResolver<T> where T : Video, new() { - public GenericVideoResolver(ILibraryManager libraryManager) - : base(libraryManager) + public GenericVideoResolver(NamingOptions namingOptions) + : base(namingOptions) { } } diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index f3b6ef0a2..732be0fe5 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; +using Emby.Naming.Common; using Emby.Naming.Video; using Jellyfin.Extensions; using MediaBrowser.Controller.Drawing; @@ -25,6 +26,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies public class MovieResolver : BaseVideoResolver<Video>, IMultiItemResolver { private readonly IImageProcessor _imageProcessor; + private readonly StackResolver _stackResolver; private string[] _validCollectionTypes = new[] { @@ -38,12 +40,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies /// <summary> /// Initializes a new instance of the <see cref="MovieResolver"/> class. /// </summary> - /// <param name="libraryManager">The library manager.</param> /// <param name="imageProcessor">The image processor.</param> - public MovieResolver(ILibraryManager libraryManager, IImageProcessor imageProcessor) - : base(libraryManager) + /// <param name="namingOptions">The naming options.</param> + public MovieResolver(IImageProcessor imageProcessor, NamingOptions namingOptions) + : base(namingOptions) { _imageProcessor = imageProcessor; + _stackResolver = new StackResolver(NamingOptions); } /// <summary> @@ -89,9 +92,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies return null; } - var files = args.FileSystemChildren - .Where(i => !LibraryManager.IgnoreFile(i, args.Parent)) - .ToList(); + var files = args.GetActualFileSystemChildren().ToList(); if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase)) { @@ -258,9 +259,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies } } - var namingOptions = LibraryManager.GetNamingOptions(); - - var resolverResult = VideoListResolver.Resolve(files, namingOptions, suppportMultiEditions).ToList(); + var resolverResult = VideoListResolver.Resolve(files, NamingOptions, suppportMultiEditions).ToList(); var result = new MultiItemResolverResult { @@ -438,7 +437,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies if (result.Items.Count == 1) { var videoPath = result.Items[0].Path; - var hasPhotos = photos.Any(i => !PhotoResolver.IsOwnedByResolvedMedia(LibraryManager, videoPath, i.Name)); + var hasPhotos = photos.Any(i => !PhotoResolver.IsOwnedByResolvedMedia(videoPath, i.Name)); if (!hasPhotos) { @@ -511,9 +510,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies return null; } - var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions(); - - var result = new StackResolver(namingOptions).ResolveDirectories(folderPaths).ToList(); + var result = _stackResolver.ResolveDirectories(folderPaths).ToList(); if (result.Count != 1) { diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs index 1c560e8a6..7dd0ab185 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs @@ -1,6 +1,7 @@ #nullable disable using System; +using Emby.Naming.Common; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -15,17 +16,17 @@ namespace Emby.Server.Implementations.Library.Resolvers public class PhotoAlbumResolver : GenericFolderResolver<PhotoAlbum> { private readonly IImageProcessor _imageProcessor; - private readonly ILibraryManager _libraryManager; + private readonly NamingOptions _namingOptions; /// <summary> /// Initializes a new instance of the <see cref="PhotoAlbumResolver"/> class. /// </summary> /// <param name="imageProcessor">The image processor.</param> - /// <param name="libraryManager">The library manager.</param> - public PhotoAlbumResolver(IImageProcessor imageProcessor, ILibraryManager libraryManager) + /// <param name="namingOptions">The naming options.</param> + public PhotoAlbumResolver(IImageProcessor imageProcessor, NamingOptions namingOptions) { _imageProcessor = imageProcessor; - _libraryManager = libraryManager; + _namingOptions = namingOptions; } /// <inheritdoc /> @@ -73,7 +74,7 @@ namespace Emby.Server.Implementations.Library.Resolvers foreach (var siblingFile in files) { - if (PhotoResolver.IsOwnedByMedia(_libraryManager, siblingFile.FullName, filename)) + if (PhotoResolver.IsOwnedByMedia(_namingOptions, siblingFile.FullName, filename)) { ownedByMedia = true; break; diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs index 57bf40e9e..51d819303 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs @@ -6,6 +6,8 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Emby.Naming.Common; +using Emby.Naming.Video; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -16,7 +18,8 @@ namespace Emby.Server.Implementations.Library.Resolvers public class PhotoResolver : ItemResolver<Photo> { private readonly IImageProcessor _imageProcessor; - private readonly ILibraryManager _libraryManager; + private readonly NamingOptions _namingOptions; + private static readonly HashSet<string> _ignoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "folder", @@ -30,10 +33,11 @@ namespace Emby.Server.Implementations.Library.Resolvers "default" }; - public PhotoResolver(IImageProcessor imageProcessor, ILibraryManager libraryManager) + + public PhotoResolver(IImageProcessor imageProcessor, NamingOptions namingOptions) { _imageProcessor = imageProcessor; - _libraryManager = libraryManager; + _namingOptions = namingOptions; } /// <summary> @@ -60,7 +64,7 @@ namespace Emby.Server.Implementations.Library.Resolvers foreach (var file in files) { - if (IsOwnedByMedia(_libraryManager, file.FullName, filename)) + if (IsOwnedByMedia(_namingOptions, file.FullName, filename)) { return null; } @@ -77,17 +81,12 @@ namespace Emby.Server.Implementations.Library.Resolvers return null; } - internal static bool IsOwnedByMedia(ILibraryManager libraryManager, string file, string imageFilename) + internal static bool IsOwnedByMedia(NamingOptions namingOptions, string file, string imageFilename) { - if (libraryManager.IsVideoFile(file)) - { - return IsOwnedByResolvedMedia(libraryManager, file, imageFilename); - } - - return false; + return VideoResolver.IsVideoFile(file, namingOptions) && IsOwnedByResolvedMedia(file, imageFilename); } - internal static bool IsOwnedByResolvedMedia(ILibraryManager libraryManager, string file, string imageFilename) + internal static bool IsOwnedByResolvedMedia(string file, string imageFilename) => imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file), StringComparison.OrdinalIgnoreCase); internal static bool IsImageFile(string path, IImageProcessor imageProcessor) diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs index cf2d22f4d..f72da3617 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs @@ -2,6 +2,7 @@ using System; using System.Linq; +using Emby.Naming.Common; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; @@ -17,9 +18,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV /// <summary> /// Initializes a new instance of the <see cref="EpisodeResolver"/> class. /// </summary> - /// <param name="libraryManager">The library manager.</param> - public EpisodeResolver(ILibraryManager libraryManager) - : base(libraryManager) + /// <param name="namingOptions">The naming options.</param> + public EpisodeResolver(NamingOptions namingOptions) + : base(namingOptions) { } diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs index 063f67543..ea4851458 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs @@ -1,6 +1,7 @@ #nullable disable using System.Globalization; +using Emby.Naming.Common; using Emby.Naming.TV; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; @@ -14,22 +15,22 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV /// </summary> public class SeasonResolver : GenericFolderResolver<Season> { - private readonly ILibraryManager _libraryManager; private readonly ILocalizationManager _localization; private readonly ILogger<SeasonResolver> _logger; + private readonly NamingOptions _namingOptions; /// <summary> /// Initializes a new instance of the <see cref="SeasonResolver"/> class. /// </summary> - /// <param name="libraryManager">The library manager.</param> + /// <param name="namingOptions">The naming options.</param> /// <param name="localization">The localization.</param> /// <param name="logger">The logger.</param> public SeasonResolver( - ILibraryManager libraryManager, + NamingOptions namingOptions, ILocalizationManager localization, ILogger<SeasonResolver> logger) { - _libraryManager = libraryManager; + _namingOptions = namingOptions; _localization = localization; _logger = logger; } @@ -43,7 +44,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV { if (args.Parent is Series series && args.IsDirectory) { - var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions(); + var namingOptions = _namingOptions; var path = args.Path; @@ -65,18 +66,15 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV var episodeInfo = resolver.Resolve(testPath, true); - if (episodeInfo != null) + if (episodeInfo?.EpisodeNumber != null && episodeInfo.SeasonNumber.HasValue) { - if (episodeInfo.EpisodeNumber.HasValue && episodeInfo.SeasonNumber.HasValue) - { - _logger.LogDebug( - "Found folder underneath series with episode number: {0}. Season {1}. Episode {2}", - path, - episodeInfo.SeasonNumber.Value, - episodeInfo.EpisodeNumber.Value); - - return null; - } + _logger.LogDebug( + "Found folder underneath series with episode number: {0}. Season {1}. Episode {2}", + path, + episodeInfo.SeasonNumber.Value, + episodeInfo.EpisodeNumber.Value); + + return null; } } diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs index b7fbe01c5..4e15acd18 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs @@ -5,7 +5,12 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; +using Emby.Naming.Common; using Emby.Naming.TV; +using Emby.Naming.Video; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Resolvers; @@ -21,17 +26,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV public class SeriesResolver : GenericFolderResolver<Series> { private readonly ILogger<SeriesResolver> _logger; - private readonly ILibraryManager _libraryManager; + private readonly NamingOptions _namingOptions; /// <summary> /// Initializes a new instance of the <see cref="SeriesResolver"/> class. /// </summary> /// <param name="logger">The logger.</param> - /// <param name="libraryManager">The library manager.</param> - public SeriesResolver(ILogger<SeriesResolver> logger, ILibraryManager libraryManager) + /// <param name="namingOptions">The naming options.</param> + public SeriesResolver(ILogger<SeriesResolver> logger, NamingOptions namingOptions) { _logger = logger; - _libraryManager = libraryManager; + _namingOptions = namingOptions; } /// <summary> @@ -54,16 +59,19 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV return null; } + var seriesInfo = Naming.TV.SeriesResolver.Resolve(_namingOptions, args.Path); + var collectionType = args.GetCollectionType(); if (string.Equals(collectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) { - var configuredContentType = _libraryManager.GetConfiguredContentType(args.Path); + // TODO refactor into separate class or something, this is copied from LibraryManager.GetConfiguredContentType + var configuredContentType = args.GetConfiguredContentType(); if (!string.Equals(configuredContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) { return new Series { Path = args.Path, - Name = Path.GetFileName(args.Path) + Name = seriesInfo.Name }; } } @@ -80,7 +88,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV return new Series { Path = args.Path, - Name = Path.GetFileName(args.Path) + Name = seriesInfo.Name }; } @@ -89,12 +97,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV return null; } - if (IsSeriesFolder(args.Path, args.FileSystemChildren, _logger, _libraryManager, false)) + if (IsSeriesFolder(args.Path, args.FileSystemChildren, false)) { return new Series { Path = args.Path, - Name = Path.GetFileName(args.Path) + Name = seriesInfo.Name }; } } @@ -103,11 +111,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV return null; } - public static bool IsSeriesFolder( + private bool IsSeriesFolder( string path, IEnumerable<FileSystemMetadata> fileSystemChildren, - ILogger<SeriesResolver> logger, - ILibraryManager libraryManager, bool isTvContentType) { foreach (var child in fileSystemChildren) @@ -116,21 +122,21 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV { if (IsSeasonFolder(child.FullName, isTvContentType)) { - logger.LogDebug("{Path} is a series because of season folder {Dir}.", path, child.FullName); + _logger.LogDebug("{Path} is a series because of season folder {Dir}.", path, child.FullName); return true; } } else { string fullName = child.FullName; - if (libraryManager.IsVideoFile(fullName)) + if (VideoResolver.IsVideoFile(path, _namingOptions)) { if (isTvContentType) { return true; } - var namingOptions = ((LibraryManager)libraryManager).GetNamingOptions(); + var namingOptions = _namingOptions; var episodeResolver = new Naming.TV.EpisodeResolver(namingOptions); @@ -143,7 +149,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV } } - logger.LogDebug("{Path} is not a series folder.", path); + _logger.LogDebug("{Path} is not a series folder.", path); return false; } diff --git a/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs new file mode 100644 index 000000000..945b559ad --- /dev/null +++ b/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs @@ -0,0 +1,156 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; +using MediaBrowser.Controller.Collections; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Querying; +using Jellyfin.Data.Enums; +using Microsoft.Extensions.Logging; +using MediaBrowser.Model.Entities; + +namespace Emby.Server.Implementations.Library.Validators +{ + /// <summary> + /// Class CollectionPostScanTask. + /// </summary> + public class CollectionPostScanTask : ILibraryPostScanTask + { + private readonly ILibraryManager _libraryManager; + private readonly ICollectionManager _collectionManager; + private readonly ILogger<CollectionPostScanTask> _logger; + + /// <summary> + /// Initializes a new instance of the <see cref="CollectionPostScanTask" /> class. + /// </summary> + /// <param name="libraryManager">The library manager.</param> + /// <param name="collectionManager">The collection manager.</param> + /// <param name="logger">The logger.</param> + public CollectionPostScanTask( + ILibraryManager libraryManager, + ICollectionManager collectionManager, + ILogger<CollectionPostScanTask> logger) + { + _libraryManager = libraryManager; + _collectionManager = collectionManager; + _logger = logger; + } + + /// <summary> + /// Runs the specified progress. + /// </summary> + /// <param name="progress">The progress.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + public async Task Run(IProgress<double> progress, CancellationToken cancellationToken) + { + var collectionNameMoviesMap = new Dictionary<string, HashSet<Guid>>(); + + foreach (var library in _libraryManager.RootFolder.Children) + { + if (!_libraryManager.GetLibraryOptions(library).AutomaticallyAddToCollection) + { + continue; + } + + var startIndex = 0; + var pagesize = 1000; + + while (true) + { + var movies = _libraryManager.GetItemList(new InternalItemsQuery + { + MediaTypes = new string[] { MediaType.Video }, + IncludeItemTypes = new[] { nameof(Movie) }, + IsVirtualItem = false, + OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }, + Parent = library, + StartIndex = startIndex, + Limit = pagesize, + Recursive = true + }); + + foreach (var m in movies) + { + if (m is Movie movie && !string.IsNullOrEmpty(movie.CollectionName)) + { + if (collectionNameMoviesMap.TryGetValue(movie.CollectionName, out var movieList)) + { + movieList.Add(movie.Id); + } + else + { + collectionNameMoviesMap[movie.CollectionName] = new HashSet<Guid> { movie.Id }; + } + } + } + + if (movies.Count < pagesize) + { + break; + } + + startIndex += pagesize; + } + } + + var numComplete = 0; + var count = collectionNameMoviesMap.Count; + + if (count == 0) + { + progress.Report(100); + return; + } + + var boxSets = _libraryManager.GetItemList(new InternalItemsQuery + { + IncludeItemTypes = new[] { nameof(BoxSet) }, + CollapseBoxSetItems = false, + Recursive = true + }); + + foreach (var (collectionName, movieIds) in collectionNameMoviesMap) + { + try + { + var boxSet = boxSets.FirstOrDefault(b => b?.Name == collectionName) as BoxSet; + if (boxSet == null) + { + // won't automatically create collection if only one movie in it + if (movieIds.Count >= 2) + { + boxSet = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions + { + Name = collectionName, + IsLocked = true + }); + + await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds); + } + } + else + { + await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds); + } + + numComplete++; + double percent = numComplete; + percent /= count; + percent *= 100; + + progress.Report(percent); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error refreshing {CollectionName} with {@MovieIds}", collectionName, movieIds); + } + } + + progress.Report(100); + } + } +} diff --git a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs index 8739a9e1b..8a9a4b865 100644 --- a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs @@ -78,7 +78,7 @@ namespace Emby.Server.Implementations.Library.Validators } catch (Exception ex) { - _logger.LogError(ex, "Error validating IBN entry {person}", person); + _logger.LogError(ex, "Error validating IBN entry {Person}", person); } // Update progress diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index 980b42729..644f9050d 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -957,7 +957,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV public async Task<ILiveStream> GetChannelStreamWithDirectStreamProvider(string channelId, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken) { - _logger.LogInformation("Streaming Channel " + channelId); + _logger.LogInformation("Streaming Channel {Id}", channelId); var result = string.IsNullOrEmpty(streamId) ? null : @@ -1027,7 +1027,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { var stream = new MediaSourceInfo { - EncoderPath = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveRecordings/" + info.Id + "/stream", + EncoderPath = _appHost.GetApiUrlForLocalAccess() + "/LiveTv/LiveRecordings/" + info.Id + "/stream", EncoderProtocol = MediaProtocol.Http, Path = info.Path, Protocol = MediaProtocol.File, @@ -1308,16 +1308,16 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV await recorder.Record(directStreamProvider, mediaStreamInfo, recordPath, duration, onStarted, activeRecordingInfo.CancellationTokenSource.Token).ConfigureAwait(false); recordingStatus = RecordingStatus.Completed; - _logger.LogInformation("Recording completed: {recordPath}", recordPath); + _logger.LogInformation("Recording completed: {RecordPath}", recordPath); } catch (OperationCanceledException) { - _logger.LogInformation("Recording stopped: {recordPath}", recordPath); + _logger.LogInformation("Recording stopped: {RecordPath}", recordPath); recordingStatus = RecordingStatus.Completed; } catch (Exception ex) { - _logger.LogError(ex, "Error recording to {recordPath}", recordPath); + _logger.LogError(ex, "Error recording to {RecordPath}", recordPath); recordingStatus = RecordingStatus.Error; } @@ -1404,7 +1404,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } catch (Exception ex) { - _logger.LogError(ex, "Error deleting 0-byte failed recording file {path}", path); + _logger.LogError(ex, "Error deleting 0-byte failed recording file {Path}", path); } } } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs index 835028b92..a88a1fe84 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs @@ -87,8 +87,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV ErrorDialog = false }; - var commandLineLogMessage = processStartInfo.FileName + " " + processStartInfo.Arguments; - _logger.LogInformation(commandLineLogMessage); + _logger.LogInformation("{Filename} {Arguments}", processStartInfo.FileName, processStartInfo.Arguments); var logFilePath = Path.Combine(_appPaths.LogDirectoryPath, "record-transcode-" + Guid.NewGuid() + ".txt"); Directory.CreateDirectory(Path.GetDirectoryName(logFilePath)); @@ -97,7 +96,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV _logFileStream = new FileStream(logFilePath, FileMode.CreateNew, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); await JsonSerializer.SerializeAsync(_logFileStream, mediaSource, _jsonOptions, cancellationToken).ConfigureAwait(false); - await _logFileStream.WriteAsync(Encoding.UTF8.GetBytes(Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine), cancellationToken).ConfigureAwait(false); + await _logFileStream.WriteAsync(Encoding.UTF8.GetBytes(Environment.NewLine + Environment.NewLine + processStartInfo.FileName + " " + processStartInfo.Arguments + Environment.NewLine + Environment.NewLine), cancellationToken).ConfigureAwait(false); _process = new Process { @@ -188,7 +187,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV CultureInfo.InvariantCulture, "-i \"{0}\" {2} -map_metadata -1 -threads {6} {3}{4}{5} -y \"{1}\"", inputTempFile, - targetFile.Replace("\"", "\\\""), // Escape quotes in filename + targetFile.Replace("\"", "\\\"", StringComparison.Ordinal), // Escape quotes in filename videoArgs, GetAudioArgs(mediaSource), subtitleArgs, @@ -225,13 +224,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { try { - _logger.LogInformation("Stopping ffmpeg recording process for {path}", _targetPath); + _logger.LogInformation("Stopping ffmpeg recording process for {Path}", _targetPath); _process.StandardInput.WriteLine("q"); } catch (Exception ex) { - _logger.LogError(ex, "Error stopping recording transcoding job for {path}", _targetPath); + _logger.LogError(ex, "Error stopping recording transcoding job for {Path}", _targetPath); } if (_hasExited) @@ -241,7 +240,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV try { - _logger.LogInformation("Calling recording process.WaitForExit for {path}", _targetPath); + _logger.LogInformation("Calling recording process.WaitForExit for {Path}", _targetPath); if (_process.WaitForExit(10000)) { @@ -250,7 +249,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } catch (Exception ex) { - _logger.LogError(ex, "Error waiting for recording process to exit for {path}", _targetPath); + _logger.LogError(ex, "Error waiting for recording process to exit for {Path}", _targetPath); } if (_hasExited) @@ -260,13 +259,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV try { - _logger.LogInformation("Killing ffmpeg recording process for {path}", _targetPath); + _logger.LogInformation("Killing ffmpeg recording process for {Path}", _targetPath); _process.Kill(); } catch (Exception ex) { - _logger.LogError(ex, "Error killing recording transcoding job for {path}", _targetPath); + _logger.LogError(ex, "Error killing recording transcoding job for {Path}", _targetPath); } } } diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index 1f963e4a2..08aa0cfd7 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -11,6 +11,7 @@ using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Net.Mime; +using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Threading; @@ -20,7 +21,6 @@ using Jellyfin.Extensions; using Jellyfin.Extensions.Json; using MediaBrowser.Common.Net; using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.LiveTv; @@ -35,7 +35,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings private readonly ILogger<SchedulesDirect> _logger; private readonly IHttpClientFactory _httpClientFactory; private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1); - private readonly ICryptoProvider _cryptoProvider; private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>(); private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; @@ -43,12 +42,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings public SchedulesDirect( ILogger<SchedulesDirect> logger, - IHttpClientFactory httpClientFactory, - ICryptoProvider cryptoProvider) + IHttpClientFactory httpClientFactory) { _logger = logger; _httpClientFactory = httpClientFactory; - _cryptoProvider = cryptoProvider; } /// <inheritdoc /> @@ -169,12 +166,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings const double DesiredAspect = 2.0 / 3; - programEntry.PrimaryImage = GetProgramImage(ApiUrl, imagesWithText, true, DesiredAspect) ?? - GetProgramImage(ApiUrl, allImages, true, DesiredAspect); + programEntry.PrimaryImage = GetProgramImage(ApiUrl, imagesWithText, DesiredAspect) ?? + GetProgramImage(ApiUrl, allImages, DesiredAspect); const double WideAspect = 16.0 / 9; - programEntry.ThumbImage = GetProgramImage(ApiUrl, imagesWithText, true, WideAspect); + programEntry.ThumbImage = GetProgramImage(ApiUrl, imagesWithText, WideAspect); // Don't supply the same image twice if (string.Equals(programEntry.PrimaryImage, programEntry.ThumbImage, StringComparison.Ordinal)) @@ -182,7 +179,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings programEntry.ThumbImage = null; } - programEntry.BackdropImage = GetProgramImage(ApiUrl, imagesWithoutText, true, WideAspect); + programEntry.BackdropImage = GetProgramImage(ApiUrl, imagesWithoutText, WideAspect); // programEntry.bannerImage = GetProgramImage(ApiUrl, data, "Banner", false) ?? // GetProgramImage(ApiUrl, data, "Banner-L1", false) ?? @@ -403,7 +400,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings return info; } - private string GetProgramImage(string apiUrl, IEnumerable<ImageDataDto> images, bool returnDefaultImage, double desiredAspect) + private static string GetProgramImage(string apiUrl, IEnumerable<ImageDataDto> images, double desiredAspect) { var match = images .OrderBy(i => Math.Abs(desiredAspect - GetAspectRatio(i))) @@ -648,7 +645,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings CancellationToken cancellationToken) { using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/token"); - var hashedPasswordBytes = _cryptoProvider.ComputeHash("SHA1", Encoding.ASCII.GetBytes(password), Array.Empty<byte>()); + var hashedPasswordBytes = SHA1.HashData(Encoding.ASCII.GetBytes(password)); // TODO: remove ToLower when Convert.ToHexString supports lowercase // Schedules Direct requires the hex to be lowercase string hashedPassword = Convert.ToHexString(hashedPasswordBytes).ToLowerInvariant(); diff --git a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs index 21e1409ac..598e3f88a 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs @@ -393,7 +393,7 @@ namespace Emby.Server.Implementations.LiveTv } catch (Exception ex) { - _logger.LogError(ex, "Error getting image info for {name}", info.Name); + _logger.LogError(ex, "Error getting image info for {Name}", info.Name); } return null; diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs index ea1a28fe8..a41b63f28 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs @@ -1054,7 +1054,7 @@ namespace Emby.Server.Implementations.LiveTv { cancellationToken.ThrowIfCancellationRequested(); - _logger.LogDebug("Refreshing guide from {name}", service.Name); + _logger.LogDebug("Refreshing guide from {Name}", service.Name); try { @@ -1135,7 +1135,7 @@ namespace Emby.Server.Implementations.LiveTv } catch (Exception ex) { - _logger.LogError(ex, "Error getting channel information for {name}", channelInfo.Item2.Name); + _logger.LogError(ex, "Error getting channel information for {Name}", channelInfo.Item2.Name); } numComplete++; @@ -1248,7 +1248,7 @@ namespace Emby.Server.Implementations.LiveTv } catch (Exception ex) { - _logger.LogError(ex, "Error getting programs for channel {name}", currentChannel.Name); + _logger.LogError(ex, "Error getting programs for channel {Name}", currentChannel.Name); } numComplete++; diff --git a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs index ecd28097d..4b7584af3 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs @@ -104,7 +104,7 @@ namespace Emby.Server.Implementations.LiveTv // Dummy this up so that direct play checks can still run if (string.IsNullOrEmpty(source.Path) && source.Protocol == MediaProtocol.Http) { - source.Path = _appHost.GetSmartApiUrl(string.Empty); + source.Path = _appHost.GetApiUrlForLocalAccess(); } } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs index f9d151a81..9ab4cc628 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(); - client.Connect(remoteIp, HdHomeRunPort); + await client.ConnectAsync(remoteIp, HdHomeRunPort).ConfigureAwait(false); using var stream = client.GetStream(); return await CheckTunerAvailability(stream, tuner, cancellationToken).ConfigureAwait(false); diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs index 31445e1ec..9fba17ca3 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs @@ -82,7 +82,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun Directory.CreateDirectory(Path.GetDirectoryName(TempFilePath)); - Logger.LogInformation("Opening HDHR UDP Live stream from {host}", uri.Host); + Logger.LogInformation("Opening HDHR UDP Live stream from {Host}", uri.Host); var remoteAddress = IPAddress.Parse(uri.Host); IPAddress localAddress = null; @@ -147,7 +147,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun // OpenedMediaSource.Path = tempFile; // OpenedMediaSource.ReadAtNativeFramerate = true; - MediaSource.Path = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts"; + MediaSource.Path = _appHost.GetApiUrlForLocalAccess() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts"; MediaSource.Protocol = MediaProtocol.Http; // OpenedMediaSource.SupportsDirectPlay = false; // OpenedMediaSource.SupportsDirectStream = true; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs index 506ef5548..708ff52d7 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs @@ -283,7 +283,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts // #EXTINF:0,84.0 - VOX Schweiz if (!string.IsNullOrWhiteSpace(nameInExtInf)) { - var numberIndex = nameInExtInf.IndexOf(' '); + var numberIndex = nameInExtInf.IndexOf(' ', StringComparison.Ordinal); if (numberIndex > 0) { var numberPart = nameInExtInf.Substring(0, numberIndex).Trim(new[] { ' ', '.' }); diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs index 3b69e55b0..b1ce7b2b3 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs @@ -83,7 +83,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts // OpenedMediaSource.Path = tempFile; // OpenedMediaSource.ReadAtNativeFramerate = true; - MediaSource.Path = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts"; + MediaSource.Path = _appHost.GetApiUrlForLocalAccess() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts"; MediaSource.Protocol = MediaProtocol.Http; // OpenedMediaSource.Path = TempFilePath; diff --git a/Emby.Server.Implementations/Localization/Core/as.json b/Emby.Server.Implementations/Localization/Core/as.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/as.json @@ -0,0 +1 @@ +{} diff --git a/Emby.Server.Implementations/Localization/Core/be.json b/Emby.Server.Implementations/Localization/Core/be.json new file mode 100644 index 000000000..56c4e7d39 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/be.json @@ -0,0 +1,4 @@ +{ + "Sync": "Сінхранізацыя", + "Playlists": "Плэйліст" +} diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json index db3c13d80..2dee5e327 100644 --- a/Emby.Server.Implementations/Localization/Core/ca.json +++ b/Emby.Server.Implementations/Localization/Core/ca.json @@ -25,7 +25,7 @@ "HeaderLiveTV": "TV en Directe", "HeaderNextUp": "A continuació", "HeaderRecordingGroups": "Grups d'Enregistrament", - "HomeVideos": "Vídeos domèstics", + "HomeVideos": "Vídeos Domèstics", "Inherit": "Hereta", "ItemAddedWithName": "{0} ha estat afegit a la biblioteca", "ItemRemovedWithName": "{0} ha estat eliminat de la biblioteca", @@ -39,7 +39,7 @@ "MixedContent": "Contingut barrejat", "Movies": "Pel·lícules", "Music": "Música", - "MusicVideos": "Vídeos musicals", + "MusicVideos": "Vídeos Musicals", "NameInstallFailed": "Instalació de {0} fallida", "NameSeasonNumber": "Temporada {0}", "NameSeasonUnknown": "Temporada Desconeguda", diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json index 1ea378321..25f51db16 100644 --- a/Emby.Server.Implementations/Localization/Core/cs.json +++ b/Emby.Server.Implementations/Localization/Core/cs.json @@ -15,7 +15,7 @@ "Favorites": "Oblíbené", "Folders": "Složky", "Genres": "Žánry", - "HeaderAlbumArtists": "Album umělce", + "HeaderAlbumArtists": "Umělci alba", "HeaderContinueWatching": "Pokračovat ve sledování", "HeaderFavoriteAlbums": "Oblíbená alba", "HeaderFavoriteArtists": "Oblíbení interpreti", diff --git a/Emby.Server.Implementations/Localization/Core/en-GB.json b/Emby.Server.Implementations/Localization/Core/en-GB.json index 86ce9240e..add578376 100644 --- a/Emby.Server.Implementations/Localization/Core/en-GB.json +++ b/Emby.Server.Implementations/Localization/Core/en-GB.json @@ -15,7 +15,7 @@ "Favorites": "Favourites", "Folders": "Folders", "Genres": "Genres", - "HeaderAlbumArtists": "Artist's Album", + "HeaderAlbumArtists": "Album artists", "HeaderContinueWatching": "Continue Watching", "HeaderFavoriteAlbums": "Favourite Albums", "HeaderFavoriteArtists": "Favourite Artists", diff --git a/Emby.Server.Implementations/Localization/Core/en-US.json b/Emby.Server.Implementations/Localization/Core/en-US.json index ca127cdb8..568a8e447 100644 --- a/Emby.Server.Implementations/Localization/Core/en-US.json +++ b/Emby.Server.Implementations/Localization/Core/en-US.json @@ -12,12 +12,12 @@ "Default": "Default", "DeviceOfflineWithName": "{0} has disconnected", "DeviceOnlineWithName": "{0} is connected", - "FailedLoginAttemptWithUserName": "Failed login attempt from {0}", + "FailedLoginAttemptWithUserName": "Failed login try from {0}", "Favorites": "Favorites", "Folders": "Folders", "Forced": "Forced", "Genres": "Genres", - "HeaderAlbumArtists": "Artist's Album", + "HeaderAlbumArtists": "Album artists", "HeaderContinueWatching": "Continue Watching", "HeaderFavoriteAlbums": "Favorite Albums", "HeaderFavoriteArtists": "Favorite Artists", diff --git a/Emby.Server.Implementations/Localization/Core/eo.json b/Emby.Server.Implementations/Localization/Core/eo.json index f92b5f673..3cadde0a0 100644 --- a/Emby.Server.Implementations/Localization/Core/eo.json +++ b/Emby.Server.Implementations/Localization/Core/eo.json @@ -1,5 +1,5 @@ { - "NotificationOptionInstallationFailed": "Instalada fiasko", + "NotificationOptionInstallationFailed": "Instalada malsukceso", "NotificationOptionAudioPlaybackStopped": "Ludado de sono haltis", "NotificationOptionAudioPlayback": "Ludado de sono lanĉis", "NameSeasonUnknown": "Sezono Nekonata", @@ -11,7 +11,7 @@ "ItemAddedWithName": "{0} aldonis al la plurmediteko", "HeaderLiveTV": "TV-etero", "HeaderContinueWatching": "Daŭrigi Spektadon", - "HeaderAlbumArtists": "Albumo de artisto", + "HeaderAlbumArtists": "Artistoj de albumo", "Folders": "Dosierujoj", "DeviceOnlineWithName": "{0} estas konektita", "Default": "Defaŭlte", @@ -48,17 +48,17 @@ "Shows": "Serioj", "HeaderFavoriteShows": "Favorataj Serioj", "TvShows": "TV-serioj", - "Favorites": "Favoratoj", + "Favorites": "Favorataj", "TaskCleanLogs": "Purigi Ĵurnalan Katalogon", - "TaskRefreshLibrary": "Skanu Plurmeditekon", + "TaskRefreshLibrary": "Skani Plurmeditekon", "ValueSpecialEpisodeName": "Speciala - {0}", - "TaskOptimizeDatabase": "Optimigi datumbazon", + "TaskOptimizeDatabase": "Optimumigi datenbazon", "TaskRefreshChannels": "Refreŝigi Kanalojn", "TaskUpdatePlugins": "Ĝisdatigi Kromprogramojn", "TaskRefreshPeople": "Refreŝigi Homojn", "TasksChannelsCategory": "Interretaj Kanaloj", "ProviderValue": "Provizanto: {0}", - "NotificationOptionPluginError": "Kromprograma malsukceso", + "NotificationOptionPluginError": "Kromprogramo malsukcesis", "MixedContent": "Miksita enhavo", "TasksApplicationCategory": "Aplikaĵo", "TasksMaintenanceCategory": "Prizorgado", @@ -75,7 +75,7 @@ "ServerNameNeedsToBeRestarted": "{0} devas esti relanĉita", "NotificationOptionVideoPlayback": "La videoludado lanĉis", "NotificationOptionServerRestartRequired": "Servila relanĉigo bezonata", - "TaskOptimizeDatabaseDescription": "Kompaktigas datenbazon kaj trunkas liberan lokon. Lanĉi ĉi tiun taskon post la teka skanado aŭ fari aliajn ŝanĝojn, kiuj implicas datenbazajn modifojn, povus plibonigi rendimenton.", + "TaskOptimizeDatabaseDescription": "Kompaktigas datenbazon kaj trunkas liberan lokon. Lanĉi ĉi tiun taskon post la plurmediteka skanado aŭ fari aliajn ŝanĝojn, kiuj implicas datenbazajn modifojn, povus plibonigi rendimenton.", "TaskUpdatePluginsDescription": "Elŝutas kaj instalas ĝisdatigojn por kromprogramojn, kiuj estas agorditaj por ĝisdatigi aŭtomate.", "TaskDownloadMissingSubtitlesDescription": "Serĉas en interreto mankantajn subtekstojn surbaze de metadatena agordaro.", "TaskRefreshPeopleDescription": "Ĝisdatigas metadatenojn por aktoroj kaj reĵisoroj en via plurmediteko.", @@ -102,9 +102,9 @@ "MessageApplicationUpdatedTo": "Jellyfin Server estis ĝisdatigita al {0}", "MessageApplicationUpdated": "Jellyfin Server estis ĝisdatigita", "TaskRefreshChannelsDescription": "Refreŝigas informon pri interretaj kanaloj.", - "TaskDownloadMissingSubtitles": "Elŝutu mankantajn subtekstojn", + "TaskDownloadMissingSubtitles": "Elŝuti mankantajn subtekstojn", "TaskCleanTranscode": "Malplenigi Transkodadan Katalogon", - "TaskRefreshChapterImages": "Eltiru Ĉapitro-Bildojn", + "TaskRefreshChapterImages": "Eltiri Ĉapitraj Bildojn", "TaskCleanCache": "Malplenigi Staplan Katalogon", "TaskCleanActivityLog": "Malplenigi Aktivecan Ĵurnalon", "PluginUpdatedWithName": "{0} estis ĝisdatigita", diff --git a/Emby.Server.Implementations/Localization/Core/es_419.json b/Emby.Server.Implementations/Localization/Core/es_419.json index a968c6dab..2ca736ad9 100644 --- a/Emby.Server.Implementations/Localization/Core/es_419.json +++ b/Emby.Server.Implementations/Localization/Core/es_419.json @@ -15,7 +15,7 @@ "HeaderFavoriteEpisodes": "Episodios favoritos", "HeaderFavoriteShows": "Programas favoritos", "HeaderContinueWatching": "Continuar viendo", - "HeaderAlbumArtists": "Artistas del álbum", + "HeaderAlbumArtists": "Artistas de álbum", "Genres": "Géneros", "Folders": "Carpetas", "Favorites": "Favoritos", @@ -29,7 +29,7 @@ "TaskRefreshChannelsDescription": "Actualiza la información de canales de Internet.", "TaskRefreshChannels": "Actualizar canales", "TaskCleanTranscodeDescription": "Elimina archivos transcodificados que tengan más de un día.", - "TaskCleanTranscode": "Limpiar directorio de transcodificado", + "TaskCleanTranscode": "Limpiar el directorio de transcodificaciones", "TaskUpdatePluginsDescription": "Descarga e instala actualizaciones para complementos que están configurados para actualizarse automáticamente.", "TaskUpdatePlugins": "Actualizar complementos", "TaskRefreshPeopleDescription": "Actualiza metadatos de actores y directores en tu biblioteca de medios.", @@ -105,7 +105,7 @@ "Inherit": "Heredar", "HomeVideos": "Videos caseros", "HeaderRecordingGroups": "Grupos de grabación", - "FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión desde {0}", + "FailedLoginAttemptWithUserName": "Intento de inicio de sesión fallido desde {0}", "DeviceOnlineWithName": "{0} está conectado", "DeviceOfflineWithName": "{0} se ha desconectado", "ChapterNameValue": "Capítulo {0}", @@ -114,10 +114,10 @@ "Application": "Aplicación", "AppDeviceValues": "App: {0}, Dispositivo: {1}", "TaskCleanActivityLogDescription": "Elimina las entradas del registro de actividad anteriores al periodo configurado.", - "TaskCleanActivityLog": "Limpiar Registro de Actividades", + "TaskCleanActivityLog": "Limpiar registro de actividades", "Undefined": "Sin definir", "Forced": "Forzado", - "Default": "Por Defecto", - "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" + "Default": "Por defecto", + "TaskOptimizeDatabaseDescription": "Compacta la base de datos y libera espacio. Ejecutar esta tarea después de escanear la biblioteca o hacer otros cambios que impliquen modificaciones en la base de datos puede mejorar el rendimiento.", + "TaskOptimizeDatabase": "Optimizar base de datos" } diff --git a/Emby.Server.Implementations/Localization/Core/et.json b/Emby.Server.Implementations/Localization/Core/et.json index c3596ecf1..8db6a0b38 100644 --- a/Emby.Server.Implementations/Localization/Core/et.json +++ b/Emby.Server.Implementations/Localization/Core/et.json @@ -17,7 +17,7 @@ "TaskCleanLogsDescription": "Kustutab logifailid, mis on vanemad kui {0} päeva.", "TaskCleanLogs": "Puhasta logikataloog", "TaskRefreshLibraryDescription": "Otsib meedikogust uusi faile ja värskendab metaandmeid.", - "Collections": "Kollektsioonid", + "Collections": "Kogumikud", "TaskRefreshLibrary": "Skaneeri meediakogu", "TaskRefreshChapterImagesDescription": "Loob peatükkidega videote jaoks pisipildid.", "TaskRefreshChapterImages": "Eralda peatükipildid", @@ -32,9 +32,9 @@ "ValueSpecialEpisodeName": "Eriepisood - {0}", "ValueHasBeenAddedToLibrary": "{0} lisati meediakogusse", "UserStartedPlayingItemWithValues": "{0} taasesitab {1} serveris {2}", - "UserPasswordChangedWithName": "Kasutaja {0} parooli on muudetud", - "UserLockedOutWithName": "Kasutaja {0} on lukustatud", - "UserDeletedWithName": "Kasutaja {0} on kustutatud", + "UserPasswordChangedWithName": "Kasutaja {0} parool muudeti", + "UserLockedOutWithName": "Kasutaja {0} lukustati", + "UserDeletedWithName": "Kasutaja {0} kustutati", "UserCreatedWithName": "Kasutaja {0} on loodud", "ScheduledTaskStartedWithName": "{0} käivitati", "ProviderValue": "Allikas: {0}", @@ -54,9 +54,9 @@ "Plugin": "Plugin", "Playlists": "Pleilistid", "Photos": "Fotod", - "NotificationOptionVideoPlaybackStopped": "Video taasesitus on peatatud", + "NotificationOptionVideoPlaybackStopped": "Video taasesitus lõppes", "NotificationOptionVideoPlayback": "Video taasesitus algas", - "NotificationOptionUserLockedOut": "Kasutaja on lukustatud", + "NotificationOptionUserLockedOut": "Kasutaja lukustati", "NotificationOptionTaskFailed": "Ajastatud ülesanne nurjus", "NotificationOptionServerRestartRequired": "Vajalik on serveri taaskäivitamine", "NotificationOptionPluginUpdateInstalled": "Paigaldati plugina uuendus", @@ -66,7 +66,7 @@ "NotificationOptionNewLibraryContent": "Lisati uut sisu", "NotificationOptionInstallationFailed": "Paigaldamine nurjus", "NotificationOptionCameraImageUploaded": "Kaamera pilt on üles laaditud", - "NotificationOptionAudioPlaybackStopped": "Heli taasesitus peatati", + "NotificationOptionAudioPlaybackStopped": "Heli taasesitus lõppes", "NotificationOptionAudioPlayback": "Heli taasesitus algas", "NotificationOptionApplicationUpdateInstalled": "Rakenduse uuendus paigaldati", "NotificationOptionApplicationUpdateAvailable": "Rakenduse uuendus on saadaval", @@ -97,12 +97,12 @@ "HeaderFavoriteArtists": "Lemmikesitajad", "HeaderFavoriteAlbums": "Lemmikalbumid", "HeaderContinueWatching": "Jätka vaatamist", - "HeaderAlbumArtists": "Albumi esitaja", + "HeaderAlbumArtists": "Albumi esitajad", "Genres": "Žanrid", "Forced": "Sunnitud", "Folders": "Kaustad", "Favorites": "Lemmikud", - "FailedLoginAttemptWithUserName": "Ebaõnnestunud sisselogimiskatse kasutajalt {0}", + "FailedLoginAttemptWithUserName": "{0} - sisselogimine nurjus", "DeviceOnlineWithName": "{0} on ühendatud", "DeviceOfflineWithName": "{0} katkestas ühenduse", "Default": "Vaikimisi", @@ -114,5 +114,10 @@ "Artists": "Esitajad", "Application": "Rakendus", "AppDeviceValues": "Rakendus: {0}, seade: {1}", - "Albums": "Albumid" + "Albums": "Albumid", + "UserOfflineFromDevice": "{0} katkestas ühenduse seadmega {1}", + "SubtitleDownloadFailureFromForItem": "Subtiitrite allalaadimine {0} > {1} nurjus", + "UserPolicyUpdatedWithName": "Kasutaja {0} õigusi värskendati", + "UserStoppedPlayingItemWithValues": "{0} lõpetas {1} taasesituse seadmes {2}", + "UserOnlineFromDevice": "{0} on ühendatud seadmest {1}" } diff --git a/Emby.Server.Implementations/Localization/Core/fa.json b/Emby.Server.Implementations/Localization/Core/fa.json index 8ab657e5b..3d3b3533f 100644 --- a/Emby.Server.Implementations/Localization/Core/fa.json +++ b/Emby.Server.Implementations/Localization/Core/fa.json @@ -6,7 +6,7 @@ "AuthenticationSucceededWithUserName": "{0} با موفقیت تایید اعتبار شد", "Books": "کتابها", "CameraImageUploadedFrom": "یک عکس جدید از دوربین ارسال شده است {0}", - "Channels": "کانالها", + "Channels": "کانالها", "ChapterNameValue": "قسمت {0}", "Collections": "مجموعهها", "DeviceOfflineWithName": "ارتباط {0} قطع شد", @@ -37,7 +37,7 @@ "MessageNamedServerConfigurationUpdatedWithValue": "پکربندی بخش {0} سرور بروزرسانی شد", "MessageServerConfigurationUpdated": "پیکربندی سرور بروزرسانی شد", "MixedContent": "محتوای مخلوط", - "Movies": "فیلمها", + "Movies": "فیلم ها", "Music": "موسیقی", "MusicVideos": "موزیک ویدیوها", "NameInstallFailed": "{0} نصب با مشکل مواجه شد", diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json index 1265b6ef5..d60955d5f 100644 --- a/Emby.Server.Implementations/Localization/Core/fr.json +++ b/Emby.Server.Implementations/Localization/Core/fr.json @@ -15,7 +15,7 @@ "Favorites": "Favoris", "Folders": "Dossiers", "Genres": "Genres", - "HeaderAlbumArtists": "Artistes de l'album", + "HeaderAlbumArtists": "Artistes d'album", "HeaderContinueWatching": "Continuer à regarder", "HeaderFavoriteAlbums": "Albums favoris", "HeaderFavoriteArtists": "Artistes préférés", diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json index 85ab1511a..acde84aaf 100644 --- a/Emby.Server.Implementations/Localization/Core/hu.json +++ b/Emby.Server.Implementations/Localization/Core/hu.json @@ -11,11 +11,11 @@ "Collections": "Gyűjtemények", "DeviceOfflineWithName": "{0} kijelentkezett", "DeviceOnlineWithName": "{0} belépett", - "FailedLoginAttemptWithUserName": "Sikertelen bejelentkezési kísérlet tőle: {0}", + "FailedLoginAttemptWithUserName": "Sikertelen bejelentkezési kísérlet innen: {0}", "Favorites": "Kedvencek", "Folders": "Könyvtárak", "Genres": "Műfajok", - "HeaderAlbumArtists": "Előadó albumai", + "HeaderAlbumArtists": "Album előadó(k)", "HeaderContinueWatching": "Megtekintés folytatása", "HeaderFavoriteAlbums": "Kedvenc albumok", "HeaderFavoriteArtists": "Kedvenc előadók", diff --git a/Emby.Server.Implementations/Localization/Core/id.json b/Emby.Server.Implementations/Localization/Core/id.json index ba3513870..37d59abd9 100644 --- a/Emby.Server.Implementations/Localization/Core/id.json +++ b/Emby.Server.Implementations/Localization/Core/id.json @@ -7,10 +7,10 @@ "MessageApplicationUpdated": "Jellyfin Server sudah diperbarui", "Latest": "Terbaru", "LabelIpAddressValue": "Alamat IP: {0}", - "ItemRemovedWithName": "{0} sudah dikeluarkan dari pustaka", + "ItemRemovedWithName": "{0} sudah dihapus dari pustaka", "ItemAddedWithName": "{0} telah dimasukkan ke dalam pustaka", - "Inherit": "Warisan", - "HomeVideos": "Video Rumah", + "Inherit": "Warisi", + "HomeVideos": "Video Rumahan", "HeaderRecordingGroups": "Grup Rekaman", "HeaderNextUp": "Selanjutnya", "HeaderLiveTV": "TV Live", @@ -73,7 +73,7 @@ "NotificationOptionCameraImageUploaded": "Gambar kamera terunggah", "NotificationOptionApplicationUpdateInstalled": "Pembaruan aplikasi terpasang", "NotificationOptionApplicationUpdateAvailable": "Pembaruan aplikasi tersedia", - "NewVersionIsAvailable": "Versi baru dari Jellyfin Server tersedia untuk diunduh.", + "NewVersionIsAvailable": "Versi baru dari Jellyfin Server sudah tersedia untuk diunduh.", "NameSeasonUnknown": "Musim tak diketahui", "NameSeasonNumber": "Musim {0}", "NameInstallFailed": "{0} penginstalan gagal", @@ -117,5 +117,7 @@ "TaskCleanActivityLog": "Bersihkan Log Aktivitas", "Undefined": "Tidak terdefinisi", "Forced": "Dipaksa", - "Default": "Bawaan" + "Default": "Bawaan", + "TaskOptimizeDatabaseDescription": "Rapihkan basis data dan membersihkan ruang kosong. Menjalankan tugas ini setelah memindai pustaka atau melakukan perubahan lain yang menyiratkan modifikasi basis data dapat meningkatkan kinerja.", + "TaskOptimizeDatabase": "Optimalkan basis data" } diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json index 5e28cf09f..4c4de4999 100644 --- a/Emby.Server.Implementations/Localization/Core/it.json +++ b/Emby.Server.Implementations/Localization/Core/it.json @@ -15,7 +15,7 @@ "Favorites": "Preferiti", "Folders": "Cartelle", "Genres": "Generi", - "HeaderAlbumArtists": "Artisti dell'Album", + "HeaderAlbumArtists": "Artisti dell'album", "HeaderContinueWatching": "Continua a guardare", "HeaderFavoriteAlbums": "Album Preferiti", "HeaderFavoriteArtists": "Artisti Preferiti", diff --git a/Emby.Server.Implementations/Localization/Core/kk.json b/Emby.Server.Implementations/Localization/Core/kk.json index d28564a7c..1b4a18deb 100644 --- a/Emby.Server.Implementations/Localization/Core/kk.json +++ b/Emby.Server.Implementations/Localization/Core/kk.json @@ -15,7 +15,7 @@ "Favorites": "Tañdaulylar", "Folders": "Qaltalar", "Genres": "Janrlar", - "HeaderAlbumArtists": "Oryndauşynyñ älbomy", + "HeaderAlbumArtists": "Älbom oryndauşylary", "HeaderContinueWatching": "Qaraudy jalğastyru", "HeaderFavoriteAlbums": "Tañdauly älbomdar", "HeaderFavoriteArtists": "Tañdauly oryndauşylar", diff --git a/Emby.Server.Implementations/Localization/Core/mk.json b/Emby.Server.Implementations/Localization/Core/mk.json index b780ef498..6baedcb2d 100644 --- a/Emby.Server.Implementations/Localization/Core/mk.json +++ b/Emby.Server.Implementations/Localization/Core/mk.json @@ -50,7 +50,7 @@ "HeaderFavoriteEpisodes": "Омилени Епизоди", "HeaderFavoriteArtists": "Омилени Изведувачи", "HeaderFavoriteAlbums": "Омилени Албуми", - "HeaderContinueWatching": "Продолжи со гледање", + "HeaderContinueWatching": "Продолжи со Гледање", "HeaderAlbumArtists": "Изведувачи од Албуми", "Genres": "Жанрови", "Folders": "Папки", diff --git a/Emby.Server.Implementations/Localization/Core/mn.json b/Emby.Server.Implementations/Localization/Core/mn.json new file mode 100644 index 000000000..7421d42fb --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/mn.json @@ -0,0 +1,14 @@ +{ + "Books": "Номууд", + "HeaderNextUp": "Дараах", + "HeaderContinueWatching": "Үргэлжлүүлэн үзэх", + "Songs": "Дуунууд", + "Playlists": "Тоглуулах жагсаалт", + "Movies": "Кино", + "Latest": "Сүүлийн үеийн", + "Genres": "Төрөл зүйл", + "Favorites": "Дуртай", + "Collections": "Багц", + "Artists": "Зураачуд", + "Albums": "Цомгууд" +} diff --git a/Emby.Server.Implementations/Localization/Core/ms.json b/Emby.Server.Implementations/Localization/Core/ms.json index b2dcf270c..4dcb99293 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", "Music": "Muzik", - "MusicVideos": "Muzik video", + "MusicVideos": "", "NameInstallFailed": "{0} pemasangan gagal", "NameSeasonNumber": "Musim {0}", "NameSeasonUnknown": "Musim Tidak Diketahui", diff --git a/Emby.Server.Implementations/Localization/Core/pl.json b/Emby.Server.Implementations/Localization/Core/pl.json index e8a32a13e..4fa8d2bb4 100644 --- a/Emby.Server.Implementations/Localization/Core/pl.json +++ b/Emby.Server.Implementations/Localization/Core/pl.json @@ -15,7 +15,7 @@ "Favorites": "Ulubione", "Folders": "Foldery", "Genres": "Gatunki", - "HeaderAlbumArtists": "Album artysty", + "HeaderAlbumArtists": "Wykonawcy albumów", "HeaderContinueWatching": "Kontynuuj odtwarzanie", "HeaderFavoriteAlbums": "Ulubione albumy", "HeaderFavoriteArtists": "Ulubieni wykonawcy", @@ -47,7 +47,7 @@ "NotificationOptionApplicationUpdateAvailable": "Dostępna aktualizacja aplikacji", "NotificationOptionApplicationUpdateInstalled": "Zaktualizowano aplikację", "NotificationOptionAudioPlayback": "Rozpoczęto odtwarzanie muzyki", - "NotificationOptionAudioPlaybackStopped": "Odtwarzane dźwięku zatrzymane", + "NotificationOptionAudioPlaybackStopped": "Odtwarzanie dźwięku zatrzymane", "NotificationOptionCameraImageUploaded": "Przekazano obraz z urządzenia przenośnego", "NotificationOptionInstallationFailed": "Nieudana instalacja", "NotificationOptionNewLibraryContent": "Dodano nową zawartość", @@ -98,7 +98,7 @@ "TaskRefreshChannels": "Odśwież kanały", "TaskCleanTranscodeDescription": "Usuwa transkodowane pliki starsze niż 1 dzień.", "TaskCleanTranscode": "Wyczyść folder transkodowania", - "TaskUpdatePluginsDescription": "Pobiera i instaluje aktualizacje dla pluginów które są skonfigurowane do automatycznej aktualizacji.", + "TaskUpdatePluginsDescription": "Pobiera i instaluje aktualizacje dla pluginów, które są skonfigurowane do automatycznej aktualizacji.", "TaskUpdatePlugins": "Aktualizuj pluginy", "TaskRefreshPeopleDescription": "Odświeża metadane o aktorów i reżyserów w Twojej bibliotece mediów.", "TaskRefreshPeople": "Odśwież obsadę", diff --git a/Emby.Server.Implementations/Localization/Core/pr.json b/Emby.Server.Implementations/Localization/Core/pr.json index e3a3bfaf1..81aa996d9 100644 --- a/Emby.Server.Implementations/Localization/Core/pr.json +++ b/Emby.Server.Implementations/Localization/Core/pr.json @@ -1,5 +1,7 @@ { "Books": "Libros", "AuthenticationSucceededWithUserName": "{0} autentificado correctamente", - "Artists": "Artistas" + "Artists": "Artistas", + "Songs": "Shantees", + "Albums": "Ships" } diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json index 8c41edf96..8870de168 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-PT.json +++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json @@ -15,7 +15,7 @@ "Favorites": "Favoritos", "Folders": "Pastas", "Genres": "Géneros", - "HeaderAlbumArtists": "Artistas do Álbum", + "HeaderAlbumArtists": "Álbum do Artista", "HeaderContinueWatching": "Continuar a Ver", "HeaderFavoriteAlbums": "Álbuns Favoritos", "HeaderFavoriteArtists": "Artistas Favoritos", @@ -39,7 +39,7 @@ "MixedContent": "Conteúdo Misto", "Movies": "Filmes", "Music": "Música", - "MusicVideos": "Videoclips", + "MusicVideos": "Videoclipes", "NameInstallFailed": "{0} falha na instalação", "NameSeasonNumber": "Temporada {0}", "NameSeasonUnknown": "Temporada Desconhecida", @@ -118,5 +118,7 @@ "TaskCleanActivityLog": "Limpar registo de atividade", "Undefined": "Indefinido", "Forced": "Forçado", - "Default": "Padrão" + "Default": "Padrão", + "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.", + "TaskOptimizeDatabase": "Otimizar base de dados" } diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json index cd016b51b..2d7163275 100644 --- a/Emby.Server.Implementations/Localization/Core/ru.json +++ b/Emby.Server.Implementations/Localization/Core/ru.json @@ -31,7 +31,7 @@ "ItemRemovedWithName": "{0} - изъято из медиатеки", "LabelIpAddressValue": "IP-адрес: {0}", "LabelRunningTimeValue": "Длительность: {0}", - "Latest": "Последнее", + "Latest": "Крайнее", "MessageApplicationUpdated": "Jellyfin Server был обновлён", "MessageApplicationUpdatedTo": "Jellyfin Server был обновлён до {0}", "MessageNamedServerConfigurationUpdatedWithValue": "Конфигурация сервера (раздел {0}) была обновлена", @@ -119,6 +119,6 @@ "Undefined": "Не определено", "Forced": "Форсир-ые", "Default": "По умолчанию", - "TaskOptimizeDatabaseDescription": "Сжимает базу данных и обрезает свободное место. Выполнение этой задачи после сканирования библиотеки или внесения других изменений, предполагающих модификации базы данных, может повысить производительность.", + "TaskOptimizeDatabaseDescription": "Сжимает базу данных и вырезает свободные места. Выполнение этой задачи после сканирования библиотеки или внесения других изменений, предполагающих модификации базы данных, может повысить производительность.", "TaskOptimizeDatabase": "Оптимизировать базу данных" } diff --git a/Emby.Server.Implementations/Localization/Core/sk.json b/Emby.Server.Implementations/Localization/Core/sk.json index ad90bd813..37da7d5ab 100644 --- a/Emby.Server.Implementations/Localization/Core/sk.json +++ b/Emby.Server.Implementations/Localization/Core/sk.json @@ -39,7 +39,7 @@ "MixedContent": "Zmiešaný obsah", "Movies": "Filmy", "Music": "Hudba", - "MusicVideos": "Hudobné videá", + "MusicVideos": "Hudobné videoklipy", "NameInstallFailed": "Inštalácia {0} zlyhala", "NameSeasonNumber": "Séria {0}", "NameSeasonUnknown": "Neznáma séria", diff --git a/Emby.Server.Implementations/Localization/Core/sq.json b/Emby.Server.Implementations/Localization/Core/sq.json index 066ef5587..2766dab06 100644 --- a/Emby.Server.Implementations/Localization/Core/sq.json +++ b/Emby.Server.Implementations/Localization/Core/sq.json @@ -74,7 +74,7 @@ "NameSeasonUnknown": "Sezon i panjohur", "NameSeasonNumber": "Sezoni {0}", "NameInstallFailed": "Instalimi i {0} dështoi", - "MusicVideos": "Video muzikore", + "MusicVideos": "Video Muzikore", "Music": "Muzikë", "Movies": "Filmat", "MixedContent": "Përmbajtje e përzier", @@ -96,7 +96,7 @@ "HeaderFavoriteArtists": "Artistët e preferuar", "HeaderFavoriteAlbums": "Albumet e preferuar", "HeaderContinueWatching": "Vazhdo të shikosh", - "HeaderAlbumArtists": "Artistët e Albumeve", + "HeaderAlbumArtists": "Artistët e albumeve", "Genres": "Zhanret", "Folders": "Skedarët", "Favorites": "Të preferuarat", diff --git a/Emby.Server.Implementations/Localization/Core/sr.json b/Emby.Server.Implementations/Localization/Core/sr.json index 15fb34186..2d6f3d53d 100644 --- a/Emby.Server.Implementations/Localization/Core/sr.json +++ b/Emby.Server.Implementations/Localization/Core/sr.json @@ -50,7 +50,7 @@ "NameSeasonUnknown": "Непозната сезона", "NameSeasonNumber": "Сезона {0}", "NameInstallFailed": "Инсталација {0} није успела", - "MusicVideos": "Музички спотови", + "MusicVideos": "Музички видео", "Music": "Музика", "Movies": "Филмови", "MixedContent": "Мешовит садржај", diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json index 6c772c6a2..1cef30b6c 100644 --- a/Emby.Server.Implementations/Localization/Core/sv.json +++ b/Emby.Server.Implementations/Localization/Core/sv.json @@ -15,7 +15,7 @@ "Favorites": "Favoriter", "Folders": "Mappar", "Genres": "Genrer", - "HeaderAlbumArtists": "Artistens album", + "HeaderAlbumArtists": "Albumsartister", "HeaderContinueWatching": "Fortsätt kolla", "HeaderFavoriteAlbums": "Favoritalbum", "HeaderFavoriteArtists": "Favoritartister", diff --git a/Emby.Server.Implementations/Localization/Core/ta.json b/Emby.Server.Implementations/Localization/Core/ta.json index d6e9aa8e5..e3a993625 100644 --- a/Emby.Server.Implementations/Localization/Core/ta.json +++ b/Emby.Server.Implementations/Localization/Core/ta.json @@ -85,7 +85,7 @@ "HeaderFavoriteArtists": "பிடித்த கலைஞர்கள்", "HeaderFavoriteAlbums": "பிடித்த ஆல்பங்கள்", "HeaderContinueWatching": "தொடர்ந்து பார்", - "HeaderAlbumArtists": "இசைக் கலைஞர்கள்", + "HeaderAlbumArtists": "கலைஞரின் ஆல்பம்", "Genres": "வகைகள்", "Favorites": "பிடித்தவை", "ChapterNameValue": "அத்தியாயம் {0}", diff --git a/Emby.Server.Implementations/Localization/Core/te.json b/Emby.Server.Implementations/Localization/Core/te.json new file mode 100644 index 000000000..a9a8ceae0 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/te.json @@ -0,0 +1,23 @@ +{ + "ValueSpecialEpisodeName": "ప్రత్యేక - {0}", + "Sync": "సమకాలీకరించు", + "Songs": "పాటలు", + "Shows": "ప్రదర్శనలు", + "Playlists": "ప్లేజాబితాలు", + "Photos": "ఫోటోలు", + "MusicVideos": "మ్యూజిక్ వీడియోలు", + "Music": "సంగీతం", + "Movies": "సినిమాలు", + "HeaderContinueWatching": "చూడటం కొనసాగించండి", + "HeaderAlbumArtists": "ఆల్బమ్ కళాకారులు", + "Genres": "శైలులు", + "Forced": "బలవంతంగా", + "Folders": "ఫోల్డర్లు", + "Favorites": "ఇష్టమైనవి", + "Default": "డిఫాల్ట్", + "Collections": "సేకరణలు", + "Channels": "ఛానెల్లు", + "Books": "పుస్తకాలు", + "Artists": "కళాకారులు", + "Albums": "ఆల్బమ్లు" +} diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json index e661299c4..8fadb88ac 100644 --- a/Emby.Server.Implementations/Localization/Core/tr.json +++ b/Emby.Server.Implementations/Localization/Core/tr.json @@ -8,7 +8,7 @@ "CameraImageUploadedFrom": "{0} 'den yeni bir kamera resmi yüklendi", "Channels": "Kanallar", "ChapterNameValue": "Bölüm {0}", - "Collections": "Koleksiyon", + "Collections": "Koleksiyonlar", "DeviceOfflineWithName": "{0} bağlantısı kesildi", "DeviceOnlineWithName": "{0} bağlı", "FailedLoginAttemptWithUserName": "{0} adresinden giriş başarısız oldu", diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json index e99ed6f0b..1c7d73615 100644 --- a/Emby.Server.Implementations/Localization/Core/uk.json +++ b/Emby.Server.Implementations/Localization/Core/uk.json @@ -16,7 +16,7 @@ "HeaderFavoriteArtists": "Улюблені виконавці", "HeaderFavoriteAlbums": "Улюблені альбоми", "HeaderContinueWatching": "Продовжити перегляд", - "HeaderAlbumArtists": "Виконавці альбомів", + "HeaderAlbumArtists": "Виконавці альбому", "Genres": "Жанри", "Folders": "Каталоги", "Favorites": "Улюблені", diff --git a/Emby.Server.Implementations/Localization/Core/vi.json b/Emby.Server.Implementations/Localization/Core/vi.json index 33aa0eea0..b7ece8d5f 100644 --- a/Emby.Server.Implementations/Localization/Core/vi.json +++ b/Emby.Server.Implementations/Localization/Core/vi.json @@ -3,7 +3,7 @@ "Favorites": "Yêu Thích", "Folders": "Thư Mục", "Genres": "Thể Loại", - "HeaderAlbumArtists": "Album Nghệ sĩ", + "HeaderAlbumArtists": "Album nghệ sĩ", "HeaderContinueWatching": "Xem Tiếp", "HeaderLiveTV": "TV Trực Tiếp", "Movies": "Phim", @@ -62,11 +62,11 @@ "PluginUninstalledWithName": "{0} đã được gỡ bỏ", "PluginInstalledWithName": "{0} đã được cài đặt", "Plugin": "Plugin", - "NotificationOptionVideoPlaybackStopped": "Phát lại video đã dừng", + "NotificationOptionVideoPlaybackStopped": "Đã dừng phát lại video", "NotificationOptionVideoPlayback": "Đã bắt đầu phát lại video", "NotificationOptionUserLockedOut": "Người dùng bị khóa", "NotificationOptionTaskFailed": "Lỗi tác vụ đã lên lịch", - "NotificationOptionServerRestartRequired": "Yêu cầu khởi động lại Server", + "NotificationOptionServerRestartRequired": "Yêu cầu khởi động lại máy chủ", "NotificationOptionPluginUpdateInstalled": "Cập nhật Plugin đã được cài đặt", "NotificationOptionPluginUninstalled": "Đã gỡ bỏ Plugin", "NotificationOptionPluginInstalled": "Đã cài đặt Plugin", @@ -75,7 +75,7 @@ "NotificationOptionInstallationFailed": "Cài đặt thất bại", "NotificationOptionCameraImageUploaded": "Đã tải lên hình ảnh máy ảnh", "NotificationOptionAudioPlaybackStopped": "Phát lại âm thanh đã dừng", - "NotificationOptionAudioPlayback": "Phát lại âm thanh đã bắt đầu", + "NotificationOptionAudioPlayback": "Đã bắt đầu phát lại âm thanh", "NotificationOptionApplicationUpdateInstalled": "Bản cập nhật ứng dụng đã được cài đặt", "NotificationOptionApplicationUpdateAvailable": "Bản cập nhật ứng dụng hiện sẵn có", "NewVersionIsAvailable": "Một phiên bản mới của Jellyfin Server sẵn có để tải.", @@ -95,7 +95,7 @@ "ItemRemovedWithName": "{0} đã xóa khỏi thư viện", "ItemAddedWithName": "{0} được thêm vào thư viện", "Inherit": "Thừa hưởng", - "HomeVideos": "Video nhà", + "HomeVideos": "Video Nhà", "HeaderRecordingGroups": "Nhóm Ghi Video", "HeaderNextUp": "Tiếp Theo", "HeaderFavoriteSongs": "Bài Hát Yêu Thích", @@ -103,7 +103,7 @@ "HeaderFavoriteEpisodes": "Tập Phim Yêu Thích", "HeaderFavoriteArtists": "Nghệ Sĩ Yêu Thích", "HeaderFavoriteAlbums": "Album Ưa Thích", - "FailedLoginAttemptWithUserName": "Nỗ lực đăng nhập thất bại từ {0}", + "FailedLoginAttemptWithUserName": "Đăng nhập không thành công thử từ {0}", "DeviceOnlineWithName": "{0} đã kết nối", "DeviceOfflineWithName": "{0} đã ngắt kết nối", "ChapterNameValue": "Phân Cảnh {0}", diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json index f9df62724..ac4eb644b 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-CN.json +++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json @@ -11,7 +11,7 @@ "Collections": "合集", "DeviceOfflineWithName": "{0} 已断开", "DeviceOnlineWithName": "{0} 已连接", - "FailedLoginAttemptWithUserName": "来自 {0} 的失败登入", + "FailedLoginAttemptWithUserName": "从 {0} 尝试登录失败", "Favorites": "我的最爱", "Folders": "文件夹", "Genres": "风格", diff --git a/Emby.Server.Implementations/Localization/Core/zu.json b/Emby.Server.Implementations/Localization/Core/zu.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/zu.json @@ -0,0 +1 @@ +{} diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index 03919197e..9cdbbb6a3 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -372,43 +372,76 @@ namespace Emby.Server.Implementations.Localization /// <inheritdoc /> public IEnumerable<LocalizationOption> GetLocalizationOptions() { - yield return new LocalizationOption("Arabic", "ar"); - yield return new LocalizationOption("Bulgarian (Bulgaria)", "bg-BG"); - yield return new LocalizationOption("Catalan", "ca"); - yield return new LocalizationOption("Chinese Simplified", "zh-CN"); - yield return new LocalizationOption("Chinese Traditional", "zh-TW"); - yield return new LocalizationOption("Croatian", "hr"); - yield return new LocalizationOption("Czech", "cs"); - yield return new LocalizationOption("Danish", "da"); - yield return new LocalizationOption("Dutch", "nl"); + yield return new LocalizationOption("Afrikaans", "af"); + yield return new LocalizationOption("العربية", "ar"); + yield return new LocalizationOption("Беларуская", "be"); + yield return new LocalizationOption("Български", "bg-BG"); + yield return new LocalizationOption("বাংলা (বাংলাদেশ)", "bn"); + yield return new LocalizationOption("Català", "ca"); + yield return new LocalizationOption("Čeština", "cs"); + yield return new LocalizationOption("Cymraeg", "cy"); + yield return new LocalizationOption("Dansk", "da"); + yield return new LocalizationOption("Deutsch", "de"); yield return new LocalizationOption("English (United Kingdom)", "en-GB"); - yield return new LocalizationOption("English (United States)", "en-US"); - yield return new LocalizationOption("French", "fr"); - yield return new LocalizationOption("French (Canada)", "fr-CA"); - yield return new LocalizationOption("German", "de"); - yield return new LocalizationOption("Greek", "el"); - yield return new LocalizationOption("Hebrew", "he"); - yield return new LocalizationOption("Hungarian", "hu"); - yield return new LocalizationOption("Italian", "it"); - yield return new LocalizationOption("Kazakh", "kk"); - yield return new LocalizationOption("Korean", "ko"); - yield return new LocalizationOption("Lithuanian", "lt-LT"); - yield return new LocalizationOption("Malay", "ms"); - yield return new LocalizationOption("Norwegian Bokmål", "nb"); - yield return new LocalizationOption("Persian", "fa"); - yield return new LocalizationOption("Polish", "pl"); - yield return new LocalizationOption("Portuguese (Brazil)", "pt-BR"); - yield return new LocalizationOption("Portuguese (Portugal)", "pt-PT"); - yield return new LocalizationOption("Russian", "ru"); - yield return new LocalizationOption("Slovak", "sk"); - yield return new LocalizationOption("Slovenian (Slovenia)", "sl-SI"); - yield return new LocalizationOption("Spanish", "es"); - yield return new LocalizationOption("Spanish (Argentina)", "es-AR"); - yield return new LocalizationOption("Spanish (Mexico)", "es-MX"); - yield return new LocalizationOption("Swedish", "sv"); - yield return new LocalizationOption("Swiss German", "gsw"); - yield return new LocalizationOption("Turkish", "tr"); + yield return new LocalizationOption("English", "en-US"); + yield return new LocalizationOption("Ελληνικά", "el"); + yield return new LocalizationOption("Esperanto", "eo"); + yield return new LocalizationOption("Español", "es"); + yield return new LocalizationOption("Español americano", "es_419"); + yield return new LocalizationOption("Español (Argentina)", "es-AR"); + yield return new LocalizationOption("Español (Dominicana)", "es_DO"); + yield return new LocalizationOption("Español (México)", "es-MX"); + yield return new LocalizationOption("Eesti", "et"); + yield return new LocalizationOption("فارسی", "fa"); + yield return new LocalizationOption("Suomi", "fi"); + yield return new LocalizationOption("Filipino", "fil"); + yield return new LocalizationOption("Français", "fr"); + yield return new LocalizationOption("Français (Canada)", "fr-CA"); + yield return new LocalizationOption("Galego", "gl"); + yield return new LocalizationOption("Schwiizerdütsch", "gsw"); + yield return new LocalizationOption("עִבְרִית", "he"); + yield return new LocalizationOption("हिन्दी", "hi"); + yield return new LocalizationOption("Hrvatski", "hr"); + yield return new LocalizationOption("Magyar", "hu"); + yield return new LocalizationOption("Bahasa Indonesia", "id"); + yield return new LocalizationOption("Íslenska", "is"); + yield return new LocalizationOption("Italiano", "it"); + yield return new LocalizationOption("日本語", "ja"); + yield return new LocalizationOption("Qazaqşa", "kk"); + yield return new LocalizationOption("한국어", "ko"); + yield return new LocalizationOption("Lietuvių", "lt"); + yield return new LocalizationOption("Latviešu", "lv"); + yield return new LocalizationOption("Македонски", "mk"); + yield return new LocalizationOption("മലയാളം", "ml"); + yield return new LocalizationOption("मराठी", "mr"); + yield return new LocalizationOption("Bahasa Melayu", "ms"); + yield return new LocalizationOption("Norsk bokmål", "nb"); + yield return new LocalizationOption("नेपाली", "ne"); + yield return new LocalizationOption("Nederlands", "nl"); + yield return new LocalizationOption("Norsk nynorsk", "nn"); + yield return new LocalizationOption("ਪੰਜਾਬੀ", "pa"); + yield return new LocalizationOption("Polski", "pl"); + yield return new LocalizationOption("Pirate", "pr"); + yield return new LocalizationOption("Português", "pt"); + yield return new LocalizationOption("Português (Brasil)", "pt-BR"); + yield return new LocalizationOption("Português (Portugal)", "pt-PT"); + yield return new LocalizationOption("Românește", "ro"); + yield return new LocalizationOption("Русский", "ru"); + yield return new LocalizationOption("Slovenčina", "sk"); + yield return new LocalizationOption("Slovenščina", "sl-SI"); + yield return new LocalizationOption("Shqip", "sq"); + yield return new LocalizationOption("Српски", "sr"); + yield return new LocalizationOption("Svenska", "sv"); + yield return new LocalizationOption("தமிழ்", "ta"); + yield return new LocalizationOption("తెలుగు", "te"); + yield return new LocalizationOption("ภาษาไทย", "th"); + yield return new LocalizationOption("Türkçe", "tr"); + yield return new LocalizationOption("Українська", "uk"); + yield return new LocalizationOption("اُردُو", "ur_PK"); yield return new LocalizationOption("Tiếng Việt", "vi"); + yield return new LocalizationOption("汉语 (简化字)", "zh-CN"); + yield return new LocalizationOption("漢語 (繁体字)", "zh-TW"); + yield return new LocalizationOption("廣東話 (香港)", "zh-HK"); } } } diff --git a/Emby.Server.Implementations/Localization/countries.json b/Emby.Server.Implementations/Localization/countries.json index b08a3ae79..22ffc5e09 100644 --- a/Emby.Server.Implementations/Localization/countries.json +++ b/Emby.Server.Implementations/Localization/countries.json @@ -630,7 +630,7 @@ "TwoLetterISORegionName": "MD" }, { - "DisplayName": "Réunion", + "DisplayName": "Réunion", "Name": "RE", "ThreeLetterISORegionName": "REU", "TwoLetterISORegionName": "RE" diff --git a/Emby.Server.Implementations/Localization/iso6392.txt b/Emby.Server.Implementations/Localization/iso6392.txt index 488901822..66fba3330 100644 --- a/Emby.Server.Implementations/Localization/iso6392.txt +++ b/Emby.Server.Implementations/Localization/iso6392.txt @@ -349,7 +349,8 @@ pli||pi|Pali|pali pol||pl|Polish|polonais pon|||Pohnpeian|pohnpei por||pt|Portuguese|portugais -pob||pt-br|Portuguese (Brazil)|portugais +pop||pt-pt|Portuguese (Portugal)|portugais (pt-pt) +pob||pt-br|Portuguese (Brazil)|portugais (pt-br) pra|||Prakrit languages|prâkrit, langues pro|||Provençal, Old (to 1500)|provençal ancien (jusqu'à 1500) pus||ps|Pushto; Pashto|pachto diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index b07798fa4..9481e26f7 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -527,7 +527,7 @@ namespace Emby.Server.Implementations.Playlists var relativeUri = folderUri.MakeRelativeUri(fileAbsoluteUri); string relativePath = Uri.UnescapeDataString(relativeUri.ToString()); - if (fileAbsoluteUri.Scheme.Equals("file", StringComparison.CurrentCultureIgnoreCase)) + if (fileAbsoluteUri.Scheme.Equals("file", StringComparison.OrdinalIgnoreCase)) { relativePath = relativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); } diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs index d52c0b2a1..d70a15dbc 100644 --- a/Emby.Server.Implementations/Plugins/PluginManager.cs +++ b/Emby.Server.Implementations/Plugins/PluginManager.cs @@ -126,7 +126,8 @@ namespace Emby.Server.Implementations.Plugins { assembly = Assembly.LoadFrom(file); - assembly.GetExportedTypes(); + // Load all required types to verify that the plugin will load + assembly.GetTypes(); } catch (FileLoadException ex) { @@ -134,7 +135,7 @@ namespace Emby.Server.Implementations.Plugins ChangePluginState(plugin, PluginStatus.Malfunctioned); continue; } - catch (TypeLoadException ex) // Undocumented exception + 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); ChangePluginState(plugin, PluginStatus.NotSupported); diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs index f2cdfeb16..21a7f4f5f 100644 --- a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs +++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs @@ -638,7 +638,7 @@ namespace Emby.Server.Implementations.ScheduledTasks { try { - _logger.LogInformation(Name + ": Cancelling"); + _logger.LogInformation("{Name}: Cancelling", Name); token.Cancel(); } catch (Exception ex) @@ -652,16 +652,16 @@ namespace Emby.Server.Implementations.ScheduledTasks { try { - _logger.LogInformation(Name + ": Waiting on Task"); + _logger.LogInformation("{Name}: Waiting on Task", Name); var exited = task.Wait(2000); if (exited) { - _logger.LogInformation(Name + ": Task exited"); + _logger.LogInformation("{Name}: Task exited", Name); } else { - _logger.LogInformation(Name + ": Timed out waiting for task to stop"); + _logger.LogInformation("{Name}: Timed out waiting for task to stop", Name); } } catch (Exception ex) @@ -674,7 +674,7 @@ namespace Emby.Server.Implementations.ScheduledTasks { try { - _logger.LogDebug(Name + ": Disposing CancellationToken"); + _logger.LogDebug("{Name}: Disposing CancellationToken", Name); token.Dispose(); } catch (Exception ex) diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs index a575b260c..0941902fc 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs @@ -161,11 +161,11 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks } catch (UnauthorizedAccessException ex) { - _logger.LogError(ex, "Error deleting directory {path}", directory); + _logger.LogError(ex, "Error deleting directory {Path}", directory); } catch (IOException ex) { - _logger.LogError(ex, "Error deleting directory {path}", directory); + _logger.LogError(ex, "Error deleting directory {Path}", directory); } } } @@ -179,11 +179,11 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks } catch (UnauthorizedAccessException ex) { - _logger.LogError(ex, "Error deleting file {path}", path); + _logger.LogError(ex, "Error deleting file {Path}", path); } catch (IOException ex) { - _logger.LogError(ex, "Error deleting file {path}", path); + _logger.LogError(ex, "Error deleting file {Path}", path); } } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs index b13fc7fc6..099d781cd 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs @@ -141,11 +141,11 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks } catch (UnauthorizedAccessException ex) { - _logger.LogError(ex, "Error deleting directory {path}", directory); + _logger.LogError(ex, "Error deleting directory {Path}", directory); } catch (IOException ex) { - _logger.LogError(ex, "Error deleting directory {path}", directory); + _logger.LogError(ex, "Error deleting directory {Path}", directory); } } } @@ -159,11 +159,11 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks } catch (UnauthorizedAccessException ex) { - _logger.LogError(ex, "Error deleting file {path}", path); + _logger.LogError(ex, "Error deleting file {Path}", path); } catch (IOException ex) { - _logger.LogError(ex, "Error deleting file {path}", path); + _logger.LogError(ex, "Error deleting file {Path}", path); } } } diff --git a/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs b/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs index bd1966623..67a9fbd3b 100644 --- a/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs +++ b/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs @@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.Sorting /// <returns>System.Int32.</returns> public int Compare(BaseItem? x, BaseItem? y) { - return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase); + return string.Compare(GetValue(x), GetValue(y), StringComparison.OrdinalIgnoreCase); } /// <summary> diff --git a/Emby.Server.Implementations/Sorting/AlbumComparer.cs b/Emby.Server.Implementations/Sorting/AlbumComparer.cs index fe7dc84cb..4d09dda84 100644 --- a/Emby.Server.Implementations/Sorting/AlbumComparer.cs +++ b/Emby.Server.Implementations/Sorting/AlbumComparer.cs @@ -25,7 +25,7 @@ namespace Emby.Server.Implementations.Sorting /// <returns>System.Int32.</returns> public int Compare(BaseItem? x, BaseItem? y) { - return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase); + return string.Compare(GetValue(x), GetValue(y), StringComparison.OrdinalIgnoreCase); } /// <summary> @@ -35,9 +35,7 @@ namespace Emby.Server.Implementations.Sorting /// <returns>System.String.</returns> private static string? GetValue(BaseItem? x) { - var audio = x as Audio; - - return audio == null ? string.Empty : audio.Album; + return x is Audio audio ? audio.Album : string.Empty; } } } diff --git a/Emby.Server.Implementations/Sorting/ArtistComparer.cs b/Emby.Server.Implementations/Sorting/ArtistComparer.cs index 7b7ba5753..a8bb55e2b 100644 --- a/Emby.Server.Implementations/Sorting/ArtistComparer.cs +++ b/Emby.Server.Implementations/Sorting/ArtistComparer.cs @@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.Sorting /// <inheritdoc /> public int Compare(BaseItem? x, BaseItem? y) { - return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase); + return string.Compare(GetValue(x), GetValue(y), StringComparison.OrdinalIgnoreCase); } /// <summary> diff --git a/Emby.Server.Implementations/Sorting/NameComparer.cs b/Emby.Server.Implementations/Sorting/NameComparer.cs index 8f87717f4..c2875eeb9 100644 --- a/Emby.Server.Implementations/Sorting/NameComparer.cs +++ b/Emby.Server.Implementations/Sorting/NameComparer.cs @@ -34,7 +34,7 @@ namespace Emby.Server.Implementations.Sorting throw new ArgumentNullException(nameof(y)); } - return string.Compare(x.Name, y.Name, StringComparison.CurrentCultureIgnoreCase); + return string.Compare(x.Name, y.Name, StringComparison.OrdinalIgnoreCase); } } } diff --git a/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs index 4123a59f8..0bd9600b9 100644 --- a/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs +++ b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs @@ -25,7 +25,7 @@ namespace Emby.Server.Implementations.Sorting /// <returns>System.Int32.</returns> public int Compare(BaseItem x, BaseItem y) { - return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase); + return string.Compare(GetValue(x), GetValue(y), StringComparison.OrdinalIgnoreCase); } private static string GetValue(BaseItem item) diff --git a/Emby.Server.Implementations/Sorting/SortNameComparer.cs b/Emby.Server.Implementations/Sorting/SortNameComparer.cs index fb97a0349..79be9a89a 100644 --- a/Emby.Server.Implementations/Sorting/SortNameComparer.cs +++ b/Emby.Server.Implementations/Sorting/SortNameComparer.cs @@ -36,7 +36,7 @@ namespace Emby.Server.Implementations.Sorting throw new ArgumentNullException(nameof(y)); } - return string.Compare(x.SortName, y.SortName, StringComparison.CurrentCultureIgnoreCase); + return string.Compare(x.SortName, y.SortName, StringComparison.OrdinalIgnoreCase); } } } diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs index bf51c3968..33e4e5651 100644 --- a/Emby.Server.Implementations/Udp/UdpServer.cs +++ b/Emby.Server.Implementations/Udp/UdpServer.cs @@ -58,7 +58,7 @@ namespace Emby.Server.Implementations.Udp _udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); } - private async Task RespondToV2Message(string messageText, EndPoint endpoint, CancellationToken cancellationToken) + private async Task RespondToV2Message(EndPoint endpoint, CancellationToken cancellationToken) { string? localUrl = _config[AddressOverrideConfigKey]; if (string.IsNullOrEmpty(localUrl)) @@ -76,7 +76,7 @@ namespace Emby.Server.Implementations.Udp try { - await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint).ConfigureAwait(false); + await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint, cancellationToken).ConfigureAwait(false); } catch (SocketException ex) { @@ -115,7 +115,7 @@ namespace Emby.Server.Implementations.Udp var text = Encoding.UTF8.GetString(_receiveBuffer, 0, result.ReceivedBytes); if (text.Contains("who is JellyfinServer?", StringComparison.OrdinalIgnoreCase)) { - await RespondToV2Message(text, result.RemoteEndPoint, cancellationToken).ConfigureAwait(false); + await RespondToV2Message(result.RemoteEndPoint, cancellationToken).ConfigureAwait(false); } } catch (SocketException ex) diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index 4a022c5db..ef95ebf94 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -571,7 +571,7 @@ namespace Emby.Server.Implementations.Updates ?? _pluginManager.Plugins.FirstOrDefault(p => p.Name.Equals(package.Name, StringComparison.OrdinalIgnoreCase) && p.Version.Equals(package.Version)); await PerformPackageInstallation(package, plugin?.Manifest.Status ?? PluginStatus.Active, cancellationToken).ConfigureAwait(false); - _logger.LogInformation(plugin == null ? "New plugin installed: {PluginName} {PluginVersion}" : "Plugin updated: {PluginName} {PluginVersion}", package.Name, package.Version); + _logger.LogInformation("Plugin {Action}: {PluginName} {PluginVersion}", plugin == null ? "installed" : "updated", package.Name, package.Version); return plugin != null; } |
