From ba03ed65fe64b724b3e8b5b94b9cbe1075c61da2 Mon Sep 17 00:00:00 2001 From: ferferga Date: Wed, 27 May 2020 19:13:41 +0200 Subject: Remove "download images in advance" option --- MediaBrowser.Model/Configuration/LibraryOptions.cs | 1 - 1 file changed, 1 deletion(-) (limited to 'MediaBrowser.Model/Configuration') diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs index 4342ccd8ae..01d6665621 100644 --- a/MediaBrowser.Model/Configuration/LibraryOptions.cs +++ b/MediaBrowser.Model/Configuration/LibraryOptions.cs @@ -12,7 +12,6 @@ namespace MediaBrowser.Model.Configuration public bool EnableRealtimeMonitor { get; set; } public bool EnableChapterImageExtraction { get; set; } public bool ExtractChapterImagesDuringLibraryScan { get; set; } - public bool DownloadImagesInAdvance { get; set; } public MediaPathInfo[] PathInfos { get; set; } public bool SaveLocalMetadata { get; set; } -- cgit v1.2.3 From 4be476ec5312387f87134915d0fd132b2ad5fa3f Mon Sep 17 00:00:00 2001 From: ConfusedPolarBear <33811686+ConfusedPolarBear@users.noreply.github.com> Date: Thu, 18 Jun 2020 01:29:47 -0500 Subject: Move all settings into the main server configuration Decreased the timeout from 30 minutes to 5. Public lookup values have been replaced with the short code. --- .../QuickConnect/ConfigurationExtension.cs | 20 ------- .../QuickConnect/QuickConnectConfiguration.cs | 15 ----- .../QuickConnectConfigurationFactory.cs | 27 --------- .../QuickConnect/QuickConnectManager.cs | 66 ++++++++++------------ .../QuickConnect/IQuickConnect.cs | 8 +-- .../Configuration/ServerConfiguration.cs | 6 ++ .../QuickConnect/QuickConnectResult.cs | 5 -- .../QuickConnect/QuickConnectResultDto.cs | 14 +---- 8 files changed, 41 insertions(+), 120 deletions(-) delete mode 100644 Emby.Server.Implementations/QuickConnect/ConfigurationExtension.cs delete mode 100644 Emby.Server.Implementations/QuickConnect/QuickConnectConfiguration.cs delete mode 100644 Emby.Server.Implementations/QuickConnect/QuickConnectConfigurationFactory.cs (limited to 'MediaBrowser.Model/Configuration') diff --git a/Emby.Server.Implementations/QuickConnect/ConfigurationExtension.cs b/Emby.Server.Implementations/QuickConnect/ConfigurationExtension.cs deleted file mode 100644 index 2a19fc36c1..0000000000 --- a/Emby.Server.Implementations/QuickConnect/ConfigurationExtension.cs +++ /dev/null @@ -1,20 +0,0 @@ -using MediaBrowser.Common.Configuration; - -namespace Emby.Server.Implementations.QuickConnect -{ - /// - /// Configuration extension to support persistent quick connect configuration. - /// - public static class ConfigurationExtension - { - /// - /// Return the current quick connect configuration. - /// - /// Configuration manager. - /// Current quick connect configuration. - public static QuickConnectConfiguration GetQuickConnectConfiguration(this IConfigurationManager manager) - { - return manager.GetConfiguration("quickconnect"); - } - } -} diff --git a/Emby.Server.Implementations/QuickConnect/QuickConnectConfiguration.cs b/Emby.Server.Implementations/QuickConnect/QuickConnectConfiguration.cs deleted file mode 100644 index 2302ddbc3f..0000000000 --- a/Emby.Server.Implementations/QuickConnect/QuickConnectConfiguration.cs +++ /dev/null @@ -1,15 +0,0 @@ -using MediaBrowser.Model.QuickConnect; - -namespace Emby.Server.Implementations.QuickConnect -{ - /// - /// Persistent quick connect configuration. - /// - public class QuickConnectConfiguration - { - /// - /// Gets or sets persistent quick connect availability state. - /// - public QuickConnectState State { get; set; } - } -} diff --git a/Emby.Server.Implementations/QuickConnect/QuickConnectConfigurationFactory.cs b/Emby.Server.Implementations/QuickConnect/QuickConnectConfigurationFactory.cs deleted file mode 100644 index d7bc84c5e2..0000000000 --- a/Emby.Server.Implementations/QuickConnect/QuickConnectConfigurationFactory.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Collections.Generic; -using MediaBrowser.Common.Configuration; - -namespace Emby.Server.Implementations.QuickConnect -{ - /// - /// Configuration factory for quick connect. - /// - public class QuickConnectConfigurationFactory : IConfigurationFactory - { - /// - /// Returns the current quick connect configuration. - /// - /// Current quick connect configuration. - public IEnumerable GetConfigurations() - { - return new[] - { - new ConfigurationStore - { - Key = "quickconnect", - ConfigurationType = typeof(QuickConnectConfiguration) - } - }; - } - } -} diff --git a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs index 7a584c7cd0..8d704f32b7 100644 --- a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs +++ b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs @@ -11,7 +11,9 @@ using MediaBrowser.Controller.QuickConnect; using MediaBrowser.Controller.Security; using MediaBrowser.Model.QuickConnect; using MediaBrowser.Model.Services; +using MediaBrowser.Common; using Microsoft.Extensions.Logging; +using MediaBrowser.Common.Extensions; namespace Emby.Server.Implementations.QuickConnect { @@ -64,9 +66,7 @@ namespace Emby.Server.Implementations.QuickConnect public QuickConnectState State { get; private set; } = QuickConnectState.Unavailable; /// - public int RequestExpiry { get; set; } = 30; - - private bool TemporaryActivation { get; set; } = false; + public int Timeout { get; set; } = 5; private DateTime DateActivated { get; set; } @@ -82,10 +82,9 @@ namespace Emby.Server.Implementations.QuickConnect /// public QuickConnectResult Activate() { - // This should not call SetEnabled since that would persist the "temporary" activation to the configuration file - State = QuickConnectState.Active; + SetEnabled(QuickConnectState.Active); + DateActivated = DateTime.Now; - TemporaryActivation = true; return new QuickConnectResult(); } @@ -96,12 +95,10 @@ namespace Emby.Server.Implementations.QuickConnect _logger.LogDebug("Changed quick connect state from {0} to {1}", State, newState); ExpireRequests(true); - State = newState; - _config.SaveConfiguration("quickconnect", new QuickConnectConfiguration() - { - State = State - }); + State = newState; + _config.Configuration.QuickConnectAvailable = newState == QuickConnectState.Available || newState == QuickConnectState.Active; + _config.SaveConfiguration(); _logger.LogDebug("Configuration saved"); } @@ -123,17 +120,16 @@ namespace Emby.Server.Implementations.QuickConnect _logger.LogDebug("Got new quick connect request from {friendlyName}", friendlyName); - var lookup = GenerateSecureRandom(); + var code = GenerateCode(); var result = new QuickConnectResult() { - Lookup = lookup, Secret = GenerateSecureRandom(), FriendlyName = friendlyName, DateAdded = DateTime.Now, - Code = GenerateCode() + Code = code }; - _currentRequests[lookup] = result; + _currentRequests[code] = result; return result; } @@ -143,17 +139,16 @@ namespace Emby.Server.Implementations.QuickConnect ExpireRequests(); AssertActive(); - string lookup = _currentRequests.Where(x => x.Value.Secret == secret).Select(x => x.Value.Lookup).DefaultIfEmpty(string.Empty).First(); + string code = _currentRequests.Where(x => x.Value.Secret == secret).Select(x => x.Value.Code).DefaultIfEmpty(string.Empty).First(); - if (!_currentRequests.TryGetValue(lookup, out QuickConnectResult result)) + if (!_currentRequests.TryGetValue(code, out QuickConnectResult result)) { - throw new KeyNotFoundException("Unable to find request with provided identifier"); + throw new ResourceNotFoundException("Unable to find request with provided secret"); } return result; } - /// public List GetCurrentRequests() { return GetCurrentRequestsInternal().Select(x => (QuickConnectResultDto)x).ToList(); @@ -186,16 +181,16 @@ namespace Emby.Server.Implementations.QuickConnect } /// - public bool AuthorizeRequest(IRequest request, string lookup) + public bool AuthorizeRequest(IRequest request, string code) { ExpireRequests(); AssertActive(); var auth = _authContext.GetAuthorizationInfo(request); - if (!_currentRequests.TryGetValue(lookup, out QuickConnectResult result)) + if (!_currentRequests.TryGetValue(code, out QuickConnectResult result)) { - throw new KeyNotFoundException("Unable to find request"); + throw new ResourceNotFoundException("Unable to find request"); } if (result.Authenticated) @@ -205,9 +200,9 @@ namespace Emby.Server.Implementations.QuickConnect result.Authentication = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); - // Advance the time on the request so it expires sooner as the client will pick up the changes in a few seconds - var added = result.DateAdded ?? DateTime.Now.Subtract(new TimeSpan(0, RequestExpiry, 0)); - result.DateAdded = added.Subtract(new TimeSpan(0, RequestExpiry - 1, 0)); + // Change the time on the request so it expires one minute into the future. It can't expire immediately as otherwise some clients wouldn't ever see that they have been authenticated. + var added = result.DateAdded ?? DateTime.Now.Subtract(new TimeSpan(0, Timeout, 0)); + result.DateAdded = added.Subtract(new TimeSpan(0, Timeout - 1, 0)); _authenticationRepository.Create(new AuthenticationInfo { @@ -271,7 +266,7 @@ namespace Emby.Server.Implementations.QuickConnect var bytes = new byte[length]; _rng.GetBytes(bytes); - return string.Join(string.Empty, bytes.Select(x => x.ToString("x2", CultureInfo.InvariantCulture))); + return Hex.Encode(bytes); } /// @@ -281,12 +276,11 @@ namespace Emby.Server.Implementations.QuickConnect private void ExpireRequests(bool expireAll = false) { // Check if quick connect should be deactivated - if (TemporaryActivation && DateTime.Now > DateActivated.AddMinutes(10) && State == QuickConnectState.Active && !expireAll) + if (State == QuickConnectState.Active && DateTime.Now > DateActivated.AddMinutes(Timeout) && !expireAll) { _logger.LogDebug("Quick connect time expired, deactivating"); SetEnabled(QuickConnectState.Available); expireAll = true; - TemporaryActivation = false; } // Expire stale connection requests @@ -296,28 +290,28 @@ namespace Emby.Server.Implementations.QuickConnect for (int i = 0; i < values.Count; i++) { var added = values[i].DateAdded ?? DateTime.UnixEpoch; - if (DateTime.Now > added.AddMinutes(RequestExpiry) || expireAll) + if (DateTime.Now > added.AddMinutes(Timeout) || expireAll) { - delete.Add(values[i].Lookup); + delete.Add(values[i].Code); } } - foreach (var lookup in delete) + foreach (var code in delete) { - _logger.LogDebug("Removing expired request {lookup}", lookup); + _logger.LogDebug("Removing expired request {code}", code); - if (!_currentRequests.TryRemove(lookup, out _)) + if (!_currentRequests.TryRemove(code, out _)) { - _logger.LogWarning("Request {lookup} already expired", lookup); + _logger.LogWarning("Request {code} already expired", code); } } } private void ReloadConfiguration() { - var config = _config.GetQuickConnectConfiguration(); + var available = _config.Configuration.QuickConnectAvailable; - State = config.State; + State = available ? QuickConnectState.Available : QuickConnectState.Unavailable; } } } diff --git a/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs index d44765e112..d31d0e5097 100644 --- a/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs +++ b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs @@ -26,9 +26,9 @@ namespace MediaBrowser.Controller.QuickConnect public QuickConnectState State { get; } /// - /// Gets or sets the time (in minutes) before a pending request will expire. + /// Gets or sets the time (in minutes) before quick connect will automatically deactivate. /// - public int RequestExpiry { get; set; } + public int Timeout { get; set; } /// /// Assert that quick connect is currently active and throws an exception if it is not. @@ -77,9 +77,9 @@ namespace MediaBrowser.Controller.QuickConnect /// Authorizes a quick connect request to connect as the calling user. /// /// HTTP request object. - /// Public request lookup value. + /// Identifying code for the request.. /// A boolean indicating if the authorization completed successfully. - bool AuthorizeRequest(IRequest request, string lookup); + bool AuthorizeRequest(IRequest request, string code); /// /// Deletes all quick connect access tokens for the provided user. diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index afbe02dd36..76b2906069 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -76,6 +76,11 @@ namespace MediaBrowser.Model.Configuration /// true if this instance is port authorized; otherwise, false. public bool IsPortAuthorized { get; set; } + /// + /// Gets or sets if quick connect is available for use on this server. + /// + public bool QuickConnectAvailable { get; set; } + public bool AutoRunWebApp { get; set; } public bool EnableRemoteAccess { get; set; } @@ -281,6 +286,7 @@ namespace MediaBrowser.Model.Configuration AutoRunWebApp = true; EnableRemoteAccess = true; + QuickConnectAvailable = false; EnableUPnP = false; MinResumePct = 5; diff --git a/MediaBrowser.Model/QuickConnect/QuickConnectResult.cs b/MediaBrowser.Model/QuickConnect/QuickConnectResult.cs index 32d7f6aba6..a10d60d57e 100644 --- a/MediaBrowser.Model/QuickConnect/QuickConnectResult.cs +++ b/MediaBrowser.Model/QuickConnect/QuickConnectResult.cs @@ -17,11 +17,6 @@ namespace MediaBrowser.Model.QuickConnect /// public string? Secret { get; set; } - /// - /// Gets or sets the public value used to uniquely identify this request. Can only be used to authorize the request. - /// - public string? Lookup { get; set; } - /// /// Gets or sets the user facing code used so the user can quickly differentiate this request from others. /// diff --git a/MediaBrowser.Model/QuickConnect/QuickConnectResultDto.cs b/MediaBrowser.Model/QuickConnect/QuickConnectResultDto.cs index 19acc7cd88..26084caf1e 100644 --- a/MediaBrowser.Model/QuickConnect/QuickConnectResultDto.cs +++ b/MediaBrowser.Model/QuickConnect/QuickConnectResultDto.cs @@ -17,25 +17,15 @@ namespace MediaBrowser.Model.QuickConnect /// public string? Code { get; private set; } - /// - /// Gets the public value used to uniquely identify this request. Can only be used to authorize the request. - /// - public string? Lookup { get; private set; } - /// /// Gets the device friendly name. /// public string? FriendlyName { get; private set; } - /// - /// Gets the DateTime that this request was created. - /// - public DateTime? DateAdded { get; private set; } - /// /// Cast an internal quick connect result to a DTO by removing all sensitive properties. /// - /// QuickConnectResult object to cast + /// QuickConnectResult object to cast. public static implicit operator QuickConnectResultDto(QuickConnectResult result) { QuickConnectResultDto resultDto = new QuickConnectResultDto @@ -43,8 +33,6 @@ namespace MediaBrowser.Model.QuickConnect Authenticated = result.Authenticated, Code = result.Code, FriendlyName = result.FriendlyName, - DateAdded = result.DateAdded, - Lookup = result.Lookup }; return resultDto; -- cgit v1.2.3 From c888b34a6219be0c06331e568d66a8fdf17bceaa Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Fri, 24 Jul 2020 21:19:56 +0800 Subject: add a method to use custom fallback fonts for subtitle rendering --- MediaBrowser.Api/Subtitles/SubtitleService.cs | 53 ++++++++++++++++++++++ .../Configuration/EncodingOptions.cs | 5 ++ 2 files changed, 58 insertions(+) (limited to 'MediaBrowser.Model/Configuration') diff --git a/MediaBrowser.Api/Subtitles/SubtitleService.cs b/MediaBrowser.Api/Subtitles/SubtitleService.cs index a70da8e56c..198e459482 100644 --- a/MediaBrowser.Api/Subtitles/SubtitleService.cs +++ b/MediaBrowser.Api/Subtitles/SubtitleService.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -122,8 +123,15 @@ namespace MediaBrowser.Api.Subtitles public int SegmentLength { get; set; } } + [Route("/FallbackFont/Font", "GET", Summary = "Gets the fallback font file")] + [Authenticated] + public class GetFallbackFont + { + } + public class SubtitleService : BaseApiService { + private readonly IServerConfigurationManager _serverConfigurationManager; private readonly ILibraryManager _libraryManager; private readonly ISubtitleManager _subtitleManager; private readonly ISubtitleEncoder _subtitleEncoder; @@ -145,6 +153,7 @@ namespace MediaBrowser.Api.Subtitles IAuthorizationContext authContext) : base(logger, serverConfigurationManager, httpResultFactory) { + _serverConfigurationManager = serverConfigurationManager; _libraryManager = libraryManager; _subtitleManager = subtitleManager; _subtitleEncoder = subtitleEncoder; @@ -298,5 +307,49 @@ namespace MediaBrowser.Api.Subtitles } }); } + + public async Task Get(GetFallbackFont request) + { + var fallbackFontPath = EncodingConfigurationExtensions.GetEncodingOptions(_serverConfigurationManager).FallbackFontPath; + + if (!string.IsNullOrEmpty(fallbackFontPath)) + { + var directoryService = new DirectoryService(_fileSystem); + + try + { + // 10 Megabytes + var maxSize = 10485760; + var fontFile = directoryService.GetFile(fallbackFontPath); + var fileSize = fontFile?.Length; + + if (fileSize != null && fileSize > 0) + { + Logger.LogInformation("Fallback font size is {0} Bytes", fileSize); + + if (fileSize <= maxSize) + { + return await ResultFactory.GetStaticFileResult(Request, fontFile.FullName); + } + + Logger.LogInformation("The selected font is too large. Maximum allowed size is 10 Megabytes"); + } + else + { + Logger.LogInformation("The selected font is null or empty"); + } + } + catch (Exception ex) + { + Logger.LogError(ex, "Error reading fallback font file"); + } + } + else + { + Logger.LogInformation("The path of fallback font has not been set"); + } + + return string.Empty; + } } } diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index 9a30f7e9f0..aa6ed5bb9f 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -9,6 +9,10 @@ namespace MediaBrowser.Model.Configuration public string TranscodingTempPath { get; set; } + public string FallbackFontPath { get; set; } + + public bool EnableFallbackFont { get; set; } + public double DownMixAudioBoost { get; set; } public bool EnableThrottling { get; set; } @@ -49,6 +53,7 @@ namespace MediaBrowser.Model.Configuration public EncodingOptions() { + EnableFallbackFont = false; DownMixAudioBoost = 2; EnableThrottling = false; ThrottleDelaySeconds = 180; -- cgit v1.2.3 From 7b862bba5aad345f0bc8d76bfb950590471ae232 Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Sat, 25 Jul 2020 00:57:34 +0800 Subject: add Tonemapping relaying on nvdec and ocl --- .../MediaEncoding/EncodingHelper.cs | 109 +++++++++++++++++++-- .../Probing/MediaStreamInfo.cs | 14 +++ .../Probing/ProbeResultNormalizer.cs | 10 ++ .../Configuration/EncodingOptions.cs | 29 +++++- MediaBrowser.Model/Entities/MediaStream.cs | 18 ++-- 5 files changed, 166 insertions(+), 14 deletions(-) (limited to 'MediaBrowser.Model/Configuration') diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 0242338b70..4a33f22e63 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -454,6 +454,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isVaapiEncoder = outputVideoCodec.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1; var isQsvDecoder = videoDecoder.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1; var isQsvEncoder = outputVideoCodec.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1; + var isNvencHevcDecoder = videoDecoder.IndexOf("hevc_cuvid", StringComparison.OrdinalIgnoreCase) != -1; var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); @@ -511,6 +512,25 @@ namespace MediaBrowser.Controller.MediaEncoding } } } + + if (state.IsVideoRequest + && string.Equals(encodingOptions.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase)) + { + var codec = state.VideoStream.Codec.ToLowerInvariant(); + var isColorDepth10 = IsColorDepth10(state); + + if (isNvencHevcDecoder && isColorDepth10 + && _mediaEncoder.SupportsHwaccel("opencl") + && encodingOptions.EnableTonemapping + && !string.IsNullOrEmpty(state.VideoStream.VideoRange) + && state.VideoStream.VideoRange.Contains("HDR", StringComparison.OrdinalIgnoreCase)) + { + arg.Append("-init_hw_device opencl=ocl:") + .Append(encodingOptions.OpenclDevice) + .Append(' ') + .Append("-filter_hw_device ocl "); + } + } } arg.Append("-i ") @@ -994,11 +1014,33 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase) && !string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) && !string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) + && !string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) && !string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase)) { param = "-pix_fmt yuv420p " + param; } + if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)) + { + var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, encodingOptions) ?? string.Empty; + var videoStream = state.VideoStream; + var isColorDepth10 = IsColorDepth10(state); + + if (videoDecoder.IndexOf("hevc_cuvid", StringComparison.OrdinalIgnoreCase) != -1 + && isColorDepth10 + && _mediaEncoder.SupportsHwaccel("opencl") + && encodingOptions.EnableTonemapping + && !string.IsNullOrEmpty(videoStream.VideoRange) + && videoStream.VideoRange.Contains("HDR", StringComparison.OrdinalIgnoreCase)) + { + param = "-pix_fmt nv12 " + param; + } + else + { + param = "-pix_fmt yuv420p " + param; + } + } + if (string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase)) { param = "-pix_fmt nv21 " + param; @@ -1599,46 +1641,54 @@ namespace MediaBrowser.Controller.MediaEncoding { outputSizeParam = GetOutputSizeParam(state, options, outputVideoCodec).TrimEnd('"'); - var index = outputSizeParam.IndexOf("hwdownload", StringComparison.OrdinalIgnoreCase); + var index = outputSizeParam.IndexOf("hwupload,tonemap_opencl", StringComparison.OrdinalIgnoreCase); if (index != -1) { outputSizeParam = outputSizeParam.Substring(index); } else { - index = outputSizeParam.IndexOf("hwupload=extra_hw_frames", StringComparison.OrdinalIgnoreCase); + index = outputSizeParam.IndexOf("hwdownload", StringComparison.OrdinalIgnoreCase); if (index != -1) { outputSizeParam = outputSizeParam.Substring(index); } else { - index = outputSizeParam.IndexOf("format", StringComparison.OrdinalIgnoreCase); + index = outputSizeParam.IndexOf("hwupload=extra_hw_frames", StringComparison.OrdinalIgnoreCase); if (index != -1) { outputSizeParam = outputSizeParam.Substring(index); } else { - index = outputSizeParam.IndexOf("yadif", StringComparison.OrdinalIgnoreCase); + index = outputSizeParam.IndexOf("format", StringComparison.OrdinalIgnoreCase); if (index != -1) { outputSizeParam = outputSizeParam.Substring(index); } else { - index = outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase); + index = outputSizeParam.IndexOf("yadif", StringComparison.OrdinalIgnoreCase); if (index != -1) { outputSizeParam = outputSizeParam.Substring(index); } else { - index = outputSizeParam.IndexOf("vpp", StringComparison.OrdinalIgnoreCase); + index = outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase); if (index != -1) { outputSizeParam = outputSizeParam.Substring(index); } + else + { + index = outputSizeParam.IndexOf("vpp", StringComparison.OrdinalIgnoreCase); + if (index != -1) + { + outputSizeParam = outputSizeParam.Substring(index); + } + } } } } @@ -2058,12 +2108,58 @@ namespace MediaBrowser.Controller.MediaEncoding var isVaapiH264Encoder = outputVideoCodec.IndexOf("h264_vaapi", StringComparison.OrdinalIgnoreCase) != -1; var isQsvH264Encoder = outputVideoCodec.IndexOf("h264_qsv", StringComparison.OrdinalIgnoreCase) != -1; var isNvdecH264Decoder = videoDecoder.IndexOf("h264_cuvid", StringComparison.OrdinalIgnoreCase) != -1; + var isNvdecHevcDecoder = videoDecoder.IndexOf("hevc_cuvid", StringComparison.OrdinalIgnoreCase) != -1; var isLibX264Encoder = outputVideoCodec.IndexOf("libx264", StringComparison.OrdinalIgnoreCase) != -1; var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + var isColorDepth10 = IsColorDepth10(state); var hasTextSubs = state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + // Currently only with the use of NVENC decoder can we get a decent performance. + // Currently only the HEVC/H265 format is supported. + // NVIDIA Pascal and Turing or higher are recommended. + if (isNvdecHevcDecoder && isColorDepth10 + && _mediaEncoder.SupportsHwaccel("opencl") + && options.EnableTonemapping + && !string.IsNullOrEmpty(videoStream.VideoRange) + && videoStream.VideoRange.Contains("HDR", StringComparison.OrdinalIgnoreCase)) + { + var parameters = "tonemap_opencl=format=nv12:primaries=bt709:transfer=bt709:matrix=bt709:tonemap={0}:desat={1}:threshold={2}:peak={3}"; + + if (options.TonemappingParam != 0) + { + parameters += ":param={4}"; + } + + if (!string.Equals(options.TonemappingRange, "auto", StringComparison.OrdinalIgnoreCase)) + { + parameters += ":range={5}"; + } + + // Upload the HDR10 or HLG data to the OpenCL device, + // use tonemap_opencl filter for tone mapping, + // and then download the SDR data to memory. + filters.Add("hwupload"); + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + parameters, + options.TonemappingAlgorithm, + options.TonemappingDesat, + options.TonemappingThreshold, + options.TonemappingPeak, + options.TonemappingParam, + options.TonemappingRange)); + filters.Add("hwdownload"); + + if (hasGraphicalSubs || state.DeInterlace("h265", true) || state.DeInterlace("hevc", true) + || string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase)) + { + filters.Add("format=nv12"); + } + } + // When the input may or may not be hardware VAAPI decodable if (isVaapiH264Encoder) { @@ -2081,7 +2177,6 @@ namespace MediaBrowser.Controller.MediaEncoding else if (IsVaapiSupported(state) && isVaapiDecoder && isLibX264Encoder) { var codec = videoStream.Codec.ToLowerInvariant(); - var isColorDepth10 = IsColorDepth10(state); // Assert 10-bit hardware VAAPI decodable if (isColorDepth10 && (string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase) diff --git a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs index a2ea0766a8..d9658cba24 100644 --- a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs +++ b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs @@ -279,6 +279,20 @@ namespace MediaBrowser.MediaEncoding.Probing [JsonPropertyName("disposition")] public IReadOnlyDictionary Disposition { get; set; } + /// + /// Gets or sets the color range. + /// + /// The color range. + [JsonPropertyName("color_range")] + public string ColorRange { get; set; } + + /// + /// Gets or sets the color space. + /// + /// The color space. + [JsonPropertyName("color_space")] + public string ColorSpace { get; set; } + /// /// Gets or sets the color transfer. /// diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index c85d963ed1..3815e00d47 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -703,6 +703,16 @@ namespace MediaBrowser.MediaEncoding.Probing stream.RefFrames = streamInfo.Refs; } + if (!string.IsNullOrEmpty(streamInfo.ColorRange)) + { + stream.ColorRange = streamInfo.ColorRange; + } + + if (!string.IsNullOrEmpty(streamInfo.ColorSpace)) + { + stream.ColorSpace = streamInfo.ColorSpace; + } + if (!string.IsNullOrEmpty(streamInfo.ColorTransfer)) { stream.ColorTransfer = streamInfo.ColorTransfer; diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index 9a30f7e9f0..843ff3ff9c 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -29,6 +29,22 @@ namespace MediaBrowser.Model.Configuration public string VaapiDevice { get; set; } + public string OpenclDevice { get; set; } + + public bool EnableTonemapping { get; set; } + + public string TonemappingAlgorithm { get; set; } + + public string TonemappingRange { get; set; } + + public double TonemappingDesat { get; set; } + + public double TonemappingThreshold { get; set; } + + public double TonemappingPeak { get; set; } + + public double TonemappingParam { get; set; } + public int H264Crf { get; set; } public int H265Crf { get; set; } @@ -53,8 +69,19 @@ namespace MediaBrowser.Model.Configuration EnableThrottling = false; ThrottleDelaySeconds = 180; EncodingThreadCount = -1; - // This is a DRM device that is almost guaranteed to be there on every intel platform, plus it's the default one in ffmpeg if you don't specify anything + // This is a DRM device that is almost guaranteed to be there on every intel platform, + // plus it's the default one in ffmpeg if you don't specify anything VaapiDevice = "/dev/dri/renderD128"; + // This is the OpenCL device that is used for tonemapping. + // The left side of the dot is the platform number, and the right side is the device number on the platform. + OpenclDevice = "0.0"; + EnableTonemapping = false; + TonemappingAlgorithm = "reinhard"; + TonemappingRange = "auto"; + TonemappingDesat = 0; + TonemappingThreshold = 0.8; + TonemappingPeak = 0; + TonemappingParam = 0; H264Crf = 23; H265Crf = 28; DeinterlaceMethod = "yadif"; diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index 1b37cfc939..19eb79acc4 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -35,6 +35,18 @@ namespace MediaBrowser.Model.Entities /// The language. public string Language { get; set; } + /// + /// Gets or sets the color range. + /// + /// The color range. + public string ColorRange { get; set; } + + /// + /// Gets or sets the color space. + /// + /// The color space. + public string ColorSpace { get; set; } + /// /// Gets or sets the color transfer. /// @@ -47,12 +59,6 @@ namespace MediaBrowser.Model.Entities /// The color primaries. public string ColorPrimaries { get; set; } - /// - /// Gets or sets the color space. - /// - /// The color space. - public string ColorSpace { get; set; } - /// /// Gets or sets the comment. /// -- cgit v1.2.3 From 7df2affb238e92c9b03813f5aa11530cb37dcbdc Mon Sep 17 00:00:00 2001 From: Orry Verducci Date: Mon, 10 Aug 2020 22:14:47 +0100 Subject: Add double rate deinterlacing option --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 3 +++ MediaBrowser.Model/Configuration/EncodingOptions.cs | 3 +++ 2 files changed, 6 insertions(+) (limited to 'MediaBrowser.Model/Configuration') diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 913e171f14..81606aa0c6 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -2071,6 +2071,9 @@ namespace MediaBrowser.Controller.MediaEncoding var hasTextSubs = state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + // If double rate deinterlacing is enabled and the input framerate is 30fps or below, otherwise the output framerate will be too high for many devices + var doubleRateDeinterlace = (options.DeinterlaceDoubleRate && (videoStream?.RealFrameRate ?? 60) <= 30); + // When the input may or may not be hardware VAAPI decodable if (isVaapiH264Encoder) { diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index 9a30f7e9f0..d7785f39c8 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -35,6 +35,8 @@ namespace MediaBrowser.Model.Configuration public string EncoderPreset { get; set; } + public bool DeinterlaceDoubleRate { get; set; } + public string DeinterlaceMethod { get; set; } public bool EnableDecodingColorDepth10Hevc { get; set; } @@ -57,6 +59,7 @@ namespace MediaBrowser.Model.Configuration VaapiDevice = "/dev/dri/renderD128"; H264Crf = 23; H265Crf = 28; + DeinterlaceDoubleRate = false; DeinterlaceMethod = "yadif"; EnableDecodingColorDepth10Hevc = true; EnableDecodingColorDepth10Vp9 = true; -- cgit v1.2.3 From d7caf88df67b2cccedc9bcbc19e80d95b997fb49 Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Tue, 25 Aug 2020 02:20:46 +0800 Subject: expose max_muxing_queue_size to user --- Jellyfin.Api/Controllers/DynamicHlsController.cs | 7 ++++++- MediaBrowser.Model/Configuration/EncodingOptions.cs | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) (limited to 'MediaBrowser.Model/Configuration') diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index b115ac6cd0..fec7f25799 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1354,15 +1354,20 @@ namespace Jellyfin.Api.Controllers segmentFormat = "mpegts"; } + var maxMuxingQueueSize = encodingOptions.MaxMuxingQueueSize >= 128 && encodingOptions.MaxMuxingQueueSize <= int.MaxValue + ? encodingOptions.MaxMuxingQueueSize.ToString(CultureInfo.InvariantCulture) + : "128"; + return string.Format( CultureInfo.InvariantCulture, - "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size 2048 -f hls -max_delay 5000000 -hls_time {6} -individual_header_trailer 0 -hls_segment_type {7} -start_number {8} -hls_segment_filename \"{9}\" -hls_playlist_type vod -hls_list_size 0 -y \"{10}\"", + "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -individual_header_trailer 0 -hls_segment_type {8} -start_number {9} -hls_segment_filename \"{10}\" -hls_playlist_type vod -hls_list_size 0 -y \"{11}\"", inputModifier, _encodingHelper.GetInputArgument(state, encodingOptions), threads, mapArgs, GetVideoArguments(state, encodingOptions, startNumber), GetAudioArguments(state, encodingOptions), + maxMuxingQueueSize, state.SegmentLength.ToString(CultureInfo.InvariantCulture), segmentFormat, startNumberParam, diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index 9a30f7e9f0..2e548e8b45 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -11,6 +11,8 @@ namespace MediaBrowser.Model.Configuration public double DownMixAudioBoost { get; set; } + public int MaxMuxingQueueSize { get; set; } + public bool EnableThrottling { get; set; } public int ThrottleDelaySeconds { get; set; } @@ -50,6 +52,7 @@ namespace MediaBrowser.Model.Configuration public EncodingOptions() { DownMixAudioBoost = 2; + MaxMuxingQueueSize = 2048; EnableThrottling = false; ThrottleDelaySeconds = 180; EncodingThreadCount = -1; -- cgit v1.2.3 From 3c0484cc9730c06892b996d0b884a05ecada07af Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 30 Aug 2020 09:32:14 -0600 Subject: Allow for dynamic cors response --- .../Extensions/ApiServiceCollectionExtensions.cs | 8 ++- .../Middleware/DynamicCorsMiddleware.cs | 68 ++++++++++++++++++++++ Jellyfin.Server/Models/ServerCorsPolicy.cs | 43 +++++++++----- Jellyfin.Server/Startup.cs | 8 ++- .../Configuration/ServerConfiguration.cs | 6 ++ 5 files changed, 115 insertions(+), 18 deletions(-) create mode 100644 Jellyfin.Server/Middleware/DynamicCorsMiddleware.cs (limited to 'MediaBrowser.Model/Configuration') diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 0fd599cfcd..b2f861542a 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -135,13 +135,17 @@ namespace Jellyfin.Server.Extensions /// /// The service collection. /// The base url for the API. + /// The configured cors hosts. /// The MVC builder. - public static IMvcBuilder AddJellyfinApi(this IServiceCollection serviceCollection, string baseUrl) + public static IMvcBuilder AddJellyfinApi( + this IServiceCollection serviceCollection, + string baseUrl, + string[] corsHosts) { return serviceCollection .AddCors(options => { - options.AddPolicy(ServerCorsPolicy.DefaultPolicyName, ServerCorsPolicy.DefaultPolicy); + options.AddPolicy(ServerCorsPolicy.DefaultPolicyName, new ServerCorsPolicy(corsHosts).Policy); }) .Configure(options => { diff --git a/Jellyfin.Server/Middleware/DynamicCorsMiddleware.cs b/Jellyfin.Server/Middleware/DynamicCorsMiddleware.cs new file mode 100644 index 0000000000..4fad898a73 --- /dev/null +++ b/Jellyfin.Server/Middleware/DynamicCorsMiddleware.cs @@ -0,0 +1,68 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Cors.Infrastructure; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; + +namespace Jellyfin.Server.Middleware +{ + /// + /// Dynamic cors middleware. + /// + public class DynamicCorsMiddleware + { + private readonly RequestDelegate _next; + private readonly ILogger _logger; + private readonly CorsMiddleware _corsMiddleware; + + /// + /// Initializes a new instance of the class. + /// + /// Next request delegate. + /// Instance of the interface. + /// Instance of the interface. + /// The cors policy name. + public DynamicCorsMiddleware( + RequestDelegate next, + ICorsService corsService, + ILoggerFactory loggerFactory, + string policyName) + { + _corsMiddleware = new CorsMiddleware(next, corsService, loggerFactory, policyName); + _next = next; + _logger = loggerFactory.CreateLogger(); + } + + /// + /// Invoke request. + /// + /// Request context. + /// Instance of the interface. + /// Task. + /// + public async Task Invoke(HttpContext context, ICorsPolicyProvider corsPolicyProvider) + { + // Only execute if is preflight request. + if (string.Equals(context.Request.Method, CorsConstants.PreflightHttpMethod, StringComparison.OrdinalIgnoreCase)) + { + // Invoke original cors middleware. + await _corsMiddleware.Invoke(context, corsPolicyProvider).ConfigureAwait(false); + if (context.Response.Headers.TryGetValue(HeaderNames.AccessControlAllowOrigin, out var headerValue) + && string.Equals(headerValue, "*", StringComparison.Ordinal)) + { + context.Response.Headers[HeaderNames.AccessControlAllowOrigin] = context.Request.Host.Value; + _logger.LogDebug("Overwriting CORS response header: {HeaderName}: {HeaderValue}", HeaderNames.AccessControlAllowOrigin, context.Request.Host.Value); + + if (!context.Response.Headers.ContainsKey(HeaderNames.AccessControlAllowCredentials)) + { + context.Response.Headers[HeaderNames.AccessControlAllowCredentials] = "true"; + } + } + } + + // Call the next delegate/middleware in the pipeline + await this._next(context).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Server/Models/ServerCorsPolicy.cs b/Jellyfin.Server/Models/ServerCorsPolicy.cs index ae010c042e..3a45db3b44 100644 --- a/Jellyfin.Server/Models/ServerCorsPolicy.cs +++ b/Jellyfin.Server/Models/ServerCorsPolicy.cs @@ -1,30 +1,47 @@ -using Microsoft.AspNetCore.Cors.Infrastructure; +using System; +using Microsoft.AspNetCore.Cors.Infrastructure; namespace Jellyfin.Server.Models { /// /// Server Cors Policy. /// - public static class ServerCorsPolicy + public class ServerCorsPolicy { /// /// Default policy name. /// - public const string DefaultPolicyName = "DefaultCorsPolicy"; + public const string DefaultPolicyName = nameof(ServerCorsPolicy); /// - /// Default Policy. Allow Everything. + /// Initializes a new instance of the class. /// - public static readonly CorsPolicy DefaultPolicy = new CorsPolicy + /// The configured cors hosts. + public ServerCorsPolicy(string[] corsHosts) { - // Allow any origin - Origins = { "*" }, + var builder = new CorsPolicyBuilder() + .AllowAnyMethod() + .AllowAnyHeader(); - // Allow any method - Methods = { "*" }, + // No hosts configured or only default configured. + if (corsHosts.Length == 0 + || (corsHosts.Length == 1 + && string.Equals(corsHosts[0], "*", StringComparison.Ordinal))) + { + builder.AllowAnyOrigin(); + } + else + { + builder.WithOrigins(corsHosts) + .AllowCredentials(); + } - // Allow any header - Headers = { "*" } - }; + Policy = builder.Build(); + } + + /// + /// Gets the cors policy. + /// + public CorsPolicy Policy { get; } } -} \ No newline at end of file +} diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index d0dd183c68..76f5e69ced 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -38,7 +38,9 @@ namespace Jellyfin.Server { services.AddResponseCompression(); services.AddHttpContextAccessor(); - services.AddJellyfinApi(_serverConfigurationManager.Configuration.BaseUrl.TrimStart('/')); + services.AddJellyfinApi( + _serverConfigurationManager.Configuration.BaseUrl.TrimStart('/'), + _serverConfigurationManager.Configuration.CorsHosts); services.AddJellyfinApiSwagger(); @@ -78,11 +80,11 @@ namespace Jellyfin.Server app.UseAuthentication(); app.UseJellyfinApiSwagger(_serverConfigurationManager); app.UseRouting(); - app.UseCors(ServerCorsPolicy.DefaultPolicyName); + app.UseMiddleware(ServerCorsPolicy.DefaultPolicyName); app.UseAuthorization(); if (_serverConfigurationManager.Configuration.EnableMetrics) { - // Must be registered after any middleware that could chagne HTTP response codes or the data will be bad + // Must be registered after any middleware that could change HTTP response codes or the data will be bad app.UseHttpMetrics(); } diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index c66091f9d5..a743277d7a 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -264,6 +264,11 @@ namespace MediaBrowser.Model.Configuration /// public long SlowResponseThresholdMs { get; set; } + /// + /// Gets or sets the cors hosts. + /// + public string[] CorsHosts { get; set; } + /// /// Initializes a new instance of the class. /// @@ -372,6 +377,7 @@ namespace MediaBrowser.Model.Configuration EnableSlowResponseWarning = true; SlowResponseThresholdMs = 500; + CorsHosts = new[] { "*" }; } } -- cgit v1.2.3 From 1feee6f95e00cf579ab16c7ca004947534545d9b Mon Sep 17 00:00:00 2001 From: crobibero Date: Wed, 2 Sep 2020 08:03:15 -0600 Subject: Properly host static files and set base url --- Jellyfin.Api/Controllers/DashboardController.cs | 102 +-------------------- .../Extensions/ApiApplicationBuilderExtensions.cs | 21 +---- .../Extensions/ApiServiceCollectionExtensions.cs | 6 +- Jellyfin.Server/Program.cs | 2 +- Jellyfin.Server/Startup.cs | 21 ++++- .../Configuration/IApplicationPaths.cs | 5 +- .../Configuration/ServerConfiguration.cs | 6 -- 7 files changed, 27 insertions(+), 136 deletions(-) (limited to 'MediaBrowser.Model/Configuration') diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs index 33abe3ccdc..3f0fc2e913 100644 --- a/Jellyfin.Api/Controllers/DashboardController.cs +++ b/Jellyfin.Api/Controllers/DashboardController.cs @@ -15,6 +15,7 @@ using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; namespace Jellyfin.Api.Controllers { @@ -26,38 +27,20 @@ namespace Jellyfin.Api.Controllers { private readonly ILogger _logger; private readonly IServerApplicationHost _appHost; - private readonly IConfiguration _appConfig; - private readonly IServerConfigurationManager _serverConfigurationManager; - private readonly IResourceFileManager _resourceFileManager; /// /// Initializes a new instance of the class. /// /// Instance of interface. /// Instance of interface. - /// Instance of interface. - /// Instance of interface. - /// Instance of interface. public DashboardController( ILogger logger, - IServerApplicationHost appHost, - IConfiguration appConfig, - IResourceFileManager resourceFileManager, - IServerConfigurationManager serverConfigurationManager) + IServerApplicationHost appHost) { _logger = logger; _appHost = appHost; - _appConfig = appConfig; - _resourceFileManager = resourceFileManager; - _serverConfigurationManager = serverConfigurationManager; } - /// - /// Gets the path of the directory containing the static web interface content, or null if the server is not - /// hosting the web client. - /// - private string? WebClientUiPath => GetWebClientUiPath(_appConfig, _serverConfigurationManager); - /// /// Gets the configuration pages. /// @@ -169,87 +152,6 @@ namespace Jellyfin.Api.Controllers return NotFound(); } - /// - /// Gets the robots.txt. - /// - /// Robots.txt returned. - /// The robots.txt. - [HttpGet("robots.txt")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ApiExplorerSettings(IgnoreApi = true)] - public ActionResult GetRobotsTxt() - { - return GetWebClientResource("robots.txt"); - } - - /// - /// Gets a resource from the web client. - /// - /// The resource name. - /// Web client returned. - /// Server does not host a web client. - /// The resource. - [HttpGet("web/{*resourceName}")] - [ApiExplorerSettings(IgnoreApi = true)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GetWebClientResource([FromRoute] string resourceName) - { - if (!_appConfig.HostWebClient() || WebClientUiPath == null) - { - return NotFound("Server does not host a web client."); - } - - var path = resourceName; - var basePath = WebClientUiPath; - - var requestPathAndQuery = Request.GetEncodedPathAndQuery(); - // Bounce them to the startup wizard if it hasn't been completed yet - if (!_serverConfigurationManager.Configuration.IsStartupWizardCompleted - && !requestPathAndQuery.Contains("wizard", StringComparison.OrdinalIgnoreCase) - && requestPathAndQuery.Contains("index", StringComparison.OrdinalIgnoreCase)) - { - return Redirect("index.html?start=wizard#!/wizardstart.html"); - } - - var stream = new FileStream(_resourceFileManager.GetResourcePath(basePath, path), FileMode.Open, FileAccess.Read); - return File(stream, MimeTypes.GetMimeType(path)); - } - - /// - /// Gets the favicon. - /// - /// Favicon.ico returned. - /// The favicon. - [HttpGet("favicon.ico")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ApiExplorerSettings(IgnoreApi = true)] - public ActionResult GetFavIcon() - { - return GetWebClientResource("favicon.ico"); - } - - /// - /// Gets the path of the directory containing the static web interface content. - /// - /// The app configuration. - /// The server configuration manager. - /// The directory path, or null if the server is not hosting the web client. - public static string? GetWebClientUiPath(IConfiguration appConfig, IServerConfigurationManager serverConfigManager) - { - if (!appConfig.HostWebClient()) - { - return null; - } - - if (!string.IsNullOrEmpty(serverConfigManager.Configuration.DashboardSourcePath)) - { - return serverConfigManager.Configuration.DashboardSourcePath; - } - - return serverConfigManager.ApplicationPaths.WebPath; - } - private IEnumerable GetConfigPages(IPlugin plugin) { return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin, i.Item1)); diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs index a1a68bcf0c..e23de86d97 100644 --- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs @@ -34,38 +34,23 @@ namespace Jellyfin.Server.Extensions .UseSwagger(c => { // Custom path requires {documentName}, SwaggerDoc documentName is 'api-docs' - c.RouteTemplate = $"/{baseUrl}{{documentName}}/openapi.json"; + c.RouteTemplate = "{documentName}/openapi.json"; c.PreSerializeFilters.Add((swagger, httpReq) => { swagger.Servers = new List { new OpenApiServer { Url = $"{httpReq.Scheme}://{httpReq.Host.Value}{apiDocBaseUrl}" } }; - - // BaseUrl is empty, ignore - if (apiDocBaseUrl.Length != 0) - { - // Update all relative paths to remove baseUrl. - var updatedPaths = new OpenApiPaths(); - foreach (var (key, value) in swagger.Paths) - { - var relativePath = key; - relativePath = relativePath.Remove(0, apiDocBaseUrl.Length); - updatedPaths.Add(relativePath, value); - } - - swagger.Paths = updatedPaths; - } }); }) .UseSwaggerUI(c => { c.DocumentTitle = "Jellyfin API"; c.SwaggerEndpoint($"/{baseUrl}api-docs/openapi.json", "Jellyfin API"); - c.RoutePrefix = $"{baseUrl}api-docs/swagger"; + c.RoutePrefix = "api-docs/swagger"; }) .UseReDoc(c => { c.DocumentTitle = "Jellyfin API"; c.SpecUrl($"/{baseUrl}api-docs/openapi.json"); - c.RoutePrefix = $"{baseUrl}api-docs/redoc"; + c.RoutePrefix = "api-docs/redoc"; }); } } diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 0160a05f92..283c870fd8 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -135,10 +135,9 @@ namespace Jellyfin.Server.Extensions /// Extension method for adding the jellyfin API to the service collection. /// /// The service collection. - /// The base url for the API. - /// An IEnumberable containing all plugin assemblies with API controllers. + /// An IEnumerable containing all plugin assemblies with API controllers. /// The MVC builder. - public static IMvcBuilder AddJellyfinApi(this IServiceCollection serviceCollection, string baseUrl, IEnumerable pluginAssemblies) + public static IMvcBuilder AddJellyfinApi(this IServiceCollection serviceCollection, IEnumerable pluginAssemblies) { IMvcBuilder mvcBuilder = serviceCollection .AddCors(options => @@ -151,7 +150,6 @@ namespace Jellyfin.Server.Extensions }) .AddMvc(opts => { - opts.UseGeneralRoutePrefix(baseUrl); opts.OutputFormatters.Insert(0, new CamelCaseJsonProfileFormatter()); opts.OutputFormatters.Insert(0, new PascalCaseJsonProfileFormatter()); diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 14cc5f4c24..223bb25580 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -169,7 +169,7 @@ namespace Jellyfin.Server // If hosting the web client, validate the client content path if (startupConfig.HostWebClient()) { - string? webContentPath = DashboardController.GetWebClientUiPath(startupConfig, appHost.ServerConfigurationManager); + string? webContentPath = appHost.ServerConfigurationManager.ApplicationPaths.WebPath; if (!Directory.Exists(webContentPath) || Directory.GetFiles(webContentPath).Length == 0) { throw new InvalidOperationException( diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index cbc1c040cb..73c00260e8 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -9,9 +9,12 @@ using MediaBrowser.Common; using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Extensions; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting; using Prometheus; @@ -44,7 +47,7 @@ namespace Jellyfin.Server { services.AddResponseCompression(); services.AddHttpContextAccessor(); - services.AddJellyfinApi(_serverConfigurationManager.Configuration.BaseUrl.TrimStart('/'), _applicationHost.GetApiPluginAssemblies()); + services.AddJellyfinApi(_applicationHost.GetApiPluginAssemblies()); services.AddJellyfinApiSwagger(); @@ -75,10 +78,12 @@ namespace Jellyfin.Server /// The application builder. /// The webhost environment. /// The server application host. + /// The application config. public void Configure( IApplicationBuilder app, IWebHostEnvironment env, - IServerApplicationHost serverApplicationHost) + IServerApplicationHost serverApplicationHost, + IConfiguration appConfig) { if (env.IsDevelopment()) { @@ -95,6 +100,16 @@ namespace Jellyfin.Server // TODO app.UseMiddleware(); + app.UsePathBase(_serverConfigurationManager.Configuration.BaseUrl); + if (appConfig.HostWebClient()) + { + app.UseStaticFiles(new StaticFileOptions + { + FileProvider = new PhysicalFileProvider(_serverConfigurationManager.ApplicationPaths.WebPath), + RequestPath = "/web" + }); + } + app.UseAuthentication(); app.UseJellyfinApiSwagger(_serverConfigurationManager); app.UseRouting(); @@ -102,7 +117,7 @@ namespace Jellyfin.Server app.UseAuthorization(); if (_serverConfigurationManager.Configuration.EnableMetrics) { - // Must be registered after any middleware that could chagne HTTP response codes or the data will be bad + // Must be registered after any middleware that could change HTTP response codes or the data will be bad app.UseHttpMetrics(); } diff --git a/MediaBrowser.Common/Configuration/IApplicationPaths.cs b/MediaBrowser.Common/Configuration/IApplicationPaths.cs index 4cea616826..57c6546675 100644 --- a/MediaBrowser.Common/Configuration/IApplicationPaths.cs +++ b/MediaBrowser.Common/Configuration/IApplicationPaths.cs @@ -1,5 +1,3 @@ -using MediaBrowser.Model.Configuration; - namespace MediaBrowser.Common.Configuration { /// @@ -17,8 +15,7 @@ namespace MediaBrowser.Common.Configuration /// Gets the path to the web UI resources folder. /// /// - /// This value is not relevant if the server is configured to not host any static web content. Additionally, - /// the value for takes precedence over this one. + /// This value is not relevant if the server is configured to not host any static web content. /// string WebPath { get; } diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 33975bc1ee..97748bd0ca 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -166,12 +166,6 @@ namespace MediaBrowser.Model.Configuration /// true if [enable dashboard response caching]; otherwise, false. public bool EnableDashboardResponseCaching { get; set; } - /// - /// Gets or sets a custom path to serve the dashboard from. - /// - /// The dashboard source path, or null if the default path should be used. - public string DashboardSourcePath { get; set; } - /// /// Gets or sets the image saving convention. /// -- cgit v1.2.3 From 78cab77f819e8d8b283f95f2e48f635bcf66fea5 Mon Sep 17 00:00:00 2001 From: cvium Date: Thu, 10 Sep 2020 11:05:46 +0200 Subject: Add Known Proxies to system configuration --- .../Extensions/ApiServiceCollectionExtensions.cs | 13 +++++++++++-- Jellyfin.Server/Startup.cs | 3 ++- MediaBrowser.Model/Configuration/ServerConfiguration.cs | 6 ++++++ 3 files changed, 19 insertions(+), 3 deletions(-) (limited to 'MediaBrowser.Model/Configuration') diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 9319b573a4..873e22819b 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net; using System.Reflection; using Jellyfin.Api.Auth; using Jellyfin.Api.Auth.DefaultAuthorizationPolicy; @@ -17,7 +18,6 @@ using Jellyfin.Api.Constants; using Jellyfin.Api.Controllers; using Jellyfin.Server.Configuration; using Jellyfin.Server.Formatters; -using Jellyfin.Server.Middleware; using MediaBrowser.Common.Json; using MediaBrowser.Model.Entities; using Microsoft.AspNetCore.Authentication; @@ -28,6 +28,7 @@ using Microsoft.AspNetCore.HttpOverrides; using Microsoft.Extensions.DependencyInjection; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; +using AuthenticationSchemes = Jellyfin.Api.Constants.AuthenticationSchemes; namespace Jellyfin.Server.Extensions { @@ -136,8 +137,9 @@ namespace Jellyfin.Server.Extensions /// /// The service collection. /// An IEnumerable containing all plugin assemblies with API controllers. + /// A list of all known proxies to trust for X-Forwarded-For. /// The MVC builder. - public static IMvcBuilder AddJellyfinApi(this IServiceCollection serviceCollection, IEnumerable pluginAssemblies) + public static IMvcBuilder AddJellyfinApi(this IServiceCollection serviceCollection, IEnumerable pluginAssemblies, IReadOnlyList knownProxies) { IMvcBuilder mvcBuilder = serviceCollection .AddCors() @@ -145,6 +147,13 @@ namespace Jellyfin.Server.Extensions .Configure(options => { options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; + for (var i = 0; i < knownProxies.Count; i++) + { + if (IPAddress.TryParse(knownProxies[i], out var address)) + { + options.KnownProxies.Add(address); + } + } }) .AddMvc(opts => { diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 9e969c0c16..2f4620aa63 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -52,7 +52,7 @@ namespace Jellyfin.Server { options.HttpsPort = _serverApplicationHost.HttpsPort; }); - services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies()); + services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.Configuration.KnownProxies); services.AddJellyfinApiSwagger(); @@ -103,6 +103,7 @@ namespace Jellyfin.Server mainApp.UseDeveloperExceptionPage(); } + mainApp.UseForwardedHeaders(); mainApp.UseMiddleware(); mainApp.UseMiddleware(); diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 68dc1cc83d..48d1a7346a 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -268,6 +268,11 @@ namespace MediaBrowser.Model.Configuration /// public string[] CorsHosts { get; set; } + /// + /// Gets or sets the known proxies. + /// + public string[] KnownProxies { get; set; } + /// /// Initializes a new instance of the class. /// @@ -378,6 +383,7 @@ namespace MediaBrowser.Model.Configuration EnableSlowResponseWarning = true; SlowResponseThresholdMs = 500; CorsHosts = new[] { "*" }; + KnownProxies = Array.Empty(); } } -- cgit v1.2.3 From 9ef79d190b2490a03c566bfaaf963fbba7d124a9 Mon Sep 17 00:00:00 2001 From: Jim Cartlidge Date: Sat, 12 Sep 2020 16:41:37 +0100 Subject: Large number of files --- Emby.Dlna/Main/DlnaEntryPoint.cs | 42 +- Emby.Dlna/PlayTo/PlayToManager.cs | 10 +- Emby.Server.Implementations/ApplicationHost.cs | 218 +--- .../Emby.Server.Implementations.csproj | 2 +- .../LiveTv/LiveTvMediaSourceProvider.cs | 2 +- .../LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs | 1 + .../TunerHosts/HdHomerun/HdHomerunUdpStream.cs | 3 +- .../LiveTv/TunerHosts/M3UTunerHost.cs | 1 + .../Networking/NetworkManager.cs | 556 --------- Emby.Server.Implementations/Udp/UdpServer.cs | 2 +- Jellyfin.Api/Auth/BaseAuthorizationHandler.cs | 3 +- .../DefaultAuthorizationHandler.cs | 3 +- .../Auth/DownloadPolicy/DownloadHandler.cs | 3 +- ...FirstTimeOrIgnoreParentalControlSetupHandler.cs | 3 +- .../FirstTimeSetupOrDefaultHandler.cs | 1 + .../FirstTimeSetupOrElevatedHandler.cs | 1 + .../IgnoreParentalControlHandler.cs | 3 +- .../LocalAccessOrRequiresElevationHandler.cs | 3 +- .../Auth/LocalAccessPolicy/LocalAccessHandler.cs | 3 +- .../RequiresElevationHandler.cs | 1 + Jellyfin.Api/Controllers/SystemController.cs | 11 +- Jellyfin.Api/Controllers/UserController.cs | 3 +- Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 3 +- Jellyfin.Api/Helpers/MediaInfoHelper.cs | 3 +- Jellyfin.Networking/Jellyfin.Networking.csproj | 39 + Jellyfin.Networking/Manager/INetworkManager.cs | 189 +++ Jellyfin.Networking/Manager/NetworkManager.cs | 1203 ++++++++++++++++++++ .../Jellyfin.Server.Implementations.csproj | 1 + .../Users/UserManager.cs | 4 +- Jellyfin.Server/CoreAppHost.cs | 4 +- .../IpBasedAccessValidationMiddleware.cs | 37 +- .../Middleware/LanFilteringMiddleware.cs | 24 +- Jellyfin.Server/Program.cs | 67 +- MediaBrowser.Common/Net/INetworkManager.cs | 97 -- MediaBrowser.Controller/IServerApplicationHost.cs | 30 +- .../Configuration/PathSubstitution.cs | 19 + .../Configuration/ServerConfiguration.cs | 388 ++++--- MediaBrowser.sln | 34 +- RSSDP/RSSDP.csproj | 1 + RSSDP/SsdpCommunicationsServer.cs | 5 +- RSSDP/SsdpDevicePublisher.cs | 11 +- .../LocalAccessPolicy/LocalAccessHandlerTests.cs | 3 +- .../JellyfinApplicationFactory.cs | 3 - 43 files changed, 1836 insertions(+), 1204 deletions(-) delete mode 100644 Emby.Server.Implementations/Networking/NetworkManager.cs create mode 100644 Jellyfin.Networking/Jellyfin.Networking.csproj create mode 100644 Jellyfin.Networking/Manager/INetworkManager.cs create mode 100644 Jellyfin.Networking/Manager/NetworkManager.cs delete mode 100644 MediaBrowser.Common/Net/INetworkManager.cs create mode 100644 MediaBrowser.Model/Configuration/PathSubstitution.cs (limited to 'MediaBrowser.Model/Configuration') diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index 40c2cc0e0a..98f50c09af 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -2,12 +2,14 @@ using System; using System.Globalization; +using System.Linq; using System.Net.Http; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using Emby.Dlna.PlayTo; using Emby.Dlna.Ssdp; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; @@ -25,6 +27,7 @@ using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Net; using MediaBrowser.Model.System; using Microsoft.Extensions.Logging; +using NetworkCollection; using Rssdp; using Rssdp.Infrastructure; using OperatingSystem = MediaBrowser.Common.System.OperatingSystem; @@ -134,20 +137,20 @@ namespace Emby.Dlna.Main { await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false); - await ReloadComponents().ConfigureAwait(false); + ReloadComponents(); _config.NamedConfigurationUpdated += OnNamedConfigurationUpdated; } - private async void OnNamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e) + private void OnNamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e) { if (string.Equals(e.Key, "dlna", StringComparison.OrdinalIgnoreCase)) { - await ReloadComponents().ConfigureAwait(false); + ReloadComponents(); } } - private async Task ReloadComponents() + private void ReloadComponents() { var options = _config.GetDlnaConfiguration(); @@ -155,7 +158,7 @@ namespace Emby.Dlna.Main if (options.EnableServer) { - await StartDevicePublisher(options).ConfigureAwait(false); + StartDevicePublisher(options); } else { @@ -225,7 +228,7 @@ namespace Emby.Dlna.Main } } - public async Task StartDevicePublisher(Configuration.DlnaOptions options) + public void StartDevicePublisher(Configuration.DlnaOptions options) { if (!options.BlastAliveMessages) { @@ -245,7 +248,7 @@ namespace Emby.Dlna.Main SupportPnpRootDevice = false }; - await RegisterServerEndpoints().ConfigureAwait(false); + RegisterServerEndpoints(); _publisher.StartBroadcastingAliveMessages(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds)); } @@ -255,39 +258,46 @@ namespace Emby.Dlna.Main } } - private async Task RegisterServerEndpoints() + private void RegisterServerEndpoints() { - var addresses = await _appHost.GetLocalIpAddresses(CancellationToken.None).ConfigureAwait(false); + var bindAddresses = _networkManager.GetInternalBindAddresses() + .Where(i => i.AddressFamily == AddressFamily.InterNetwork || (i.AddressFamily == AddressFamily.InterNetworkV6 && i.Address.ScopeId != 0)); var udn = CreateUuid(_appHost.SystemId); - foreach (var address in addresses) + if (!bindAddresses.Any()) { - if (address.AddressFamily == AddressFamily.InterNetworkV6) + // No interfaces returned, so use loopback. + bindAddresses = _networkManager.GetLoopbacks(); + } + + foreach (var addr in bindAddresses) + { + if (addr.AddressFamily == AddressFamily.InterNetworkV6) { // Not supporting IPv6 right now continue; } // Limit to LAN addresses only - if (!_networkManager.IsAddressInSubnets(address, true, true)) + if (!_networkManager.IsInLocalNetwork(addr)) { continue; } var fullService = "urn:schemas-upnp-org:device:MediaServer:1"; - _logger.LogInformation("Registering publisher for {0} on {1}", fullService, address); + _logger.LogInformation("Registering publisher for {0} on {1}", fullService, addr); var descriptorUri = "/dlna/" + udn + "/description.xml"; - var uri = new Uri(_appHost.GetLocalApiUrl(address) + descriptorUri); + var uri = new Uri(_appHost.GetSmartApiUrl(addr.Address) + descriptorUri); var device = new SsdpRootDevice { CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info. Location = uri, // Must point to the URL that serves your devices UPnP description document. - Address = address, - SubnetMask = _networkManager.GetLocalIpSubnetMask(address), + Address = addr.Address, + SubnetMask = ((IPNetAddress)addr).Mask, // MIGRATION: This fields is going. FriendlyName = "Jellyfin", Manufacturer = "Jellyfin", ModelName = "Jellyfin Server", diff --git a/Emby.Dlna/PlayTo/PlayToManager.cs b/Emby.Dlna/PlayTo/PlayToManager.cs index 21877f121f..10887bf60d 100644 --- a/Emby.Dlna/PlayTo/PlayToManager.cs +++ b/Emby.Dlna/PlayTo/PlayToManager.cs @@ -177,15 +177,7 @@ namespace Emby.Dlna.PlayTo _sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName); - string serverAddress; - if (info.LocalIpAddress == null || info.LocalIpAddress.Equals(IPAddress.Any) || info.LocalIpAddress.Equals(IPAddress.IPv6Any)) - { - serverAddress = await _appHost.GetLocalApiUrl(cancellationToken).ConfigureAwait(false); - } - else - { - serverAddress = _appHost.GetLocalApiUrl(info.LocalIpAddress); - } + string serverAddress = _appHost.GetSmartApiUrl(info.LocalIpAddress); controller = new PlayToController( sessionInfo, diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 642e2fdbe9..cc04cb03f5 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -46,6 +46,7 @@ using Emby.Server.Implementations.SyncPlay; using Emby.Server.Implementations.TV; using Emby.Server.Implementations.Updates; using Jellyfin.Api.Helpers; +using Jellyfin.Networking.Manager; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Events; @@ -97,6 +98,7 @@ using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Plugins.TheTvdb; using MediaBrowser.Providers.Subtitles; using MediaBrowser.XbmcMetadata.Providers; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -117,7 +119,6 @@ namespace Emby.Server.Implementations private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" }; private readonly IFileSystem _fileSystemManager; - private readonly INetworkManager _networkManager; private readonly IXmlSerializer _xmlSerializer; private readonly IStartupOptions _startupOptions; @@ -188,6 +189,11 @@ namespace Emby.Server.Implementations /// The plugins. public IReadOnlyList Plugins => _plugins; + /// + /// Gets the NetworkManager object. + /// + private readonly INetworkManager _networkManager; + /// /// Gets the logger factory. /// @@ -211,7 +217,7 @@ namespace Emby.Server.Implementations private readonly List _disposableParts = new List(); /// - /// Gets the configuration manager. + /// Gets or sets the configuration manager. /// /// The configuration manager. protected IConfigurationManager ConfigurationManager { get; set; } @@ -244,28 +250,25 @@ namespace Emby.Server.Implementations /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. - /// Instance of the interface. /// Instance of the interface. public ApplicationHost( IServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IStartupOptions options, IFileSystem fileSystem, - INetworkManager networkManager, IServiceCollection serviceCollection) { _xmlSerializer = new MyXmlSerializer(); ServiceCollection = serviceCollection; - _networkManager = networkManager; - networkManager.LocalSubnetsFn = GetConfiguredLocalSubnets; - ApplicationPaths = applicationPaths; LoggerFactory = loggerFactory; _fileSystemManager = fileSystem; ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager); + _networkManager = new NetworkManager((IServerConfigurationManager)ConfigurationManager, LoggerFactory.CreateLogger()); + Logger = LoggerFactory.CreateLogger(); _startupOptions = options; @@ -278,8 +281,6 @@ namespace Emby.Server.Implementations fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem)); - _networkManager.NetworkChanged += OnNetworkChanged; - CertificateInfo = new CertificateInfo { Path = ServerConfigurationManager.Configuration.CertificatePath, @@ -308,16 +309,6 @@ namespace Emby.Server.Implementations .Replace(appPaths.InternalMetadataPath, appPaths.VirtualInternalMetadataPath, StringComparison.OrdinalIgnoreCase); } - private string[] GetConfiguredLocalSubnets() - { - return ServerConfigurationManager.Configuration.LocalNetworkSubnets; - } - - private void OnNetworkChanged(object sender, EventArgs e) - { - _validAddressResults.Clear(); - } - /// public Version ApplicationVersion { get; } @@ -398,7 +389,7 @@ namespace Emby.Server.Implementations /// /// Resolves this instance. /// - /// The type + /// The type. /// ``0. public T Resolve() => ServiceProvider.GetService(); @@ -1091,13 +1082,10 @@ namespace Emby.Server.Implementations /// /// Gets the system status. /// - /// The cancellation token. + /// Where this request originated. /// SystemInfo. - public async Task GetSystemInfo(CancellationToken cancellationToken) + public SystemInfo GetSystemInfo(IPAddress source) { - var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false); - var transcodingTempPath = ConfigurationManager.GetTranscodePath(); - return new SystemInfo { HasPendingRestart = HasPendingRestart, @@ -1117,9 +1105,9 @@ namespace Emby.Server.Implementations CanSelfRestart = CanSelfRestart, CanLaunchWebBrowser = CanLaunchWebBrowser, HasUpdateAvailable = HasUpdateAvailable, - TranscodingTempPath = transcodingTempPath, + TranscodingTempPath = ConfigurationManager.GetTranscodePath(), ServerName = FriendlyName, - LocalAddress = localAddress, + LocalAddress = GetSmartApiUrl(source), SupportsLibraryMonitor = true, EncoderLocation = _mediaEncoder.EncoderLocation, SystemArchitecture = RuntimeInformation.OSArchitecture, @@ -1132,10 +1120,8 @@ namespace Emby.Server.Implementations .Select(i => new WakeOnLanInfo(i)) .ToList(); - public async Task GetPublicSystemInfo(CancellationToken cancellationToken) + public PublicSystemInfo GetPublicSystemInfo(IPAddress source) { - var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false); - return new PublicSystemInfo { Version = ApplicationVersionString, @@ -1143,8 +1129,8 @@ namespace Emby.Server.Implementations Id = SystemId, OperatingSystem = OperatingSystem.Id.ToString(), ServerName = FriendlyName, - LocalAddress = localAddress, - StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted + LocalAddress = GetSmartApiUrl(source), + StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted }; } @@ -1152,186 +1138,51 @@ namespace Emby.Server.Implementations public bool ListenWithHttps => Certificate != null && ServerConfigurationManager.Configuration.EnableHttps; /// - public async Task GetLocalApiUrl(CancellationToken cancellationToken) + public string GetSmartApiUrl(object source) { - try - { - // Return the first matched address, if found, or the first known local address - var addresses = await GetLocalIpAddressesInternal(false, 1, cancellationToken).ConfigureAwait(false); - if (addresses.Count == 0) - { - return null; - } - - return GetLocalApiUrl(addresses[0]); - } - catch (Exception ex) + // Published server ends with a / + if (_startupOptions.PublishedServerUrl != null) { - Logger.LogError(ex, "Error getting local Ip address information"); + // Published server ends with a '/', so we need to remove it. + return _startupOptions.PublishedServerUrl.ToString().Trim('/'); } - return null; - } + string smart = _networkManager.GetBindInterface(source, out int? port); - /// - /// Removes the scope id from IPv6 addresses. - /// - /// The IPv6 address. - /// The IPv6 address without the scope id. - private ReadOnlySpan RemoveScopeId(ReadOnlySpan address) - { - var index = address.IndexOf('%'); - if (index == -1) + // If the smartAPI doesn't start with http then treat it as a host or ip. + if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase)) { - return address; + return smart.Trim('/'); } - return address.Slice(0, index); + return GetLocalApiUrl(smart.Trim('/'), source is HttpRequest request ? request.Scheme : null, port); } - /// - public string GetLocalApiUrl(IPAddress ipAddress) + /// + public string GetLoopbackHttpApiUrl() { - if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6) + if (NetworkManager.IsIP6Enabled) { - var str = RemoveScopeId(ipAddress.ToString()); - Span span = new char[str.Length + 2]; - span[0] = '['; - str.CopyTo(span.Slice(1)); - span[^1] = ']'; - - return GetLocalApiUrl(span); + return GetLocalApiUrl("::1", Uri.UriSchemeHttp, HttpPort); } - return GetLocalApiUrl(ipAddress.ToString()); - } - - /// - public string GetLoopbackHttpApiUrl() - { return GetLocalApiUrl("127.0.0.1", Uri.UriSchemeHttp, HttpPort); } /// - public string GetLocalApiUrl(ReadOnlySpan host, string scheme = null, int? port = null) + public string GetLocalApiUrl(string host, string scheme = null, int? port = null) { // 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 { Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp), - Host = host.ToString(), + Host = host, Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort), Path = ServerConfigurationManager.Configuration.BaseUrl }.ToString().TrimEnd('/'); } - public Task> GetLocalIpAddresses(CancellationToken cancellationToken) - { - return GetLocalIpAddressesInternal(true, 0, cancellationToken); - } - - private async Task> GetLocalIpAddressesInternal(bool allowLoopback, int limit, CancellationToken cancellationToken) - { - var addresses = ServerConfigurationManager - .Configuration - .LocalNetworkAddresses - .Select(x => NormalizeConfiguredLocalAddress(x)) - .Where(i => i != null) - .ToList(); - - if (addresses.Count == 0) - { - addresses.AddRange(_networkManager.GetLocalIpAddresses()); - } - - var resultList = new List(); - - foreach (var address in addresses) - { - if (!allowLoopback) - { - if (address.Equals(IPAddress.Loopback) || address.Equals(IPAddress.IPv6Loopback)) - { - continue; - } - } - - if (await IsLocalIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false)) - { - resultList.Add(address); - - if (limit > 0 && resultList.Count >= limit) - { - return resultList; - } - } - } - - return resultList; - } - - public IPAddress NormalizeConfiguredLocalAddress(ReadOnlySpan address) - { - var index = address.Trim('/').IndexOf('/'); - if (index != -1) - { - address = address.Slice(index + 1); - } - - if (IPAddress.TryParse(address.Trim('/'), out IPAddress result)) - { - return result; - } - - return null; - } - - private readonly ConcurrentDictionary _validAddressResults = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - - private async Task IsLocalIpAddressValidAsync(IPAddress address, CancellationToken cancellationToken) - { - if (address.Equals(IPAddress.Loopback) - || address.Equals(IPAddress.IPv6Loopback)) - { - return true; - } - - var apiUrl = GetLocalApiUrl(address) + "/system/ping"; - - if (_validAddressResults.TryGetValue(apiUrl, out var cachedResult)) - { - return cachedResult; - } - - try - { - using var request = new HttpRequestMessage(HttpMethod.Post, apiUrl); - using var response = await _httpClientFactory.CreateClient(NamedClient.Default) - .SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); - - await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); - var result = await System.Text.Json.JsonSerializer.DeserializeAsync(stream, JsonDefaults.GetOptions(), cancellationToken).ConfigureAwait(false); - var valid = string.Equals(Name, result, StringComparison.OrdinalIgnoreCase); - - _validAddressResults.AddOrUpdate(apiUrl, valid, (k, v) => valid); - Logger.LogDebug("Ping test result to {0}. Success: {1}", apiUrl, valid); - return valid; - } - catch (OperationCanceledException) - { - Logger.LogDebug("Ping test result to {0}. Success: {1}", apiUrl, "Cancelled"); - throw; - } - catch (Exception ex) - { - Logger.LogDebug(ex, "Ping test result to {0}. Success: {1}", apiUrl, false); - - _validAddressResults.AddOrUpdate(apiUrl, false, (k, v) => false); - return false; - } - } - public string FriendlyName => string.IsNullOrEmpty(ServerConfigurationManager.Configuration.ServerName) ? Environment.MachineName @@ -1486,6 +1337,7 @@ namespace Emby.Server.Implementations _disposed = true; } + } internal class CertificateInfo diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 0a348f0d00..db81167433 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -36,7 +36,7 @@ - + diff --git a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs index 8a0c0043a9..598cf0af7c 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs @@ -76,7 +76,7 @@ namespace Emby.Server.Implementations.LiveTv } var list = sources.ToList(); - var serverUrl = await _appHost.GetLocalApiUrl(cancellationToken).ConfigureAwait(false); + var serverUrl = _appHost.GetSmartApiUrl(string.Empty); foreach (var source in list) { diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index 28e30fac8b..1cf129ad2d 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -10,6 +10,7 @@ using System.Net.Http; using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs index 6730751d50..02ee302d0d 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs @@ -7,6 +7,7 @@ using System.Net; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller; @@ -57,7 +58,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun var mediaSource = OriginalMediaSource; var uri = new Uri(mediaSource.Path); - var localPort = _networkManager.GetRandomUnusedUdpPort(); + var localPort = 50000; // Will return to random after next PR. Directory.CreateDirectory(Path.GetDirectoryName(TempFilePath)); diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index 8107bc427b..f297ecd5df 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller; diff --git a/Emby.Server.Implementations/Networking/NetworkManager.cs b/Emby.Server.Implementations/Networking/NetworkManager.cs deleted file mode 100644 index 089ec30e6b..0000000000 --- a/Emby.Server.Implementations/Networking/NetworkManager.cs +++ /dev/null @@ -1,556 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.NetworkInformation; -using System.Net.Sockets; -using System.Threading.Tasks; -using MediaBrowser.Common.Net; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.Networking -{ - /// - /// Class to take care of network interface management. - /// - public class NetworkManager : INetworkManager - { - private readonly ILogger _logger; - - private IPAddress[] _localIpAddresses; - private readonly object _localIpAddressSyncLock = new object(); - - private readonly object _subnetLookupLock = new object(); - private readonly Dictionary> _subnetLookup = new Dictionary>(StringComparer.Ordinal); - - private List _macAddresses; - - /// - /// Initializes a new instance of the class. - /// - /// Logger to use for messages. - public NetworkManager(ILogger logger) - { - _logger = logger; - - NetworkChange.NetworkAddressChanged += OnNetworkAddressChanged; - NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged; - } - - /// - public event EventHandler NetworkChanged; - - /// - public Func LocalSubnetsFn { get; set; } - - private void OnNetworkAvailabilityChanged(object sender, NetworkAvailabilityEventArgs e) - { - _logger.LogDebug("NetworkAvailabilityChanged"); - OnNetworkChanged(); - } - - private void OnNetworkAddressChanged(object sender, EventArgs e) - { - _logger.LogDebug("NetworkAddressChanged"); - OnNetworkChanged(); - } - - private void OnNetworkChanged() - { - lock (_localIpAddressSyncLock) - { - _localIpAddresses = null; - _macAddresses = null; - } - - NetworkChanged?.Invoke(this, EventArgs.Empty); - } - - /// - public IPAddress[] GetLocalIpAddresses() - { - lock (_localIpAddressSyncLock) - { - if (_localIpAddresses == null) - { - var addresses = GetLocalIpAddressesInternal().ToArray(); - - _localIpAddresses = addresses; - } - - return _localIpAddresses; - } - } - - private List GetLocalIpAddressesInternal() - { - var list = GetIPsDefault().ToList(); - - if (list.Count == 0) - { - list = GetLocalIpAddressesFallback().GetAwaiter().GetResult().ToList(); - } - - var listClone = new List(); - - var subnets = LocalSubnetsFn(); - - foreach (var i in list) - { - if (i.IsIPv6LinkLocal || i.ToString().StartsWith("169.254.", StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - if (Array.IndexOf(subnets, $"[{i}]") == -1) - { - listClone.Add(i); - } - } - - return listClone - .OrderBy(i => i.AddressFamily == AddressFamily.InterNetwork ? 0 : 1) - // .ThenBy(i => listClone.IndexOf(i)) - .GroupBy(i => i.ToString()) - .Select(x => x.First()) - .ToList(); - } - - /// - public bool IsInPrivateAddressSpace(string endpoint) - { - return IsInPrivateAddressSpace(endpoint, true); - } - - // Checks if the address in endpoint is an RFC1918, RFC1122, or RFC3927 address - private bool IsInPrivateAddressSpace(string endpoint, bool checkSubnets) - { - if (string.Equals(endpoint, "::1", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - // IPV6 - if (endpoint.Split('.').Length > 4) - { - // Handle ipv4 mapped to ipv6 - var originalEndpoint = endpoint; - endpoint = endpoint.Replace("::ffff:", string.Empty, StringComparison.OrdinalIgnoreCase); - - if (string.Equals(endpoint, originalEndpoint, StringComparison.OrdinalIgnoreCase)) - { - return false; - } - } - - // Private address space: - - if (string.Equals(endpoint, "localhost", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - if (!IPAddress.TryParse(endpoint, out var ipAddress)) - { - return false; - } - - byte[] octet = ipAddress.GetAddressBytes(); - - if ((octet[0] == 10) || - (octet[0] == 172 && (octet[1] >= 16 && octet[1] <= 31)) || // RFC1918 - (octet[0] == 192 && octet[1] == 168) || // RFC1918 - (octet[0] == 127) || // RFC1122 - (octet[0] == 169 && octet[1] == 254)) // RFC3927 - { - return true; - } - - if (checkSubnets && IsInPrivateAddressSpaceAndLocalSubnet(endpoint)) - { - return true; - } - - return false; - } - - /// - public bool IsInPrivateAddressSpaceAndLocalSubnet(string endpoint) - { - if (endpoint.StartsWith("10.", StringComparison.OrdinalIgnoreCase)) - { - var endpointFirstPart = endpoint.Split('.')[0]; - - var subnets = GetSubnets(endpointFirstPart); - - foreach (var subnet_Match in subnets) - { - // logger.LogDebug("subnet_Match:" + subnet_Match); - - if (endpoint.StartsWith(subnet_Match + ".", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - } - - return false; - } - - // Gives a list of possible subnets from the system whose interface ip starts with endpointFirstPart - private List GetSubnets(string endpointFirstPart) - { - lock (_subnetLookupLock) - { - if (_subnetLookup.TryGetValue(endpointFirstPart, out var subnets)) - { - return subnets; - } - - subnets = new List(); - - foreach (var adapter in NetworkInterface.GetAllNetworkInterfaces()) - { - foreach (var unicastIPAddressInformation in adapter.GetIPProperties().UnicastAddresses) - { - if (unicastIPAddressInformation.Address.AddressFamily == AddressFamily.InterNetwork && endpointFirstPart == unicastIPAddressInformation.Address.ToString().Split('.')[0]) - { - int subnet_Test = 0; - foreach (string part in unicastIPAddressInformation.IPv4Mask.ToString().Split('.')) - { - if (part.Equals("0", StringComparison.Ordinal)) - { - break; - } - - subnet_Test++; - } - - var subnet_Match = string.Join(".", unicastIPAddressInformation.Address.ToString().Split('.').Take(subnet_Test).ToArray()); - - // TODO: Is this check necessary? - if (adapter.OperationalStatus == OperationalStatus.Up) - { - subnets.Add(subnet_Match); - } - } - } - } - - _subnetLookup[endpointFirstPart] = subnets; - - return subnets; - } - } - - /// - public bool IsInLocalNetwork(string endpoint) - { - return IsInLocalNetworkInternal(endpoint, true); - } - - /// - public bool IsAddressInSubnets(string addressString, string[] subnets) - { - return IsAddressInSubnets(IPAddress.Parse(addressString), addressString, subnets); - } - - /// - public bool IsAddressInSubnets(IPAddress address, bool excludeInterfaces, bool excludeRFC) - { - byte[] octet = address.GetAddressBytes(); - - if ((octet[0] == 127) || // RFC1122 - (octet[0] == 169 && octet[1] == 254)) // RFC3927 - { - // don't use on loopback or 169 interfaces - return false; - } - - string addressString = address.ToString(); - string excludeAddress = "[" + addressString + "]"; - var subnets = LocalSubnetsFn(); - - // Include any address if LAN subnets aren't specified - if (subnets.Length == 0) - { - return true; - } - - // Exclude any addresses if they appear in the LAN list in [ ] - if (Array.IndexOf(subnets, excludeAddress) != -1) - { - return false; - } - - return IsAddressInSubnets(address, addressString, subnets); - } - - /// - /// Checks if the give address falls within the ranges given in [subnets]. The addresses in subnets can be hosts or subnets in the CIDR format. - /// - /// IPAddress version of the address. - /// The address to check. - /// If true, check against addresses in the LAN settings which have [] arroud and return true if it matches the address give in address. - /// falseif the address isn't in the subnets, true otherwise. - private static bool IsAddressInSubnets(IPAddress address, string addressString, string[] subnets) - { - foreach (var subnet in subnets) - { - var normalizedSubnet = subnet.Trim(); - // Is the subnet a host address and does it match the address being passes? - if (string.Equals(normalizedSubnet, addressString, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - // Parse CIDR subnets and see if address falls within it. - if (normalizedSubnet.Contains('/', StringComparison.Ordinal)) - { - try - { - var ipNetwork = IPNetwork.Parse(normalizedSubnet); - if (ipNetwork.Contains(address)) - { - return true; - } - } - catch - { - // Ignoring - invalid subnet passed encountered. - } - } - } - - return false; - } - - private bool IsInLocalNetworkInternal(string endpoint, bool resolveHost) - { - if (string.IsNullOrEmpty(endpoint)) - { - throw new ArgumentNullException(nameof(endpoint)); - } - - if (IPAddress.TryParse(endpoint, out var address)) - { - var addressString = address.ToString(); - - var localSubnetsFn = LocalSubnetsFn; - if (localSubnetsFn != null) - { - var localSubnets = localSubnetsFn(); - foreach (var subnet in localSubnets) - { - // Only validate if there's at least one valid entry. - if (!string.IsNullOrWhiteSpace(subnet)) - { - return IsAddressInSubnets(address, addressString, localSubnets) || IsInPrivateAddressSpace(addressString, false); - } - } - } - - int lengthMatch = 100; - if (address.AddressFamily == AddressFamily.InterNetwork) - { - lengthMatch = 4; - if (IsInPrivateAddressSpace(addressString, true)) - { - return true; - } - } - else if (address.AddressFamily == AddressFamily.InterNetworkV6) - { - lengthMatch = 9; - if (IsInPrivateAddressSpace(endpoint, true)) - { - return true; - } - } - - // Should be even be doing this with ipv6? - if (addressString.Length >= lengthMatch) - { - var prefix = addressString.Substring(0, lengthMatch); - - if (GetLocalIpAddresses().Any(i => i.ToString().StartsWith(prefix, StringComparison.OrdinalIgnoreCase))) - { - return true; - } - } - } - else if (resolveHost) - { - if (Uri.TryCreate(endpoint, UriKind.RelativeOrAbsolute, out var uri)) - { - try - { - var host = uri.DnsSafeHost; - _logger.LogDebug("Resolving host {0}", host); - - address = GetIpAddresses(host).GetAwaiter().GetResult().FirstOrDefault(); - - if (address != null) - { - _logger.LogDebug("{0} resolved to {1}", host, address); - - return IsInLocalNetworkInternal(address.ToString(), false); - } - } - catch (InvalidOperationException) - { - // Can happen with reverse proxy or IIS url rewriting? - } - catch (Exception ex) - { - _logger.LogError(ex, "Error resolving hostname"); - } - } - } - - return false; - } - - private static Task GetIpAddresses(string hostName) - { - return Dns.GetHostAddressesAsync(hostName); - } - - private IEnumerable GetIPsDefault() - { - IEnumerable interfaces; - - try - { - interfaces = NetworkInterface.GetAllNetworkInterfaces() - .Where(x => x.OperationalStatus == OperationalStatus.Up - || x.OperationalStatus == OperationalStatus.Unknown); - } - catch (NetworkInformationException ex) - { - _logger.LogError(ex, "Error in GetAllNetworkInterfaces"); - return Enumerable.Empty(); - } - - return interfaces.SelectMany(network => - { - var ipProperties = network.GetIPProperties(); - - // Exclude any addresses if they appear in the LAN list in [ ] - - return ipProperties.UnicastAddresses - .Select(i => i.Address) - .Where(i => i.AddressFamily == AddressFamily.InterNetwork || i.AddressFamily == AddressFamily.InterNetworkV6); - }).GroupBy(i => i.ToString()) - .Select(x => x.First()); - } - - private static async Task> GetLocalIpAddressesFallback() - { - var host = await Dns.GetHostEntryAsync(Dns.GetHostName()).ConfigureAwait(false); - - // Reverse them because the last one is usually the correct one - // It's not fool-proof so ultimately the consumer will have to examine them and decide - return host.AddressList - .Where(i => i.AddressFamily == AddressFamily.InterNetwork || i.AddressFamily == AddressFamily.InterNetworkV6) - .Reverse(); - } - - /// - /// Gets a random port number that is currently available. - /// - /// System.Int32. - public int GetRandomUnusedTcpPort() - { - var listener = new TcpListener(IPAddress.Any, 0); - listener.Start(); - var port = ((IPEndPoint)listener.LocalEndpoint).Port; - listener.Stop(); - return port; - } - - /// - public int GetRandomUnusedUdpPort() - { - var localEndPoint = new IPEndPoint(IPAddress.Any, 0); - using (var udpClient = new UdpClient(localEndPoint)) - { - return ((IPEndPoint)udpClient.Client.LocalEndPoint).Port; - } - } - - /// - public List GetMacAddresses() - { - return _macAddresses ??= GetMacAddressesInternal().ToList(); - } - - private static IEnumerable GetMacAddressesInternal() - => NetworkInterface.GetAllNetworkInterfaces() - .Where(i => i.NetworkInterfaceType != NetworkInterfaceType.Loopback) - .Select(x => x.GetPhysicalAddress()) - .Where(x => !x.Equals(PhysicalAddress.None)); - - /// - public bool IsInSameSubnet(IPAddress address1, IPAddress address2, IPAddress subnetMask) - { - IPAddress network1 = GetNetworkAddress(address1, subnetMask); - IPAddress network2 = GetNetworkAddress(address2, subnetMask); - return network1.Equals(network2); - } - - private IPAddress GetNetworkAddress(IPAddress address, IPAddress subnetMask) - { - byte[] ipAdressBytes = address.GetAddressBytes(); - byte[] subnetMaskBytes = subnetMask.GetAddressBytes(); - - if (ipAdressBytes.Length != subnetMaskBytes.Length) - { - throw new ArgumentException("Lengths of IP address and subnet mask do not match."); - } - - byte[] broadcastAddress = new byte[ipAdressBytes.Length]; - for (int i = 0; i < broadcastAddress.Length; i++) - { - broadcastAddress[i] = (byte)(ipAdressBytes[i] & subnetMaskBytes[i]); - } - - return new IPAddress(broadcastAddress); - } - - /// - public IPAddress GetLocalIpSubnetMask(IPAddress address) - { - NetworkInterface[] interfaces; - - try - { - var validStatuses = new[] { OperationalStatus.Up, OperationalStatus.Unknown }; - - interfaces = NetworkInterface.GetAllNetworkInterfaces() - .Where(i => validStatuses.Contains(i.OperationalStatus)) - .ToArray(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error in GetAllNetworkInterfaces"); - return null; - } - - foreach (NetworkInterface ni in interfaces) - { - foreach (UnicastIPAddressInformation ip in ni.GetIPProperties().UnicastAddresses) - { - if (ip.Address.Equals(address) && ip.IPv4Mask != null) - { - return ip.IPv4Mask; - } - } - } - - return null; - } - } -} diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs index b7a59cee2d..3dc34da5cf 100644 --- a/Emby.Server.Implementations/Udp/UdpServer.cs +++ b/Emby.Server.Implementations/Udp/UdpServer.cs @@ -49,7 +49,7 @@ namespace Emby.Server.Implementations.Udp { string localUrl = !string.IsNullOrEmpty(_config[AddressOverrideConfigKey]) ? _config[AddressOverrideConfigKey] - : await _appHost.GetLocalApiUrl(cancellationToken).ConfigureAwait(false); + : _appHost.GetSmartApiUrl(string.Empty); // MIGRATION: Temp value. if (!string.IsNullOrEmpty(localUrl)) { diff --git a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs index d732b6bc6a..08746b346b 100644 --- a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs +++ b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs @@ -1,6 +1,7 @@ -using System.Security.Claims; +using System.Security.Claims; using Jellyfin.Api.Helpers; using Jellyfin.Data.Enums; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; diff --git a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs index b5913daab9..69e6a8fb2d 100644 --- a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs +++ b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs b/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs index b61680ab1a..d1297119cd 100644 --- a/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs +++ b/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs b/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs index 31482a930f..53b5d47787 100644 --- a/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs +++ b/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs index 9815e252ee..abdf2858d0 100644 --- a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs +++ b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs index decbe0c035..ada8a0d4ec 100644 --- a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs +++ b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using Jellyfin.Api.Constants; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; diff --git a/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs b/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs index 5213bc4cb7..475e3cdac4 100644 --- a/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs +++ b/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs index 14722aa57e..d022c9067f 100644 --- a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs +++ b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs @@ -1,5 +1,6 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using Jellyfin.Api.Constants; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs index af73352bcc..418d63de68 100644 --- a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs +++ b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs b/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs index b235c4b63b..a1cddbca3e 100644 --- a/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs +++ b/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using Jellyfin.Api.Constants; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index 4cb1984a2f..6876b47b44 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.IO; @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; @@ -64,9 +65,9 @@ namespace Jellyfin.Api.Controllers [HttpGet("Info")] [Authorize(Policy = Policies.FirstTimeSetupOrIgnoreParentalControl)] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> GetSystemInfo() + public ActionResult GetSystemInfo() { - return await _appHost.GetSystemInfo(CancellationToken.None).ConfigureAwait(false); + return _appHost.GetSystemInfo(Request.HttpContext.Connection.RemoteIpAddress); } /// @@ -76,9 +77,9 @@ namespace Jellyfin.Api.Controllers /// A with public info about the system. [HttpGet("Info/Public")] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> GetPublicSystemInfo() + public ActionResult GetPublicSystemInfo() { - return await _appHost.GetPublicSystemInfo(CancellationToken.None).ConfigureAwait(false); + return _appHost.GetPublicSystemInfo(Request.HttpContext.Connection.RemoteIpAddress); } /// diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 630e9df6ac..152e650bc0 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; @@ -7,6 +7,7 @@ using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.UserDtos; using Jellyfin.Data.Enums; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Authentication; diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index af0519ffa8..3be8734b94 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -8,6 +8,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Models.StreamingDtos; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs index 1207fb5134..d63e3ab118 100644 --- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs +++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Globalization; using System.Linq; using System.Text.Json; @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; diff --git a/Jellyfin.Networking/Jellyfin.Networking.csproj b/Jellyfin.Networking/Jellyfin.Networking.csproj new file mode 100644 index 0000000000..b6834cb0ee --- /dev/null +++ b/Jellyfin.Networking/Jellyfin.Networking.csproj @@ -0,0 +1,39 @@ + + + Jellyfin.Networking + Library + netstandard2.1 + false + true + true + enable + + + + + + + + + + + + + + + ../jellyfin.ruleset + + + + + + + + + + + + + + + diff --git a/Jellyfin.Networking/Manager/INetworkManager.cs b/Jellyfin.Networking/Manager/INetworkManager.cs new file mode 100644 index 0000000000..ba571750b9 --- /dev/null +++ b/Jellyfin.Networking/Manager/INetworkManager.cs @@ -0,0 +1,189 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.NetworkInformation; +using MediaBrowser.Model.Configuration; +using NetworkCollection; + +namespace Jellyfin.Networking.Manager +{ + /// + /// Interface for the NetworkManager class. + /// + public interface INetworkManager + { + /// + /// Event triggered on network changes. + /// + event EventHandler NetworkChanged; + + /// + /// Gets the Published server override list. + /// + Dictionary PublishedServerOverrides { get; } + + /// + /// Gets a value indicating whether is all IPv6 interfaces are trusted as internal. + /// + public bool TrustAllIP6Interfaces { get; } + + /// + /// Gets returns the remote address filter. + /// + NetCollection RemoteAddressFilter { get; } + + /// + /// Calculates the list of interfaces to use for Kestrel. + /// + /// A NetCollection object containing all the interfaces to bind. + /// If all the interfaces are specified, and none are excluded, it returns zero items + /// to represent any address. + NetCollection GetAllBindInterfaces(); + + /// + /// Returns a collection containing the loopback interfaces. + /// + /// Netcollection. + public NetCollection GetLoopbacks(); + + /// + /// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo) + /// If no bind addresses are specified, an internal interface address is selected. + /// The priority of selection is as follows:- + /// + /// The value contained in the startup parameter --published-server-url. + /// + /// If the user specified custom subnet overrides, the correct subnet for the source address. + /// + /// If the user specified bind interfaces to use:- + /// The bind interface that contains the source subnet. + /// The first bind interface specified that suits best first the source's endpoint. eg. external or internal. + /// + /// If the source is from a public subnet address range and the user hasn't specified any bind addresses:- + /// The first public interface that isn't a loopback and contains the source subnet. + /// The first public interface that isn't a loopback. Priority is given to interfaces with gateways. + /// An internal interface if there are no public ip addresses. + /// + /// If the source is from a private subnet address range and the user hasn't specified any bind addresses:- + /// The first private interface that contains the source subnet. + /// The first private interface that isn't a loopback. Priority is given to interfaces with gateways. + /// + /// If no interfaces meet any of these criteria, then a loopback address is returned. + /// + /// Interface that have been specifically excluded from binding are not used in any of the calculations. + /// + /// Source of the request. + /// Optional port returned, if it's part of an override. + /// IP Address to use, or loopback address if all else fails. + string GetBindInterface(object? source, out int? port); + + /// + /// Checks to see if the ip address is specifically excluded in LocalNetworkAddresses. + /// + /// IP address to check. + /// True if it is. + bool IsExcludedInterface(IPAddress address); + + /// + /// Get a list of all the MAC addresses associated with active interfaces. + /// + /// List of MAC addresses. + List GetMacAddresses(); + + /// + /// Checks to see if the IP Address provided matches an interface that has a gateway. + /// + /// IP to check. Can be an IPAddress or an IPObject. + /// Result of the check. + public bool IsGatewayInterface(object? addressObj); + + /// + /// Returns true if the address is a private address. + /// The config option TrustIP6Interfaces overrides this functions behaviour. + /// + /// Address to check. + /// True or False. + bool IsPrivateAddressRange(IPObject address); + + /// + /// Returns true if the address is part of the user defined LAN. + /// The config option TrustIP6Interfaces overrides this functions behaviour. + /// + /// IP to check. + /// True if endpoint is within the LAN range. + bool IsInLocalNetwork(string address); + + /// + /// Returns true if the address is part of the user defined LAN. + /// The config option TrustIP6Interfaces overrides this functions behaviour. + /// + /// IP to check. + /// True if endpoint is within the LAN range. + bool IsInLocalNetwork(IPObject address); + + /// + /// Returns true if the address is part of the user defined LAN. + /// The config option TrustIP6Interfaces overrides this functions behaviour. + /// + /// IP to check. + /// True if endpoint is within the LAN range. + bool IsInLocalNetwork(IPAddress address); + + /// + /// Attempts to convert the token to an IP address, permitting for interface descriptions and indexes. + /// eg. "eth1", or "TP-LINK Wireless USB Adapter". + /// + /// Token to parse. + /// Resultant object if successful. + /// Success of the operation. + bool TryParseInterface(string token, out IPNetAddress result); + + /// + /// Parses an array of strings into a NetCollection. + /// + /// Values to parse. + /// When true, only include values in []. When false, ignore bracketed values. + /// IPCollection object containing the value strings. + NetCollection CreateIPCollection(string[] values, bool bracketed = false); + + /// + /// Returns all the internal Bind interface addresses. + /// + /// An internal list of interfaces addresses. + NetCollection GetInternalBindAddresses(); + + /// + /// Checks to see if an IP address is still a valid interface address. + /// + /// IP address to check. + /// True if it is. + bool IsValidInterfaceAddress(IPAddress address); + + /// + /// Returns true if the IP address is in the excluded list. + /// + /// IP to check. + /// True if excluded. + bool IsExcluded(IPAddress ip); + + /// + /// Returns true if the IP address is in the excluded list. + /// + /// IP to check. + /// True if excluded. + bool IsExcluded(EndPoint ip); + + /// + /// Gets the filtered LAN ip addresses. + /// + /// Optional filter for the list. + /// Returns a filtered list of LAN addresses. + NetCollection GetFilteredLANSubnets(NetCollection? filter = null); + + /// + /// Reloads all settings and re-initialises the instance. + /// + /// to use. + public void UpdateSettings(ServerConfiguration config); + } +} diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs new file mode 100644 index 0000000000..36a0a94a09 --- /dev/null +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -0,0 +1,1203 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Threading.Tasks; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.Configuration; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using NetworkCollection; + +namespace Jellyfin.Networking.Manager +{ + /// + /// Class to take care of network interface management. + /// + public class NetworkManager : INetworkManager, IDisposable + { + private static NetworkManager? _instance; + + /// + /// Contains the description of the interface along with its index. + /// + private readonly SortedList _interfaceNames; + + /// + /// Threading lock for network interfaces. + /// + private readonly object _intLock = new object(); + + /// + /// List of all interface addresses and masks. + /// + private readonly NetCollection _interfaceAddresses; + + /// + /// List of all interface MAC addresses. + /// + private readonly List _macAddresses; + + private readonly ILogger _logger; + + private readonly IConfigurationManager _configurationManager; + + /// + /// Holds the bind address overrides. + /// + private readonly Dictionary _overrideUrls; + + /// + /// Used to stop "event-racing conditions". + /// + private bool _eventfire; + + /// + /// Unfiltered user defined LAN subnets. (Configuration.LocalNetworkSubnets). + /// or internal interface network subnets if undefined by user. + /// + private NetCollection _lanSubnets; + + /// + /// User defined list of subnets to excluded from the LAN. + /// + private NetCollection _excludedSubnets; + + /// + /// List of interface addresses to bind the WS. + /// + private NetCollection _bindAddresses; + + /// + /// List of interface addresses to exclude from bind. + /// + private NetCollection _bindExclusions; + + /// + /// Caches list of all internal filtered interface addresses and masks. + /// + private NetCollection _internalInterfaces; + + /// + /// Flag set when no custom LAN has been defined in the config. + /// + private bool _usingPrivateAddresses; + + /// + /// True if this object is disposed. + /// + private bool _disposed; + + /// + /// Initializes a new instance of the class. + /// + /// IServerConfigurationManager instance. + /// Logger to use for messages. +#pragma warning disable CS8618 // Non-nullable field is uninitialized. : Values are set in InitialiseLAN function. Compiler doesn't yet recognise this. + public NetworkManager(IConfigurationManager configurationManager, ILogger logger) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _configurationManager = configurationManager ?? throw new ArgumentNullException(nameof(configurationManager)); + + _interfaceAddresses = new NetCollection(unique: false); + _macAddresses = new List(); + _interfaceNames = new SortedList(); + _overrideUrls = new Dictionary(); + + UpdateSettings((ServerConfiguration)_configurationManager.CommonConfiguration); + if (!IsIP6Enabled && !IsIP4Enabled) + { + throw new ApplicationException("IPv4 and IPv6 cannot both be disabled."); + } + + NetworkChange.NetworkAddressChanged += OnNetworkAddressChanged; + NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged; + + _configurationManager.ConfigurationUpdated += ConfigurationUpdated; + + Instance = this; + } +#pragma warning restore CS8618 // Non-nullable field is uninitialized. + + /// + /// Event triggered on network changes. + /// + public event EventHandler? NetworkChanged; + + /// + /// Gets the singleton of this object. + /// + public static NetworkManager Instance + { + get => GetInstance(); + + internal set + { + _instance = value; + } + } + + /// + /// Gets the unique network location signature, which is updated on every network change. + /// + public static string NetworkLocationSignature { get; internal set; } = Guid.NewGuid().ToString(); + + /// + /// Gets a value indicating whether IP6 is enabled. + /// + public static bool IsIP6Enabled { get; internal set; } + + /// + /// Gets a value indicating whether IP4 is enabled. + /// + public static bool IsIP4Enabled { get; internal set; } = true; + + /// + /// Gets a value indicating whether is multi-socket binding available. + /// + public static bool EnableMultiSocketBinding { get; internal set; } = true; + + /// + /// Gets the number of times the network address has changed. + /// + public static int NetworkChangeCount { get; internal set; } = 1; + + /// + public NetCollection RemoteAddressFilter { get; private set; } + + /// + /// Gets a value indicating whether is all IPv6 interfaces are trusted as internal. + /// + public bool TrustAllIP6Interfaces { get; internal set; } + + /// + /// Gets the Published server override list. + /// + public Dictionary PublishedServerOverrides => _overrideUrls; + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + public List GetMacAddresses() + { + // Populated in construction - so always has values. + lock (_intLock) + { + return _macAddresses.ToList(); + } + } + + /// + public bool IsGatewayInterface(object? addressObj) + { + var address = (addressObj is IPAddress addressIP) ? + addressIP : (addressObj is IPObject addressIPObj) ? + addressIPObj.Address : IPAddress.None; + + lock (_intLock) + { + return _internalInterfaces.Where(i => i.Address.Equals(address) && (i.Tag < 0)).Any(); + } + } + + /// + public NetCollection GetLoopbacks() + { + NetCollection nc = new NetCollection(); + if (IsIP4Enabled) + { + nc.Add(IPAddress.Loopback); + } + + if (IsIP6Enabled) + { + nc.Add(IPAddress.IPv6Loopback); + } + + return nc; + } + + /// + public bool IsExcluded(IPAddress ip) + { + return _excludedSubnets.Contains(ip); + } + + /// + public bool IsExcluded(EndPoint ip) + { + if (ip != null) + { + return _excludedSubnets.Contains(((IPEndPoint)ip).Address); + } + + return false; + } + + /// + public NetCollection CreateIPCollection(string[] values, bool bracketed = false) + { + NetCollection col = new NetCollection(); + if (values != null) + { + for (int a = 0; a < values.Length; a++) + { + string v = values[a].Trim(); + + try + { + if (v.StartsWith("[", StringComparison.OrdinalIgnoreCase) && v.EndsWith("]", StringComparison.OrdinalIgnoreCase)) + { + if (bracketed) + { + AddToCollection(col, v.Remove(v.Length - 1).Substring(1)); + } + } + else if (v.StartsWith("!", StringComparison.OrdinalIgnoreCase)) + { + if (bracketed) + { + AddToCollection(col, v.Substring(1)); + } + } + else if (!bracketed) + { + AddToCollection(col, v); + } + } + catch (ArgumentException e) + { + _logger.LogInformation("Ignoring LAN value {value}. Reason : {reason}", v, e.Message); + } + } + } + + return col; + } + + /// + public NetCollection GetAllBindInterfaces() + { + lock (_intLock) + { + int count = _bindAddresses.Count; + + if (count == 0) + { + if (_bindExclusions.Count > 0) + { + // Return all the interfaces except the ones specifically excluded. + return _interfaceAddresses.Exclude(_bindExclusions); + } + + // No bind address and no exclusions, so listen on all interfaces. + NetCollection result = new NetCollection(); + + if (IsIP4Enabled) + { + result.Add(IPAddress.Any); + } + + if (IsIP6Enabled) + { + result.Add(IPAddress.IPv6Any); + } + + return result; + } + + // Remove any excluded bind interfaces. + return _bindAddresses.Exclude(_bindExclusions); + } + } + + /// + public string GetBindInterface(object? source, out int? port) + { + bool chromeCast = false; + port = null; + // Parse the source object in an attempt to discover where the request originated. + IPObject sourceAddr; + if (source is HttpRequest sourceReq) + { + port = sourceReq.Host.Port; + if (IPHost.TryParse(sourceReq.Host.Host, out IPHost host)) + { + sourceAddr = host; + } + else + { + // Assume it's external, as we cannot resolve the host. + sourceAddr = IPHost.None; + } + } + else if (source is string sourceStr && !string.IsNullOrEmpty(sourceStr)) + { + if (string.Equals(sourceStr, "chromecast", StringComparison.OrdinalIgnoreCase)) + { + chromeCast = true; + // Just assign a variable so has source = true; + sourceAddr = IPNetAddress.IP4Loopback; + } + + if (IPHost.TryParse(sourceStr, out IPHost host)) + { + sourceAddr = host; + } + else + { + // Assume it's external, as we cannot resolve the host. + sourceAddr = IPHost.None; + } + } + else if (source is IPAddress sourceIP) + { + sourceAddr = new IPNetAddress(sourceIP); + } + else + { + // If we have no idea, then assume it came from an external address. + sourceAddr = IPHost.None; + } + + // Do we have a source? + bool haveSource = !sourceAddr.Address.Equals(IPAddress.None); + + if (haveSource) + { + if (!IsIP6Enabled && sourceAddr.AddressFamily == AddressFamily.InterNetworkV6) + { + _logger.LogWarning("IPv6 is disabled in JellyFin, but enabled in the OS. This may affect how the interface is selected."); + } + + if (!IsIP4Enabled && sourceAddr.AddressFamily == AddressFamily.InterNetwork) + { + _logger.LogWarning("IPv4 is disabled in JellyFin, but enabled in the OS. This may affect how the interface is selected."); + } + } + + bool isExternal = haveSource && !IsInLocalNetwork(sourceAddr); + + string bindPreference = string.Empty; + if (haveSource) + { + // Check for user override. + foreach (var addr in _overrideUrls) + { + // Remaining. Match anything. + if (addr.Key.Equals(IPAddress.Broadcast)) + { + bindPreference = addr.Value; + break; + } + else if ((addr.Key.Equals(IPAddress.Any) || addr.Key.Equals(IPAddress.IPv6Any)) && (isExternal || chromeCast)) + { + // External. + bindPreference = addr.Value; + break; + } + else if (addr.Key.Contains(sourceAddr)) + { + // Match ip address. + bindPreference = addr.Value; + break; + } + } + } + + _logger.LogDebug("GetBindInterface: Souce: {0}, External: {1}:", haveSource, isExternal); + + if (!string.IsNullOrEmpty(bindPreference)) + { + // Has it got a port defined? + var parts = bindPreference.Split(':'); + if (parts.Length > 1) + { + if (int.TryParse(parts[1], out int p)) + { + bindPreference = parts[0]; + port = p; + } + } + + _logger.LogInformation("{0}: Using BindAddress {1}:{2}", sourceAddr, bindPreference, port); + return bindPreference; + } + + string ipresult; + + // No preference given, so move on to bind addresses. + lock (_intLock) + { + var nc = _bindAddresses.Exclude(_bindExclusions).Where(p => !p.IsLoopback()); + + int count = nc.Count(); + if (count == 1 && (_bindAddresses[0].Equals(IPAddress.Any) || _bindAddresses.Equals(IPAddress.IPv6Any))) + { + // Ignore IPAny addresses. + count = 0; + } + + if (count != 0) + { + // Check to see if any of the bind interfaces are in the same subnet. + + IEnumerable bindResult; + IPAddress? defaultGateway = null; + + if (isExternal) + { + // Find all external bind addresses. Store the default gateway, but check to see if there is a better match first. + bindResult = nc.Where(p => !IsInLocalNetwork(p)).OrderBy(p => p.Tag); + defaultGateway = bindResult.FirstOrDefault()?.Address; + bindResult = bindResult.Where(p => p.Contains(sourceAddr)).OrderBy(p => p.Tag); + } + else + { + // Look for the best internal address. + bindResult = nc.Where(p => IsInLocalNetwork(p) && p.Contains(sourceAddr)).OrderBy(p => p.Tag); + } + + if (bindResult.Any()) + { + ipresult = FormatIP6String(bindResult.First().Address); + _logger.LogDebug("{0}: GetBindInterface: Has source, found a match bind interface subnets. {1}", sourceAddr, ipresult); + return ipresult; + } + + if (isExternal && defaultGateway != null) + { + ipresult = FormatIP6String(defaultGateway); + _logger.LogDebug("{0}: GetBindInterface: Using first user defined external interface. {1}", sourceAddr, ipresult); + return ipresult; + } + + ipresult = FormatIP6String(nc.First().Address); + _logger.LogDebug("{0}: GetBindInterface: Selected first user defined interface. {1}", sourceAddr, ipresult); + + if (isExternal) + { + // TODO: remove this after testing. + _logger.LogWarning("{0}: External request received, however, only an internal interface bind found.", sourceAddr); + } + + return ipresult; + } + + if (isExternal) + { + // Get the first WAN interface address that isn't a loopback. + var extResult = _interfaceAddresses + .Exclude(_bindExclusions) + .Where(p => !IsInLocalNetwork(p)) + .OrderBy(p => p.Tag); + + if (extResult.Any()) + { + // Does the request originate in one of the interface subnets? + // (For systems with multiple internal network cards, and multiple subnets) + foreach (var intf in extResult) + { + if (!IsInLocalNetwork(intf) && intf.Contains(sourceAddr)) + { + ipresult = FormatIP6String(intf.Address); + _logger.LogDebug("{0}: GetBindInterface: Selected best external on interface on range. {1}", sourceAddr, ipresult); + return ipresult; + } + } + + ipresult = FormatIP6String(extResult.First().Address); + _logger.LogDebug("{0}: GetBindInterface: Selected first external interface. {0}", sourceAddr, ipresult); + return ipresult; + } + + // Have to return something, so return an internal address + + // TODO: remove this after testing. + _logger.LogWarning("{0}: External request received, however, no WAN interface found.", sourceAddr); + } + + // Get the first LAN interface address that isn't a loopback. + var result = _interfaceAddresses + .Exclude(_bindExclusions) + .Where(p => IsInLocalNetwork(p)) + .OrderBy(p => p.Tag); + + if (result.Any()) + { + if (haveSource) + { + // Does the request originate in one of the interface subnets? + // (For systems with multiple internal network cards, and multiple subnets) + foreach (var intf in result) + { + if (intf.Contains(sourceAddr)) + { + ipresult = FormatIP6String(intf.Address); + _logger.LogDebug("{0}: GetBindInterface: Has source, matched best internal interface on range. {1}", sourceAddr, ipresult); + return ipresult; + } + } + } + + ipresult = FormatIP6String(result.First().Address); + _logger.LogDebug("{0}: GetBindInterface: Matched first internal interface. {1}", sourceAddr, ipresult); + return ipresult; + } + + // There isn't any others, so we'll use the loopback. + ipresult = IsIP6Enabled ? "::" : "127.0.0.1"; + _logger.LogWarning("{0}: GetBindInterface: Loopback return.", sourceAddr, ipresult); + return ipresult; + } + } + + /// + public NetCollection GetInternalBindAddresses() + { + lock (_intLock) + { + int count = _bindAddresses.Count; + + if (count == 0) + { + if (_bindExclusions.Count > 0) + { + // Return all the internal interfaces except the ones excluded. + return new NetCollection(_internalInterfaces.Where(p => !_bindExclusions.Contains(p))); + } + + // No bind address, so return all internal interfaces. + return new NetCollection(_internalInterfaces.Where(p => !p.IsLoopback())); + } + + return new NetCollection(_bindAddresses.Where(p => !p.IsLoopback())); + } + } + + /// + public bool IsInLocalNetwork(IPObject address) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + if (address.Equals(IPAddress.None)) + { + return false; + } + + // See conversation at https://github.com/jellyfin/jellyfin/pull/3515. + if (TrustAllIP6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6) + { + return true; + } + + lock (_intLock) + { + // As private addresses can be redefined by Configuration.LocalNetworkAddresses + return _lanSubnets.Contains(address) && !_excludedSubnets.Contains(address); + } + } + + /// + public bool IsInLocalNetwork(string address) + { + if (IPHost.TryParse(address, out IPHost ep)) + { + lock (_intLock) + { + return _lanSubnets.Contains(ep) && !_excludedSubnets.Contains(ep); + } + } + + return false; + } + + /// + public bool IsInLocalNetwork(IPAddress address) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + // See conversation at https://github.com/jellyfin/jellyfin/pull/3515. + if (TrustAllIP6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6) + { + return true; + } + + lock (_intLock) + { + // As private addresses can be redefined by Configuration.LocalNetworkAddresses + return _lanSubnets.Contains(address) && !_excludedSubnets.Contains(address); + } + } + + /// + public bool IsPrivateAddressRange(IPObject address) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + // See conversation at https://github.com/jellyfin/jellyfin/pull/3515. + if (TrustAllIP6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6) + { + return true; + } + else + { + return address.IsPrivateAddressRange(); + } + } + + /// + public bool IsExcludedInterface(IPAddress address) + { + lock (_intLock) + { + if (_bindExclusions.Count > 0) + { + return _bindExclusions.Contains(address); + } + + return false; + } + } + + /// + public NetCollection GetFilteredLANSubnets(NetCollection? filter = null) + { + lock (_intLock) + { + if (filter == null) + { + return NetCollection.AsNetworks(_lanSubnets.Exclude(_excludedSubnets)); + } + + return _lanSubnets.Exclude(filter); + } + } + + /// + public bool IsValidInterfaceAddress(IPAddress address) + { + lock (_intLock) + { + return _interfaceAddresses.Contains(address); + } + } + + /// + public bool TryParseInterface(string token, out IPNetAddress result) + { + if (string.IsNullOrEmpty(token)) + { + result = IPNetAddress.None; + return false; + } + + if (_interfaceNames != null && _interfaceNames.TryGetValue(token.ToLower(CultureInfo.InvariantCulture), out int index)) + { + _logger.LogInformation("Interface {0} used in settings. Using its interface addresses.", token); + + // Replace interface tags with the interface IP's. + foreach (IPNetAddress iface in _interfaceAddresses) + { + if (Math.Abs(iface.Tag) == index && + ((IsIP4Enabled && iface.Address.AddressFamily == AddressFamily.InterNetwork) || + (IsIP6Enabled && iface.Address.AddressFamily == AddressFamily.InterNetworkV6))) + { + result = iface; + return true; + } + } + } + + return IPNetAddress.TryParse(token, out result); + } + + /// + /// Reloads all settings and re-initialises the instance. + /// + /// to use. + public void UpdateSettings(ServerConfiguration config) + { + if (config == null) + { + throw new ArgumentNullException(nameof(config)); + } + + IsIP4Enabled = Socket.OSSupportsIPv6 && config.EnableIPV4; + IsIP6Enabled = Socket.OSSupportsIPv6 && config.EnableIPV6; + TrustAllIP6Interfaces = config.TrustAllIP6Interfaces; + EnableMultiSocketBinding = config.EnableMultiSocketBinding; + + InitialiseInterfaces(); + InitialiseLAN(config); + InitialiseBind(config); + InitialiseRemote(config); + InitialiseOverrides(config); + } + + /// + /// Protected implementation of Dispose pattern. + /// + /// True to dispose the managed state. + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _configurationManager.ConfigurationUpdated -= ConfigurationUpdated; + NetworkChange.NetworkAddressChanged -= OnNetworkAddressChanged; + NetworkChange.NetworkAvailabilityChanged -= OnNetworkAvailabilityChanged; + } + + _disposed = true; + } + } + + private static NetworkManager GetInstance() + { + if (_instance == null) + { + throw new ApplicationException("NetworkManager is not initialised."); + } + + return _instance; + } + + private void ConfigurationUpdated(object? sender, EventArgs args) + { + UpdateSettings((ServerConfiguration)_configurationManager.CommonConfiguration); + } + + /// + /// Converts an IPAddress into a string. + /// Ipv6 addresses are returned in [ ], with their scope removed. + /// + /// Address to convert. + /// URI save conversion of the address. + private string FormatIP6String(IPAddress address) + { + var str = address.ToString(); + if (address.AddressFamily == AddressFamily.InterNetworkV6) + { + int i = str.IndexOf("%", StringComparison.OrdinalIgnoreCase); + + if (i != -1) + { + str = str.Substring(0, i); + } + + return $"[{str}]"; + } + + return str; + } + + /// + /// Parses strings into the collection, replacing any interface references. + /// + /// Collection. + /// String to parse. + private void AddToCollection(NetCollection col, string token) + { + // Is it the name of an interface (windows) eg, Wireless LAN adapter Wireless Network Connection 1. + // Null check required here for automated testing. + if (_interfaceNames != null && _interfaceNames.TryGetValue(token.ToLower(CultureInfo.InvariantCulture), out int index)) + { + _logger.LogInformation("Interface {0} used in settings. Using its interface addresses.", token); + + // Replace interface tags with the interface IP's. + foreach (IPNetAddress iface in _interfaceAddresses) + { + if (Math.Abs(iface.Tag) == index && + ((IsIP4Enabled && iface.Address.AddressFamily == AddressFamily.InterNetwork) || + (IsIP6Enabled && iface.Address.AddressFamily == AddressFamily.InterNetworkV6))) + { + col.Add(iface); + } + } + } + else if (NetCollection.TryParse(token, out IPObject obj)) + { + if (!IsIP6Enabled) + { + // Remove IP6 addresses from multi-homed IPHosts. + obj.Remove(AddressFamily.InterNetworkV6); + if (!obj.IsIP6()) + { + col.Add(obj); + } + } + else if (!IsIP4Enabled) + { + // Remove IP4 addresses from multi-homed IPHosts. + obj.Remove(AddressFamily.InterNetwork); + if (obj.IsIP6()) + { + col.Add(obj); + } + } + else + { + col.Add(obj); + } + } + else + { + _logger.LogDebug("Invalid or unknown network {0}.", token); + } + } + + /// + /// Handler for network change events. + /// + /// Sender. + /// Network availablity information. + private void OnNetworkAvailabilityChanged(object? sender, NetworkAvailabilityEventArgs e) + { + _logger.LogDebug("Network availability changed."); + OnNetworkChanged(); + } + + /// + /// Handler for network change events. + /// + /// Sender. + /// Event arguments. + private void OnNetworkAddressChanged(object? sender, EventArgs e) + { + _logger.LogDebug("Network address change detected."); + OnNetworkChanged(); + } + + /// + /// Async task that waits for 2 seconds before re-initialising the settings, as typically these events fire multiple times in succession. + /// + /// The network change async. + private async Task OnNetworkChangeAsync() + { + try + { + await Task.Delay(2000).ConfigureAwait(false); + InitialiseInterfaces(); + // Recalculate LAN caches. + InitialiseLAN((ServerConfiguration)_configurationManager.CommonConfiguration); + + NetworkChanged?.Invoke(this, EventArgs.Empty); + } + finally + { + _eventfire = false; + } + } + + /// + /// Triggers our event, and re-loads interface information. + /// + private void OnNetworkChanged() + { + // As per UPnP Device Architecture v1.0 Annex A - IPv6 Support. + NetworkLocationSignature = Guid.NewGuid().ToString(); + NetworkChangeCount++; + if (NetworkChangeCount > 99) + { + NetworkChangeCount = 1; + } + + if (!_eventfire) + { + _logger.LogDebug("Network Address Change Event."); + // As network events tend to fire one after the other only fire once every second. + _eventfire = true; + _ = OnNetworkChangeAsync(); + } + } + + /// + /// Parses the user defined overrides into the dictionary object. + /// Overrides are the equivalent of localised publishedServerUrl, enabling + /// different addresses to be advertised over different subnets. + /// format is subnet=ipaddress|host|uri + /// when subnet = 0.0.0.0, any external address matches. + /// + private void InitialiseOverrides(ServerConfiguration config) + { + string[] overrides = config.PublishedServerUriBySubnet; + if (overrides == null) + { + lock (_intLock) + { + _overrideUrls.Clear(); + } + + return; + } + + lock (_intLock) + { + _overrideUrls.Clear(); + + foreach (var entry in overrides) + { + var parts = entry.Split('='); + if (parts.Length != 2) + { + _logger.LogError("Unable to parse bind override. {0}", entry); + } + else + { + var replacement = parts[1].Trim(); + if (string.Equals(parts[0], "remaining", StringComparison.OrdinalIgnoreCase)) + { + _overrideUrls[new IPNetAddress(IPAddress.Broadcast)] = replacement; + } + else if (string.Equals(parts[0], "external", StringComparison.OrdinalIgnoreCase)) + { + _overrideUrls[new IPNetAddress(IPAddress.Any)] = replacement; + } + else if (TryParseInterface(parts[0], out IPNetAddress address)) + { + _overrideUrls[address] = replacement; + } + else + { + _logger.LogError("Unable to parse bind ip address. {0}", parts[1]); + } + } + } + } + } + + private void InitialiseBind(ServerConfiguration config) + { + string[] ba = config.LocalNetworkAddresses; + + // TODO: remove when bug fixed: https://github.com/jellyfin/jellyfin-web/issues/1334 + + if (ba.Length == 1 && ba[0].IndexOf(',', StringComparison.OrdinalIgnoreCase) != -1) + { + ba = ba[0].Split(','); + } + + // TODO: end fix. + + // Read and parse bind addresses and exclusions, removing ones that don't exist. + _bindAddresses = CreateIPCollection(ba).Union(_interfaceAddresses); + _bindExclusions = CreateIPCollection(ba, true).Union(_interfaceAddresses); + _logger.LogInformation("Using bind addresses: {0}", _bindAddresses); + _logger.LogInformation("Using bind exclusions: {0}", _bindExclusions); + } + + private void InitialiseRemote(ServerConfiguration config) + { + RemoteAddressFilter = CreateIPCollection(config.RemoteIPFilter); + } + + /// + /// Initialises internal LAN cache settings. + /// + private void InitialiseLAN(ServerConfiguration config) + { + lock (_intLock) + { + _logger.LogDebug("Refreshing LAN information."); + + // Get config options. + string[] subnets = config.LocalNetworkSubnets; + + // Create lists from user settings. + + _lanSubnets = CreateIPCollection(subnets); + _excludedSubnets = NetCollection.AsNetworks(CreateIPCollection(subnets, true)); + + // If no LAN addresses are specified - all private subnets are deemed to be the LAN + _usingPrivateAddresses = _lanSubnets.Count == 0; + + // NOTE: The order of the commands in this statement matters. + if (_usingPrivateAddresses) + { + _logger.LogDebug("Using LAN interface addresses as user provided no LAN details."); + // Internal interfaces must be private and not excluded. + _internalInterfaces = new NetCollection(_interfaceAddresses.Where(i => IsPrivateAddressRange(i) && !_excludedSubnets.Contains(i))); + + // Subnets are the same as the calculated internal interface. + _lanSubnets = new NetCollection(); + + // We must listen on loopback for LiveTV to function regardless of the settings. + if (IsIP6Enabled) + { + _lanSubnets.Add(IPNetAddress.IP6Loopback); + _lanSubnets.Add(IPNetAddress.Parse("fc00::/7")); // ULA + _lanSubnets.Add(IPNetAddress.Parse("fe80::/10")); // Site local + } + + if (IsIP4Enabled) + { + _lanSubnets.Add(IPNetAddress.IP4Loopback); + _lanSubnets.Add(IPNetAddress.Parse("10.0.0.0/8")); + _lanSubnets.Add(IPNetAddress.Parse("172.16.0.0/12")); + _lanSubnets.Add(IPNetAddress.Parse("192.168.0.0/16")); + } + } + else + { + // We must listen on loopback for LiveTV to function regardless of the settings. + if (IsIP6Enabled) + { + _lanSubnets.Add(IPNetAddress.IP6Loopback); + } + + if (IsIP4Enabled) + { + _lanSubnets.Add(IPNetAddress.IP4Loopback); + } + + // Internal interfaces must be private, not excluded and part of the LocalNetworkSubnet. + _internalInterfaces = new NetCollection(_interfaceAddresses.Where(i => IsInLocalNetwork(i) && !_excludedSubnets.Contains(i) && _lanSubnets.Contains(i))); + } + + _logger.LogInformation("Defined LAN addresses : {0}", _lanSubnets); + _logger.LogInformation("Defined LAN exclusions : {0}", _excludedSubnets); + _logger.LogInformation("Using LAN addresses: {0}", NetCollection.AsNetworks(_lanSubnets.Exclude(_excludedSubnets))); + } + } + + /// + /// Generate a list of all the interface ip addresses and submasks where that are in the active/unknown state. + /// Generate a list of all active mac addresses that aren't loopback addreses. + /// + private void InitialiseInterfaces() + { + lock (_intLock) + { + _logger.LogDebug("Refreshing interfaces."); + + _interfaceNames.Clear(); + _interfaceAddresses.Clear(); + + try + { + IEnumerable nics = NetworkInterface.GetAllNetworkInterfaces() + .Where(i => i.SupportsMulticast && i.OperationalStatus == OperationalStatus.Up); + + foreach (NetworkInterface adapter in nics) + { + try + { + IPInterfaceProperties ipProperties = adapter.GetIPProperties(); + PhysicalAddress mac = adapter.GetPhysicalAddress(); + + // populate mac list + if (adapter.NetworkInterfaceType != NetworkInterfaceType.Loopback && mac != null && mac != PhysicalAddress.None) + { + _macAddresses.Add(mac); + } + + // populate interface address list + foreach (UnicastIPAddressInformation info in ipProperties.UnicastAddresses) + { + if (IsIP4Enabled && info.Address.AddressFamily == AddressFamily.InterNetwork) + { + IPNetAddress nw = new IPNetAddress(info.Address, info.IPv4Mask) + { + // Keep the number of gateways on this interface, along with its index. + Tag = ipProperties.GetIPv4Properties().Index + }; + + int tag = nw.Tag; + /* Mono on OSX doesn't give any gateway addresses, so check DNS entries */ + if ((ipProperties.GatewayAddresses.Count > 0 || ipProperties.DnsAddresses.Count > 0) && !nw.IsLoopback()) + { + // -ve Tags signify the interface has a gateway. + nw.Tag *= -1; + } + + _interfaceAddresses.Add(nw); + + // Store interface name so we can use the name in Collections. + _interfaceNames[adapter.Description.ToLower(CultureInfo.InvariantCulture)] = tag; + _interfaceNames["eth" + tag.ToString(CultureInfo.InvariantCulture)] = tag; + } + else if (IsIP6Enabled && info.Address.AddressFamily == AddressFamily.InterNetworkV6) + { + IPNetAddress nw = new IPNetAddress(info.Address, (byte)info.PrefixLength) + { + // Keep the number of gateways on this interface, along with its index. + Tag = ipProperties.GetIPv6Properties().Index + }; + + int tag = nw.Tag; + /* Mono on OSX doesn't give any gateway addresses, so check DNS entries */ + if ((ipProperties.GatewayAddresses.Count > 0 || ipProperties.DnsAddresses.Count > 0) && !nw.IsLoopback()) + { + // -ve Tags signify the interface has a gateway. + nw.Tag *= -1; + } + + _interfaceAddresses.Add(nw); + + // Store interface name so we can use the name in Collections. + _interfaceNames[adapter.Description.ToLower(CultureInfo.InvariantCulture)] = tag; + _interfaceNames["eth" + tag.ToString(CultureInfo.InvariantCulture)] = tag; + } + } + } +#pragma warning disable CA1031 // Do not catch general exception types + catch + { + // Ignore error, and attempt to continue. + } +#pragma warning restore CA1031 // Do not catch general exception types + } + + _logger.LogDebug("Discovered {0} interfaces.", _interfaceAddresses.Count); + _logger.LogDebug("Interfaces addresses : {0}", _interfaceAddresses); + + // If for some reason we don't have an interface info, resolve our DNS name. + if (_interfaceAddresses.Count == 0) + { + _logger.LogWarning("No interfaces information available. Using loopback."); + + IPHost host = new IPHost(Dns.GetHostName()); + foreach (var a in host.GetAddresses()) + { + _interfaceAddresses.Add(a); + } + + if (_interfaceAddresses.Count == 0) + { + _logger.LogError("No interfaces information available. Resolving DNS name."); + // Last ditch attempt - use loopback address. + _interfaceAddresses.Add(IPNetAddress.IP4Loopback); + if (IsIP6Enabled) + { + _interfaceAddresses.Add(IPNetAddress.IP6Loopback); + } + } + } + } + catch (NetworkInformationException ex) + { + _logger.LogError(ex, "Error in InitialiseInterfaces."); + } + } + } + } +} diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index 30ed3e6af3..335a1a9156 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -36,6 +36,7 @@ + diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 8f04baa089..8c19665cc1 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable #pragma warning disable CA1307 using System; @@ -12,10 +12,10 @@ using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Data.Events; using Jellyfin.Data.Events.Users; +using Jellyfin.Networking.Manager; using MediaBrowser.Common; using MediaBrowser.Common.Cryptography; using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Net; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Events; diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index 8d569a779a..566ba0ad85 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -5,6 +5,7 @@ using System.Reflection; using Emby.Drawing; using Emby.Server.Implementations; using Jellyfin.Drawing.Skia; +using Jellyfin.Networking.Manager; using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations.Activity; using Jellyfin.Server.Implementations.Events; @@ -34,21 +35,18 @@ namespace Jellyfin.Server /// The to be used by the . /// The to be used by the . /// The to be used by the . - /// The to be used by the . /// The to be used by the . public CoreAppHost( IServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IStartupOptions options, IFileSystem fileSystem, - INetworkManager networkManager, IServiceCollection collection) : base( applicationPaths, loggerFactory, options, fileSystem, - networkManager, collection) { } diff --git a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs index 4bda8f2737..110290027b 100644 --- a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs +++ b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs @@ -1,9 +1,11 @@ using System.Linq; using System.Threading.Tasks; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Http; +using NetworkCollection; namespace Jellyfin.Server.Middleware { @@ -38,36 +40,35 @@ namespace Jellyfin.Server.Middleware return; } - var remoteIp = httpContext.GetNormalizedRemoteIp(); + var remoteIp = httpContext.Connection.RemoteIpAddress; if (serverConfigurationManager.Configuration.EnableRemoteAccess) { - var addressFilter = serverConfigurationManager.Configuration.RemoteIPFilter.Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(); + // Comma separated list of IP addresses or IP/netmask entries for networks that will be allowed to connect remotely. + // If left blank, all remote addresses will be allowed. + NetCollection remoteAddressFilter = networkManager.RemoteAddressFilter; - if (addressFilter.Length > 0 && !networkManager.IsInLocalNetwork(remoteIp)) + if (remoteAddressFilter.Count > 0 && !networkManager.IsInLocalNetwork(remoteIp)) { - if (serverConfigurationManager.Configuration.IsRemoteIPFilterBlacklist) + // remoteAddressFilter is a whitelist or blacklist. + bool isListed = remoteAddressFilter.Contains(remoteIp); + if (!serverConfigurationManager.Configuration.IsRemoteIPFilterBlacklist) { - if (networkManager.IsAddressInSubnets(remoteIp, addressFilter)) - { - return; - } + // Black list, so flip over. + isListed = !isListed; } - else + + if (!isListed) { - if (!networkManager.IsAddressInSubnets(remoteIp, addressFilter)) - { - return; - } + // If your name isn't on the list, you arn't coming in. + return; } } } - else + else if (!networkManager.IsInLocalNetwork(remoteIp)) { - if (!networkManager.IsInLocalNetwork(remoteIp)) - { - return; - } + // Remote not enabled. So everyone should be LAN. + return; } await _next(httpContext).ConfigureAwait(false); diff --git a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs index 9d795145aa..2ff6f8a765 100644 --- a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs +++ b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Threading.Tasks; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Http; @@ -32,32 +33,13 @@ namespace Jellyfin.Server.Middleware /// The async task. public async Task Invoke(HttpContext httpContext, INetworkManager networkManager, IServerConfigurationManager serverConfigurationManager) { - var currentHost = httpContext.Request.Host.ToString(); - var hosts = serverConfigurationManager - .Configuration - .LocalNetworkAddresses - .Select(NormalizeConfiguredLocalAddress) - .ToList(); + var host = httpContext.Connection.RemoteIpAddress; - if (hosts.Count == 0) + if (!networkManager.IsInLocalNetwork(host) && !serverConfigurationManager.Configuration.EnableRemoteAccess) { - await _next(httpContext).ConfigureAwait(false); return; } - currentHost ??= string.Empty; - - if (networkManager.IsInPrivateAddressSpace(currentHost)) - { - hosts.Add("localhost"); - hosts.Add("127.0.0.1"); - - if (hosts.All(i => currentHost.IndexOf(i, StringComparison.OrdinalIgnoreCase) == -1)) - { - return; - } - } - await _next(httpContext).ConfigureAwait(false); } diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index c933d679f4..8549e39dbc 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -12,8 +12,8 @@ using System.Threading.Tasks; using CommandLine; using Emby.Server.Implementations; using Emby.Server.Implementations.IO; -using Emby.Server.Implementations.Networking; using Jellyfin.Api.Controllers; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Extensions; using Microsoft.AspNetCore.Hosting; @@ -24,6 +24,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using NetworkCollection; using Serilog; using Serilog.Extensions.Logging; using SQLitePCL; @@ -161,7 +162,6 @@ namespace Jellyfin.Server _loggerFactory, options, new ManagedFileSystem(_loggerFactory.CreateLogger(), appPaths), - new NetworkManager(_loggerFactory.CreateLogger()), serviceCollection); try @@ -272,57 +272,16 @@ namespace Jellyfin.Server return builder .UseKestrel((builderContext, options) => { - var addresses = appHost.ServerConfigurationManager - .Configuration - .LocalNetworkAddresses - .Select(x => appHost.NormalizeConfiguredLocalAddress(x)) - .Where(i => i != null) - .ToHashSet(); - if (addresses.Count > 0 && !addresses.Contains(IPAddress.Any)) - { - if (!addresses.Contains(IPAddress.Loopback)) - { - // we must listen on loopback for LiveTV to function regardless of the settings - addresses.Add(IPAddress.Loopback); - } + NetCollection addresses = NetworkManager.Instance.GetAllBindInterfaces(); - foreach (var address in addresses) - { - _logger.LogInformation("Kestrel listening on {IpAddress}", address); - options.Listen(address, appHost.HttpPort); - if (appHost.ListenWithHttps) - { - options.Listen(address, appHost.HttpsPort, listenOptions => - { - listenOptions.UseHttps(appHost.Certificate); - listenOptions.Protocols = HttpProtocols.Http1AndHttp2; - }); - } - else if (builderContext.HostingEnvironment.IsDevelopment()) - { - try - { - options.Listen(address, appHost.HttpsPort, listenOptions => - { - listenOptions.UseHttps(); - listenOptions.Protocols = HttpProtocols.Http1AndHttp2; - }); - } - catch (InvalidOperationException ex) - { - _logger.LogError(ex, "Failed to listen to HTTPS using the ASP.NET Core HTTPS development certificate. Please ensure it has been installed and set as trusted."); - } - } - } - } - else + bool flagged = false; + foreach (IPObject netAdd in addresses) { - _logger.LogInformation("Kestrel listening on all interfaces"); - options.ListenAnyIP(appHost.HttpPort); - + _logger.LogInformation("Kestrel listening on {0}", netAdd); + options.Listen(netAdd.Address, appHost.HttpPort); if (appHost.ListenWithHttps) { - options.ListenAnyIP(appHost.HttpsPort, listenOptions => + options.Listen(netAdd.Address, appHost.HttpsPort, listenOptions => { listenOptions.UseHttps(appHost.Certificate); listenOptions.Protocols = HttpProtocols.Http1AndHttp2; @@ -332,15 +291,19 @@ namespace Jellyfin.Server { try { - options.ListenAnyIP(appHost.HttpsPort, listenOptions => + options.Listen(netAdd.Address, appHost.HttpsPort, listenOptions => { listenOptions.UseHttps(); listenOptions.Protocols = HttpProtocols.Http1AndHttp2; }); } - catch (InvalidOperationException ex) + catch (InvalidOperationException) { - _logger.LogError(ex, "Failed to listen to HTTPS using the ASP.NET Core HTTPS development certificate. Please ensure it has been installed and set as trusted."); + if (!flagged) + { + _logger.LogWarning("Failed to listen to HTTPS using the ASP.NET Core HTTPS development certificate. Please ensure it has been installed and set as trusted."); + flagged = true; + } } } } diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs deleted file mode 100644 index a0330afeff..0000000000 --- a/MediaBrowser.Common/Net/INetworkManager.cs +++ /dev/null @@ -1,97 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.NetworkInformation; - -namespace MediaBrowser.Common.Net -{ - public interface INetworkManager - { - event EventHandler NetworkChanged; - - /// - /// Gets or sets a function to return the list of user defined LAN addresses. - /// - Func LocalSubnetsFn { get; set; } - - /// - /// Gets a random port TCP number that is currently available. - /// - /// System.Int32. - int GetRandomUnusedTcpPort(); - - /// - /// Gets a random port UDP number that is currently available. - /// - /// System.Int32. - int GetRandomUnusedUdpPort(); - - /// - /// Returns the MAC Address from first Network Card in Computer. - /// - /// The MAC Address. - List GetMacAddresses(); - - /// - /// Determines whether [is in private address space] [the specified endpoint]. - /// - /// The endpoint. - /// true if [is in private address space] [the specified endpoint]; otherwise, false. - bool IsInPrivateAddressSpace(string endpoint); - - /// - /// Determines whether [is in private address space 10.x.x.x] [the specified endpoint] and exists in the subnets returned by GetSubnets(). - /// - /// The endpoint. - /// true if [is in private address space 10.x.x.x] [the specified endpoint]; otherwise, false. - bool IsInPrivateAddressSpaceAndLocalSubnet(string endpoint); - - /// - /// Determines whether [is in local network] [the specified endpoint]. - /// - /// The endpoint. - /// true if [is in local network] [the specified endpoint]; otherwise, false. - bool IsInLocalNetwork(string endpoint); - - /// - /// Investigates an caches a list of interface addresses, excluding local link and LAN excluded addresses. - /// - /// The list of ipaddresses. - IPAddress[] GetLocalIpAddresses(); - - /// - /// Checks if the given address falls within the ranges given in [subnets]. The addresses in subnets can be hosts or subnets in the CIDR format. - /// - /// The address to check. - /// If true, check against addresses in the LAN settings surrounded by brackets ([]). - /// trueif the address is in at least one of the given subnets, false otherwise. - bool IsAddressInSubnets(string addressString, string[] subnets); - - /// - /// Returns true if address is in the LAN list in the config file. - /// - /// The address to check. - /// If true, check against addresses in the LAN settings which have [] arroud and return true if it matches the address give in address. - /// If true, returns false if address is in the 127.x.x.x or 169.128.x.x range. - /// falseif the address isn't in the LAN list, true if the address has been defined as a LAN address. - bool IsAddressInSubnets(IPAddress address, bool excludeInterfaces, bool excludeRFC); - - /// - /// Checks if address is in the LAN list in the config file. - /// - /// Source address to check. - /// Destination address to check against. - /// Destination subnet to check against. - /// true/falsedepending on whether address1 is in the same subnet as IPAddress2 with subnetMask. - bool IsInSameSubnet(IPAddress address1, IPAddress address2, IPAddress subnetMask); - - /// - /// Returns the subnet mask of an interface with the given address. - /// - /// The address to check. - /// Returns the subnet mask of an interface with the given address, or null if an interface match cannot be found. - IPAddress GetLocalIpSubnetMask(IPAddress address); - } -} diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index cfad17fb72..f147e6a86c 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -56,41 +56,27 @@ namespace MediaBrowser.Controller /// /// Gets the system info. /// + /// The originator of the request. /// SystemInfo. - Task GetSystemInfo(CancellationToken cancellationToken); + SystemInfo GetSystemInfo(IPAddress source); - Task GetPublicSystemInfo(CancellationToken cancellationToken); - - /// - /// Gets all the local IP addresses of this API instance. Each address is validated by sending a 'ping' request - /// to the API that should exist at the address. - /// - /// A cancellation token that can be used to cancel the task. - /// A list containing all the local IP addresses of the server. - Task> GetLocalIpAddresses(CancellationToken cancellationToken); + PublicSystemInfo GetPublicSystemInfo(IPAddress address); /// /// Gets a local (LAN) URL that can be used to access the API. The hostname used is the first valid configured - /// IP address that can be found via . HTTPS will be preferred when available. + /// HTTPS will be preferred when available. /// - /// A cancellation token that can be used to cancel the task. + /// The source of the request. /// The server URL. - Task GetLocalApiUrl(CancellationToken cancellationToken); + string GetSmartApiUrl(object source); /// - /// Gets a localhost URL that can be used to access the API using the loop-back IP address (127.0.0.1) + /// Gets a localhost URL that can be used to access the API using the loop-back IP address. /// over HTTP (not HTTPS). /// /// The API URL. string GetLoopbackHttpApiUrl(); - /// - /// Gets a local (LAN) URL that can be used to access the API. HTTPS will be preferred when available. - /// - /// The IP address to use as the hostname in the URL. - /// The API URL. - string GetLocalApiUrl(IPAddress address); - /// /// Gets a local (LAN) URL that can be used to access the API. /// Note: if passing non-null scheme or port it is up to the caller to ensure they form the correct pair. @@ -105,7 +91,7 @@ namespace MediaBrowser.Controller /// preferring the HTTPS port, if available. /// /// The API URL. - string GetLocalApiUrl(ReadOnlySpan hostname, string scheme = null, int? port = null); + string GetLocalApiUrl(string hostname, string scheme = null, int? port = null); /// /// Open a URL in an external browser window. diff --git a/MediaBrowser.Model/Configuration/PathSubstitution.cs b/MediaBrowser.Model/Configuration/PathSubstitution.cs new file mode 100644 index 0000000000..40eb36c2ea --- /dev/null +++ b/MediaBrowser.Model/Configuration/PathSubstitution.cs @@ -0,0 +1,19 @@ +#nullable enable +#pragma warning disable CS1591 +#pragma warning disable CA1819 + +using System; +using System.Collections.Generic; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Updates; + +namespace MediaBrowser.Model.Configuration +{ + + public class PathSubstitution + { + public string From { get; set; } = string.Empty; + + public string To { get; set; } = string.Empty; + } +} diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 48d1a7346a..073a629829 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -1,5 +1,6 @@ #nullable disable #pragma warning disable CS1591 +#pragma warning disable CA1819 using System; using System.Collections.Generic; @@ -15,41 +16,174 @@ namespace MediaBrowser.Model.Configuration { public const int DefaultHttpPort = 8096; public const int DefaultHttpsPort = 8920; - private string _baseUrl; + private string _baseUrl = string.Empty; + + /// + /// Initializes a new instance of the class. + /// + public ServerConfiguration() + { + MetadataOptions = new[] + { + new MetadataOptions() + { + ItemType = "Book" + }, + new MetadataOptions() + { + ItemType = "Movie" + }, + new MetadataOptions + { + ItemType = "MusicVideo", + DisabledMetadataFetchers = new[] { "The Open Movie Database" }, + DisabledImageFetchers = new[] { "The Open Movie Database" } + }, + new MetadataOptions + { + ItemType = "Series", + DisabledMetadataFetchers = new[] { "TheMovieDb" }, + DisabledImageFetchers = new[] { "TheMovieDb" } + }, + new MetadataOptions + { + ItemType = "MusicAlbum", + DisabledMetadataFetchers = new[] { "TheAudioDB" } + }, + new MetadataOptions + { + ItemType = "MusicArtist", + DisabledMetadataFetchers = new[] { "TheAudioDB" } + }, + new MetadataOptions + { + ItemType = "BoxSet" + }, + new MetadataOptions + { + ItemType = "Season", + DisabledMetadataFetchers = new[] { "TheMovieDb" }, + }, + new MetadataOptions + { + ItemType = "Episode", + DisabledMetadataFetchers = new[] { "The Open Movie Database", "TheMovieDb" }, + DisabledImageFetchers = new[] { "The Open Movie Database", "TheMovieDb" } + } + }; + } /// /// Gets or sets a value indicating whether to enable automatic port forwarding. /// - public bool EnableUPnP { get; set; } + public bool EnableUPnP { get; set; } = false; /// /// Gets or sets a value indicating whether to enable prometheus metrics exporting. /// - public bool EnableMetrics { get; set; } + public bool EnableMetrics { get; set; } = false; /// /// Gets or sets the public mapped port. /// /// The public mapped port. - public int PublicPort { get; set; } + public int PublicPort { get; set; } = DefaultHttpPort; + + /// + /// Gets or sets a value indicating whether the http port should be mapped as part of UPnP automatic port forwarding. + /// + public bool UPnPCreateHttpPortMap { get; set; } = false; + + /// + /// Gets or sets client udp port range. + /// + public string UDPPortRange { get; set; } = string.Empty; + + /// + /// Gets or sets a value indicating whether gets or sets IPV6 capability. + /// + public bool EnableIPV6 { get; set; } = false; + + /// + /// Gets or sets a value indicating whether gets or sets IPV4 capability. + /// + public bool EnableIPV4 { get; set; } = true; + + /// + /// Gets or sets a value indicating whether detailed ssdp logs are sent to the console/log. + /// If the setting "Emby.Dlna": "Debug" msut be set in logging.default.json for this property to work. + /// + public bool EnableSSDPTracing { get; set; } = false; + + /// + /// Gets or sets a value indicating whether an IP address is to be used to filter the detailed ssdp logs that are being sent to the console/log. + /// If the setting "Emby.Dlna": "Debug" msut be set in logging.default.json for this property to work. + /// + public string SSDPTracingFilter { get; set; } = string.Empty; + + /// + /// Gets or sets the number of times SSDP UDP messages are sent. + /// + public int UDPSendCount { get; set; } = 2; + + /// + /// Gets or sets the delay between each groups of SSDP messages (in ms). + /// + public int UDPSendDelay { get; set; } = 100; + + /// + /// Gets or sets the time (in seconds) between the pings of SSDP gateway monitor. + /// + public int GatewayMonitorPeriod { get; set; } = 60; + + /// + /// Gets a value indicating whether is multi-socket binding available. + /// + public bool EnableMultiSocketBinding { get; } = true; + + /// + /// Gets or sets a value indicating whether all IPv6 interfaces should be treated as on the internal network. + /// Depending on the address range implemented ULA ranges might not be used. + /// + public bool TrustAllIP6Interfaces { get; set; } = false; + + /// + /// Gets or sets the ports that HDHomerun uses. + /// + public string HDHomerunPortRange { get; set; } = string.Empty; + + /// + /// Gets or sets PublishedServerUri to advertise for specific subnets. + /// + public string[] PublishedServerUriBySubnet { get; set; } = Array.Empty(); + + /// + /// Gets or sets a value indicating whether gets or sets Autodiscovery tracing. + /// + public bool AutoDiscoveryTracing { get; set; } = false; + + /// + /// Gets or sets a value indicating whether Autodiscovery is enabled. + /// + public bool AutoDiscovery { get; set; } = true; /// /// Gets or sets the public HTTPS port. /// /// The public HTTPS port. - public int PublicHttpsPort { get; set; } + public int PublicHttpsPort { get; set; } = DefaultHttpsPort; /// /// Gets or sets the HTTP server port number. /// /// The HTTP server port number. - public int HttpServerPortNumber { get; set; } + public int HttpServerPortNumber { get; set; } = DefaultHttpPort; /// /// Gets or sets the HTTPS server port number. /// /// The HTTPS server port number. - public int HttpsPortNumber { get; set; } + public int HttpsPortNumber { get; set; } = DefaultHttpsPort; /// /// Gets or sets a value indicating whether to use HTTPS. @@ -58,19 +192,19 @@ namespace MediaBrowser.Model.Configuration /// In order for HTTPS to be used, in addition to setting this to true, valid values must also be /// provided for and . /// - public bool EnableHttps { get; set; } + public bool EnableHttps { get; set; } = false; - public bool EnableNormalizedItemByNameIds { get; set; } + public bool EnableNormalizedItemByNameIds { get; set; } = true; /// /// Gets or sets the filesystem path of an X.509 certificate to use for SSL. /// - public string CertificatePath { get; set; } + public string CertificatePath { get; set; } = string.Empty; /// /// Gets or sets the password required to access the X.509 certificate data in the file specified by . /// - public string CertificatePassword { get; set; } + public string CertificatePassword { get; set; } = string.Empty; /// /// Gets or sets a value indicating whether this instance is port authorized. @@ -79,92 +213,95 @@ namespace MediaBrowser.Model.Configuration public bool IsPortAuthorized { get; set; } /// - /// Gets or sets if quick connect is available for use on this server. + /// Gets or sets a value indicating whether quick connect is available for use on this server. /// - public bool QuickConnectAvailable { get; set; } + public bool QuickConnectAvailable { get; set; } = false; - public bool AutoRunWebApp { get; set; } + public bool AutoRunWebApp { get; set; } = true; - public bool EnableRemoteAccess { get; set; } + /// + /// Gets or sets a value indicating whether access outside of the LAN is permitted. + /// + public bool EnableRemoteAccess { get; set; } = true; /// /// Gets or sets a value indicating whether [enable case sensitive item ids]. /// /// true if [enable case sensitive item ids]; otherwise, false. - public bool EnableCaseSensitiveItemIds { get; set; } + public bool EnableCaseSensitiveItemIds { get; set; } = true; - public bool DisableLiveTvChannelUserDataName { get; set; } + public bool DisableLiveTvChannelUserDataName { get; set; } = true; /// /// Gets or sets the metadata path. /// /// The metadata path. - public string MetadataPath { get; set; } + public string MetadataPath { get; set; } = string.Empty; - public string MetadataNetworkPath { get; set; } + public string MetadataNetworkPath { get; set; } = string.Empty; /// /// Gets or sets the preferred metadata language. /// /// The preferred metadata language. - public string PreferredMetadataLanguage { get; set; } + public string PreferredMetadataLanguage { get; set; } = string.Empty; /// /// Gets or sets the metadata country code. /// /// The metadata country code. - public string MetadataCountryCode { get; set; } + public string MetadataCountryCode { get; set; } = "US"; /// - /// Characters to be replaced with a ' ' in strings to create a sort name. + /// Gets or sets characters to be replaced with a ' ' in strings to create a sort name. /// /// The sort replace characters. - public string[] SortReplaceCharacters { get; set; } + public string[] SortReplaceCharacters { get; set; } = new[] { ".", "+", "%" }; /// - /// Characters to be removed from strings to create a sort name. + /// Gets or sets characters to be removed from strings to create a sort name. /// /// The sort remove characters. - public string[] SortRemoveCharacters { get; set; } + public string[] SortRemoveCharacters { get; set; } = new[] { ",", "&", "-", "{", "}", "'" }; /// - /// Words to be removed from strings to create a sort name. + /// Gets or sets words to be removed from strings to create a sort name. /// /// The sort remove words. - public string[] SortRemoveWords { get; set; } + public string[] SortRemoveWords { get; set; } = new[] { "the", "a", "an" }; /// /// Gets or sets the minimum percentage of an item that must be played in order for playstate to be updated. /// /// The min resume PCT. - public int MinResumePct { get; set; } + public int MinResumePct { get; set; } = 5; /// /// Gets or sets the maximum percentage of an item that can be played while still saving playstate. If this percentage is crossed playstate will be reset to the beginning and the item will be marked watched. /// /// The max resume PCT. - public int MaxResumePct { get; set; } + public int MaxResumePct { get; set; } = 90; /// /// Gets or sets the minimum duration that an item must have in order to be eligible for playstate updates.. /// /// The min resume duration seconds. - public int MinResumeDurationSeconds { get; set; } + public int MinResumeDurationSeconds { get; set; } = 300; /// - /// The delay in seconds that we will wait after a file system change to try and discover what has been added/removed + /// Gets or sets the delay in seconds that we will wait after a file system change to try and discover what has been added/removed /// Some delay is necessary with some items because their creation is not atomic. It involves the creation of several /// different directories and files. /// /// The file watcher delay. - public int LibraryMonitorDelay { get; set; } + public int LibraryMonitorDelay { get; set; } = 60; /// /// Gets or sets a value indicating whether [enable dashboard response caching]. /// Allows potential contributors without visual studio to modify production dashboard code and test changes. /// /// true if [enable dashboard response caching]; otherwise, false. - public bool EnableDashboardResponseCaching { get; set; } + public bool EnableDashboardResponseCaching { get; set; } = true; /// /// Gets or sets the image saving convention. @@ -174,9 +311,9 @@ namespace MediaBrowser.Model.Configuration public MetadataOptions[] MetadataOptions { get; set; } - public bool SkipDeserializationForBasicTypes { get; set; } + public bool SkipDeserializationForBasicTypes { get; set; } = true; - public string ServerName { get; set; } + public string ServerName { get; set; } = string.Empty; public string BaseUrl { @@ -208,189 +345,80 @@ namespace MediaBrowser.Model.Configuration } } - public string UICulture { get; set; } - - public bool SaveMetadataHidden { get; set; } + public string UICulture { get; set; } = "en-US"; - public NameValuePair[] ContentTypes { get; set; } + public bool SaveMetadataHidden { get; set; } = false; - public int RemoteClientBitrateLimit { get; set; } + public NameValuePair[] ContentTypes { get; set; } = Array.Empty(); - public bool EnableFolderView { get; set; } + public int RemoteClientBitrateLimit { get; set; } = 0; - public bool EnableGroupingIntoCollections { get; set; } + public bool EnableFolderView { get; set; } = false; - public bool DisplaySpecialsWithinSeasons { get; set; } + public bool EnableGroupingIntoCollections { get; set; } = false; - public string[] LocalNetworkSubnets { get; set; } + public bool DisplaySpecialsWithinSeasons { get; set; } = true; - public string[] LocalNetworkAddresses { get; set; } + /// + /// Gets or sets the subnets that are deemed to make up the LAN. + /// + public string[] LocalNetworkSubnets { get; set; } = Array.Empty(); - public string[] CodecsUsed { get; set; } + /// + /// Gets or sets the interface addresses which Jellyfin will bind to. If empty, all interfaces will be used. + /// + public string[] LocalNetworkAddresses { get; set; } = Array.Empty(); - public List PluginRepositories { get; set; } + public string[] CodecsUsed { get; set; } = Array.Empty(); - public bool IgnoreVirtualInterfaces { get; set; } + public List PluginRepositories { get; set; } = new List(); - public bool EnableExternalContentInSuggestions { get; set; } + public bool EnableExternalContentInSuggestions { get; set; } = true; /// /// Gets or sets a value indicating whether the server should force connections over HTTPS. /// - public bool RequireHttps { get; set; } + public bool RequireHttps { get; set; } = false; - public bool EnableNewOmdbSupport { get; set; } + public bool EnableNewOmdbSupport { get; set; } = true; - public string[] RemoteIPFilter { get; set; } + /// + /// Gets or sets the filter for remote IP connectivity. Used in conjuntion with . + /// + public string[] RemoteIPFilter { get; set; } = Array.Empty(); - public bool IsRemoteIPFilterBlacklist { get; set; } + /// + /// Gets or sets a value indicating whether contains a blacklist or a whitelist. Default is a whitelist. + /// + public bool IsRemoteIPFilterBlacklist { get; set; } = false; - public int ImageExtractionTimeoutMs { get; set; } + public int ImageExtractionTimeoutMs { get; set; } = 0; - public PathSubstitution[] PathSubstitutions { get; set; } + public PathSubstitution[] PathSubstitutions { get; set; } = Array.Empty(); - public bool EnableSimpleArtistDetection { get; set; } + public bool EnableSimpleArtistDetection { get; set; } = false; - public string[] UninstalledPlugins { get; set; } + public string[] UninstalledPlugins { get; set; } = Array.Empty(); /// /// Gets or sets a value indicating whether slow server responses should be logged as a warning. /// - public bool EnableSlowResponseWarning { get; set; } + public bool EnableSlowResponseWarning { get; set; } = true; /// /// Gets or sets the threshold for the slow response time warning in ms. /// - public long SlowResponseThresholdMs { get; set; } + public long SlowResponseThresholdMs { get; set; } = 500; /// /// Gets or sets the cors hosts. /// - public string[] CorsHosts { get; set; } + public string[] CorsHosts { get; set; } = new[] { "*" }; /// /// Gets or sets the known proxies. /// - public string[] KnownProxies { get; set; } - - /// - /// Initializes a new instance of the class. - /// - public ServerConfiguration() - { - UninstalledPlugins = Array.Empty(); - RemoteIPFilter = Array.Empty(); - LocalNetworkSubnets = Array.Empty(); - LocalNetworkAddresses = Array.Empty(); - CodecsUsed = Array.Empty(); - PathSubstitutions = Array.Empty(); - IgnoreVirtualInterfaces = false; - EnableSimpleArtistDetection = false; - SkipDeserializationForBasicTypes = true; - - PluginRepositories = new List(); - - DisplaySpecialsWithinSeasons = true; - EnableExternalContentInSuggestions = true; - - ImageSavingConvention = ImageSavingConvention.Compatible; - PublicPort = DefaultHttpPort; - PublicHttpsPort = DefaultHttpsPort; - HttpServerPortNumber = DefaultHttpPort; - HttpsPortNumber = DefaultHttpsPort; - EnableMetrics = false; - EnableHttps = false; - EnableDashboardResponseCaching = true; - EnableCaseSensitiveItemIds = true; - EnableNormalizedItemByNameIds = true; - DisableLiveTvChannelUserDataName = true; - EnableNewOmdbSupport = true; - - AutoRunWebApp = true; - EnableRemoteAccess = true; - QuickConnectAvailable = false; - - EnableUPnP = false; - MinResumePct = 5; - MaxResumePct = 90; - - // 5 minutes - MinResumeDurationSeconds = 300; - - LibraryMonitorDelay = 60; - - ContentTypes = Array.Empty(); - - PreferredMetadataLanguage = "en"; - MetadataCountryCode = "US"; - - SortReplaceCharacters = new[] { ".", "+", "%" }; - SortRemoveCharacters = new[] { ",", "&", "-", "{", "}", "'" }; - SortRemoveWords = new[] { "the", "a", "an" }; - - BaseUrl = string.Empty; - UICulture = "en-US"; - - MetadataOptions = new[] - { - new MetadataOptions() - { - ItemType = "Book" - }, - new MetadataOptions() - { - ItemType = "Movie" - }, - new MetadataOptions - { - ItemType = "MusicVideo", - DisabledMetadataFetchers = new[] { "The Open Movie Database" }, - DisabledImageFetchers = new[] { "The Open Movie Database" } - }, - new MetadataOptions - { - ItemType = "Series", - DisabledMetadataFetchers = new[] { "TheMovieDb" }, - DisabledImageFetchers = new[] { "TheMovieDb" } - }, - new MetadataOptions - { - ItemType = "MusicAlbum", - DisabledMetadataFetchers = new[] { "TheAudioDB" } - }, - new MetadataOptions - { - ItemType = "MusicArtist", - DisabledMetadataFetchers = new[] { "TheAudioDB" } - }, - new MetadataOptions - { - ItemType = "BoxSet" - }, - new MetadataOptions - { - ItemType = "Season", - DisabledMetadataFetchers = new[] { "TheMovieDb" }, - }, - new MetadataOptions - { - ItemType = "Episode", - DisabledMetadataFetchers = new[] { "The Open Movie Database", "TheMovieDb" }, - DisabledImageFetchers = new[] { "The Open Movie Database", "TheMovieDb" } - } - }; - - EnableSlowResponseWarning = true; - SlowResponseThresholdMs = 500; - CorsHosts = new[] { "*" }; - KnownProxies = Array.Empty(); - } - } - - public class PathSubstitution - { - public string From { get; set; } + public string[] KnownProxies { get; set; } = Array.Empty(); - public string To { get; set; } } } diff --git a/MediaBrowser.sln b/MediaBrowser.sln index 25402aee13..d460c0ab0c 100644 --- a/MediaBrowser.sln +++ b/MediaBrowser.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26730.3 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30503.244 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server", "Jellyfin.Server\Jellyfin.Server.csproj", "{07E39F42-A2C6-4B32-AF8C-725F957A73FF}" EndProject @@ -66,12 +66,18 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Data", "Jellyfin.D EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server.Implementations", "Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj", "{DAE48069-6D86-4BA6-B148-D1D49B6DDA52}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Networking", "Jellyfin.Networking\Jellyfin.Networking.csproj", "{0A3FCC4D-C714-4072-B90F-E374A15F9FF9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Release|Any CPU.Build.0 = Release|Any CPU {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Debug|Any CPU.Build.0 = Debug|Any CPU {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -132,10 +138,6 @@ Global {960295EE-4AF4-4440-A525-B4C295B01A61}.Debug|Any CPU.Build.0 = Debug|Any CPU {960295EE-4AF4-4440-A525-B4C295B01A61}.Release|Any CPU.ActiveCfg = Release|Any CPU {960295EE-4AF4-4440-A525-B4C295B01A61}.Release|Any CPU.Build.0 = Release|Any CPU - {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Release|Any CPU.Build.0 = Release|Any CPU {154872D9-6C12-4007-96E3-8F70A58386CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {154872D9-6C12-4007-96E3-8F70A58386CE}.Debug|Any CPU.Build.0 = Debug|Any CPU {154872D9-6C12-4007-96E3-8F70A58386CE}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -176,10 +178,22 @@ Global {DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Debug|Any CPU.Build.0 = Debug|Any CPU {DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Release|Any CPU.ActiveCfg = Release|Any CPU {DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Release|Any CPU.Build.0 = Release|Any CPU + {0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {DF194677-DFD3-42AF-9F75-D44D5A416478} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + {28464062-0939-4AA7-9F7B-24DDDA61A7C0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + {3998657B-1CCC-49DD-A19F-275DC8495F57} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + {A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + {2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + {462584F7-5023-4019-9EAC-B98CA458C0A0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE} EndGlobalSection @@ -201,12 +215,4 @@ Global $0.DotNetNamingPolicy = $2 $2.DirectoryNamespaceAssociation = PrefixedHierarchical EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {DF194677-DFD3-42AF-9F75-D44D5A416478} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} - {28464062-0939-4AA7-9F7B-24DDDA61A7C0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} - {3998657B-1CCC-49DD-A19F-275DC8495F57} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} - {A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} - {2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} - {462584F7-5023-4019-9EAC-B98CA458C0A0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} - EndGlobalSection EndGlobal diff --git a/RSSDP/RSSDP.csproj b/RSSDP/RSSDP.csproj index 664663bd76..3266a05cbf 100644 --- a/RSSDP/RSSDP.csproj +++ b/RSSDP/RSSDP.csproj @@ -6,6 +6,7 @@ + diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs index a4be32e7d2..e28a2f48d4 100644 --- a/RSSDP/SsdpCommunicationsServer.cs +++ b/RSSDP/SsdpCommunicationsServer.cs @@ -7,6 +7,7 @@ using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Net; using MediaBrowser.Model.Net; using Microsoft.Extensions.Logging; @@ -352,7 +353,7 @@ namespace Rssdp.Infrastructure if (_enableMultiSocketBinding) { - foreach (var address in _networkManager.GetLocalIpAddresses()) + foreach (var address in _networkManager.GetAllBindInterfaces()) { if (address.AddressFamily == AddressFamily.InterNetworkV6) { @@ -362,7 +363,7 @@ namespace Rssdp.Infrastructure try { - sockets.Add(_SocketFactory.CreateSsdpUdpSocket(address, _LocalPort)); + sockets.Add(_SocketFactory.CreateSsdpUdpSocket(address.Address, _LocalPort)); } catch (Exception ex) { diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs index 1a8577d8da..43fccdad42 100644 --- a/RSSDP/SsdpDevicePublisher.cs +++ b/RSSDP/SsdpDevicePublisher.cs @@ -5,7 +5,9 @@ using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Net; +using NetworkCollection; namespace Rssdp.Infrastructure { @@ -300,17 +302,14 @@ namespace Rssdp.Infrastructure foreach (var device in deviceList) { - if (!_sendOnlyMatchedHost || - _networkManager.IsInSameSubnet(device.ToRootDevice().Address, remoteEndPoint.Address, device.ToRootDevice().SubnetMask)) + var ip1 = new IPNetAddress(device.ToRootDevice().Address, device.ToRootDevice().SubnetMask); + var ip2 = new IPNetAddress(remoteEndPoint.Address, device.ToRootDevice().SubnetMask); + if (!_sendOnlyMatchedHost || ip1.NetworkAddress.Equals(ip2.NetworkAddress)) { SendDeviceSearchResponses(device, remoteEndPoint, receivedOnlocalIpAddress, cancellationToken); } } } - else - { - // WriteTrace(String.Format("Sending 0 search responses.")); - } }); } diff --git a/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs index 09ffa84689..2c7f0c4f9b 100644 --- a/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs @@ -1,9 +1,10 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading.Tasks; using AutoFixture; using AutoFixture.AutoMoq; using Jellyfin.Api.Auth.LocalAccessPolicy; using Jellyfin.Api.Constants; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; diff --git a/tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs b/tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs index 77f1640fa3..939023a95b 100644 --- a/tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs +++ b/tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs @@ -3,8 +3,6 @@ using System.Collections.Concurrent; using System.IO; using Emby.Server.Implementations; using Emby.Server.Implementations.IO; -using Emby.Server.Implementations.Networking; -using Jellyfin.Drawing.Skia; using Jellyfin.Server; using MediaBrowser.Common; using Microsoft.AspNetCore.Hosting; @@ -81,7 +79,6 @@ namespace Jellyfin.Api.Tests loggerFactory, commandLineOpts, new ManagedFileSystem(loggerFactory.CreateLogger(), appPaths), - new NetworkManager(loggerFactory.CreateLogger()), serviceCollection); _disposableComponents.Add(appHost); appHost.Init(); -- cgit v1.2.3 From 5cca8bffea339f1043d692f337ab002dbee3a03b Mon Sep 17 00:00:00 2001 From: spookbits <71300703+spooksbit@users.noreply.github.com> Date: Wed, 16 Sep 2020 13:17:14 -0400 Subject: Removed browser auto-load functionality from the server. Added profiles in launchSettings to start either the web client or the swagger API page. Removed --noautorunwebapp as this is the default functionality. --- CONTRIBUTORS.md | 1 + .../Browser/BrowserLauncher.cs | 51 ------------- .../EntryPoints/StartupWizard.cs | 83 ---------------------- Emby.Server.Implementations/IStartupOptions.cs | 5 -- Jellyfin.Server/Jellyfin.Server.csproj | 2 +- Jellyfin.Server/Properties/launchSettings.json | 8 ++- Jellyfin.Server/StartupOptions.cs | 4 -- .../Configuration/ServerConfiguration.cs | 3 - .../JellyfinApplicationFactory.cs | 3 +- 9 files changed, 10 insertions(+), 150 deletions(-) delete mode 100644 Emby.Server.Implementations/Browser/BrowserLauncher.cs delete mode 100644 Emby.Server.Implementations/EntryPoints/StartupWizard.cs (limited to 'MediaBrowser.Model/Configuration') diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index f1fe65064b..2ec147ba2f 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -103,6 +103,7 @@ - [sl1288](https://github.com/sl1288) - [sorinyo2004](https://github.com/sorinyo2004) - [sparky8251](https://github.com/sparky8251) + - [spookbits](https://github.com/spookbits) - [stanionascu](https://github.com/stanionascu) - [stevehayles](https://github.com/stevehayles) - [SuperSandro2000](https://github.com/SuperSandro2000) diff --git a/Emby.Server.Implementations/Browser/BrowserLauncher.cs b/Emby.Server.Implementations/Browser/BrowserLauncher.cs deleted file mode 100644 index f8108d1c2d..0000000000 --- a/Emby.Server.Implementations/Browser/BrowserLauncher.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.Browser -{ - /// - /// Assists in opening application URLs in an external browser. - /// - public static class BrowserLauncher - { - /// - /// Opens the home page of the web client. - /// - /// The app host. - public static void OpenWebApp(IServerApplicationHost appHost) - { - TryOpenUrl(appHost, "/web/index.html"); - } - - /// - /// Opens the swagger API page. - /// - /// The app host. - public static void OpenSwaggerPage(IServerApplicationHost appHost) - { - TryOpenUrl(appHost, "/api-docs/swagger"); - } - - /// - /// Opens the specified URL in an external browser window. Any exceptions will be logged, but ignored. - /// - /// The application host. - /// The URL to open, relative to the server base URL. - private static void TryOpenUrl(IServerApplicationHost appHost, string relativeUrl) - { - try - { - string baseUrl = appHost.GetLocalApiUrl("localhost"); - appHost.LaunchUrl(baseUrl + relativeUrl); - } - catch (Exception ex) - { - var logger = appHost.Resolve>(); - logger?.LogError(ex, "Failed to open browser window with URL {URL}", relativeUrl); - } - } - } -} diff --git a/Emby.Server.Implementations/EntryPoints/StartupWizard.cs b/Emby.Server.Implementations/EntryPoints/StartupWizard.cs deleted file mode 100644 index 2e738deeb5..0000000000 --- a/Emby.Server.Implementations/EntryPoints/StartupWizard.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System.Threading.Tasks; -using Emby.Server.Implementations.Browser; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Extensions; -using MediaBrowser.Controller.Plugins; -using Microsoft.Extensions.Configuration; - -namespace Emby.Server.Implementations.EntryPoints -{ - /// - /// Class StartupWizard. - /// - public sealed class StartupWizard : IServerEntryPoint - { - private readonly IServerApplicationHost _appHost; - private readonly IConfiguration _appConfig; - private readonly IServerConfigurationManager _config; - private readonly IStartupOptions _startupOptions; - - /// - /// Initializes a new instance of the class. - /// - /// The application host. - /// The application configuration. - /// The configuration manager. - /// The application startup options. - public StartupWizard( - IServerApplicationHost appHost, - IConfiguration appConfig, - IServerConfigurationManager config, - IStartupOptions startupOptions) - { - _appHost = appHost; - _appConfig = appConfig; - _config = config; - _startupOptions = startupOptions; - } - - /// - public Task RunAsync() - { - Run(); - return Task.CompletedTask; - } - - private void Run() - { - if (!_appHost.CanLaunchWebBrowser) - { - return; - } - - // Always launch the startup wizard if possible when it has not been completed - if (!_config.Configuration.IsStartupWizardCompleted && _appConfig.HostWebClient()) - { - BrowserLauncher.OpenWebApp(_appHost); - return; - } - - // Do nothing if the web app is configured to not run automatically - if (!_config.Configuration.AutoRunWebApp || _startupOptions.NoAutoRunWebApp) - { - return; - } - - // Launch the swagger page if the web client is not hosted, otherwise open the web client - if (_appConfig.HostWebClient()) - { - BrowserLauncher.OpenWebApp(_appHost); - } - else - { - BrowserLauncher.OpenSwaggerPage(_appHost); - } - } - - /// - public void Dispose() - { - } - } -} diff --git a/Emby.Server.Implementations/IStartupOptions.cs b/Emby.Server.Implementations/IStartupOptions.cs index e7e72c686b..4bef59543f 100644 --- a/Emby.Server.Implementations/IStartupOptions.cs +++ b/Emby.Server.Implementations/IStartupOptions.cs @@ -16,11 +16,6 @@ namespace Emby.Server.Implementations /// bool IsService { get; } - /// - /// Gets the value of the --noautorunwebapp command line option. - /// - bool NoAutoRunWebApp { get; } - /// /// Gets the value of the --package-name command line option. /// diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 0ac309a0b0..b66314a8ea 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -1,4 +1,4 @@ - + diff --git a/Jellyfin.Server/Properties/launchSettings.json b/Jellyfin.Server/Properties/launchSettings.json index b6e2bcf976..ebdd0dedaa 100644 --- a/Jellyfin.Server/Properties/launchSettings.json +++ b/Jellyfin.Server/Properties/launchSettings.json @@ -2,14 +2,20 @@ "profiles": { "Jellyfin.Server": { "commandName": "Project", + "launchBrowser": true, + "launchUrl": "web", + "applicationUrl": "http://localhost:8096", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "Jellyfin.Server (nowebclient)": { "commandName": "Project", + "launchBrowser": true, + "launchUrl": "api-docs/swagger", + "applicationUrl": "http://localhost:8096", "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" + "ASPNETCORE_ENVIRONMENT": "Development" }, "commandLineArgs": "--nowebclient" } diff --git a/Jellyfin.Server/StartupOptions.cs b/Jellyfin.Server/StartupOptions.cs index 41a1430d26..b634340927 100644 --- a/Jellyfin.Server/StartupOptions.cs +++ b/Jellyfin.Server/StartupOptions.cs @@ -63,10 +63,6 @@ namespace Jellyfin.Server [Option("service", Required = false, HelpText = "Run as headless service.")] public bool IsService { get; set; } - /// - [Option("noautorunwebapp", Required = false, HelpText = "Run headless if startup wizard is complete.")] - public bool NoAutoRunWebApp { get; set; } - /// [Option("package-name", Required = false, HelpText = "Used when packaging Jellyfin (example, synology).")] public string? PackageName { get; set; } diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 48d1a7346a..8b78ad842e 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -83,8 +83,6 @@ namespace MediaBrowser.Model.Configuration /// public bool QuickConnectAvailable { get; set; } - public bool AutoRunWebApp { get; set; } - public bool EnableRemoteAccess { get; set; } /// @@ -306,7 +304,6 @@ namespace MediaBrowser.Model.Configuration DisableLiveTvChannelUserDataName = true; EnableNewOmdbSupport = true; - AutoRunWebApp = true; EnableRemoteAccess = true; QuickConnectAvailable = false; diff --git a/tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs b/tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs index 77f1640fa3..bd3d356870 100644 --- a/tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs +++ b/tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs @@ -47,8 +47,7 @@ namespace Jellyfin.Api.Tests // Specify the startup command line options var commandLineOpts = new StartupOptions { - NoWebClient = true, - NoAutoRunWebApp = true + NoWebClient = true }; // Use a temporary directory for the application paths -- cgit v1.2.3 From bbe2400b59563991f1689c68ef36d623afd39f0a Mon Sep 17 00:00:00 2001 From: Jim Cartlidge Date: Wed, 30 Sep 2020 17:51:17 +0100 Subject: Updating to NetCollection 1.03 --- Jellyfin.Networking/Jellyfin.Networking.csproj | 3 +- Jellyfin.Networking/Manager/NetworkManager.cs | 150 ++++++++++++++------- MediaBrowser.Common/Net/INetworkManager.cs | 17 ++- .../Configuration/ServerConfiguration.cs | 23 ++-- 4 files changed, 128 insertions(+), 65 deletions(-) (limited to 'MediaBrowser.Model/Configuration') diff --git a/Jellyfin.Networking/Jellyfin.Networking.csproj b/Jellyfin.Networking/Jellyfin.Networking.csproj index 454dd9b7df..06d387dc87 100644 --- a/Jellyfin.Networking/Jellyfin.Networking.csproj +++ b/Jellyfin.Networking/Jellyfin.Networking.csproj @@ -28,8 +28,7 @@ - - + diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 83661d6b31..6f2970ef89 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -12,6 +12,7 @@ using MediaBrowser.Model.Configuration; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using NetworkCollection; +using NetworkCollection.Udp; namespace Jellyfin.Networking.Manager { @@ -125,29 +126,19 @@ namespace Jellyfin.Networking.Manager public event EventHandler? NetworkChanged; /// - /// Gets the unique network location signature, which is updated on every network change. + /// Gets or sets a value indicating whether testing is taking place. /// - public static string NetworkLocationSignature { get; internal set; } = Guid.NewGuid().ToString(); + public static string MockNetworkSettings { get; set; } = string.Empty; /// - /// Gets a value indicating whether IP6 is enabled. + /// Gets or sets a value indicating whether IP6 is enabled. /// - public static bool IsIP6Enabled { get; internal set; } + public bool IsIP6Enabled { get; set; } /// - /// Gets a value indicating whether IP4 is enabled. + /// Gets or sets a value indicating whether IP4 is enabled. /// - public static bool IsIP4Enabled { get; internal set; } = true; - - /// - /// Gets a value indicating whether is multi-socket binding available. - /// - public static bool EnableMultiSocketBinding { get; internal set; } = true; - - /// - /// Gets the number of times the network address has changed. - /// - public static int NetworkChangeCount { get; internal set; } = 1; + public bool IsIP4Enabled { get; set; } /// public NetCollection RemoteAddressFilter { get; private set; } @@ -271,7 +262,7 @@ namespace Jellyfin.Networking.Manager } /// - public NetCollection GetAllBindInterfaces() + public NetCollection GetAllBindInterfaces(bool individualInterfaces = false) { lock (_intLock) { @@ -285,6 +276,11 @@ namespace Jellyfin.Networking.Manager return _interfaceAddresses.Exclude(_bindExclusions); } + if (individualInterfaces) + { + return new NetCollection(_interfaceAddresses); + } + // No bind address and no exclusions, so listen on all interfaces. NetCollection result = new NetCollection(); @@ -311,12 +307,6 @@ namespace Jellyfin.Networking.Manager { if (!string.IsNullOrEmpty(source)) { - if (string.Equals(source, "chromecast", StringComparison.OrdinalIgnoreCase)) - { - // Just assign a variable so has source = true; - return GetBindInterface(IPNetAddress.IP4Loopback, out port); - } - if (IPHost.TryParse(source, out IPHost host)) { return GetBindInterface(host, out port); @@ -572,16 +562,18 @@ namespace Jellyfin.Networking.Manager } /// - public bool TryParseInterface(string token, out IPNetAddress result) + public bool TryParseInterface(string token, out NetCollection? result) { + result = null; if (string.IsNullOrEmpty(token)) { - result = IPNetAddress.None; return false; } if (_interfaceNames != null && _interfaceNames.TryGetValue(token.ToLower(CultureInfo.InvariantCulture), out int index)) { + result = new NetCollection(); + _logger.LogInformation("Interface {0} used in settings. Using its interface addresses.", token); // Replace interface tags with the interface IP's. @@ -591,13 +583,14 @@ namespace Jellyfin.Networking.Manager ((IsIP4Enabled && iface.Address.AddressFamily == AddressFamily.InterNetwork) || (IsIP6Enabled && iface.Address.AddressFamily == AddressFamily.InterNetworkV6))) { - result = iface; - return true; + result.Add(iface); } } + + return true; } - return IPNetAddress.TryParse(token, out result); + return false; } /// @@ -614,9 +607,27 @@ namespace Jellyfin.Networking.Manager IsIP4Enabled = Socket.OSSupportsIPv6 && config.EnableIPV4; IsIP6Enabled = Socket.OSSupportsIPv6 && config.EnableIPV6; TrustAllIP6Interfaces = config.TrustAllIP6Interfaces; - EnableMultiSocketBinding = config.EnableMultiSocketBinding; + UdpHelper.EnableMultiSocketBinding = config.EnableMultiSocketBinding; + + if (string.IsNullOrEmpty(MockNetworkSettings)) + { + InitialiseInterfaces(); + } + else // Used in testing only. + { + // Format is ,,: . Set index to -ve to simulate a gateway. + var interfaceList = MockNetworkSettings.Split(':'); + foreach (var details in interfaceList) + { + var parts = details.Split(','); + var address = IPNetAddress.Parse(parts[0]); + var index = int.Parse(parts[1], CultureInfo.InvariantCulture); + address.Tag = index; + _interfaceAddresses.Add(address); + _interfaceNames.Add(parts[2], Math.Abs(index)); + } + } - InitialiseInterfaces(); InitialiseLAN(config); InitialiseBind(config); InitialiseRemote(config); @@ -671,6 +682,40 @@ namespace Jellyfin.Networking.Manager return str; } + /// + /// Checks the string to see if it matches any interface names. + /// + /// String to check. + /// Interface index number. + /// True if an interface name matches the token. + private bool IsInterface(string token, out int index) + { + index = -1; + + // Is it the name of an interface (windows) eg, Wireless LAN adapter Wireless Network Connection 1. + // Null check required here for automated testing. + if (_interfaceNames != null && token.Length > 1) + { + bool partial = token[^1] == '*'; + if (partial) + { + token = token[0..^1]; + } + + foreach ((string interfc, int interfcIndex) in _interfaceNames) + { + if ((!partial && string.Equals(interfc, token, StringComparison.OrdinalIgnoreCase)) || + (partial && interfc.StartsWith(token, true, CultureInfo.InvariantCulture))) + { + index = interfcIndex; + return true; + } + } + } + + return false; + } + /// /// Parses strings into the collection, replacing any interface references. /// @@ -680,7 +725,7 @@ namespace Jellyfin.Networking.Manager { // Is it the name of an interface (windows) eg, Wireless LAN adapter Wireless Network Connection 1. // Null check required here for automated testing. - if (_interfaceNames != null && _interfaceNames.TryGetValue(token.ToLower(CultureInfo.InvariantCulture), out int index)) + if (IsInterface(token, out int index)) { _logger.LogInformation("Interface {0} used in settings. Using its interface addresses.", token); @@ -774,14 +819,6 @@ namespace Jellyfin.Networking.Manager /// private void OnNetworkChanged() { - // As per UPnP Device Architecture v1.0 Annex A - IPv6 Support. - NetworkLocationSignature = Guid.NewGuid().ToString(); - NetworkChangeCount++; - if (NetworkChangeCount > 99) - { - NetworkChangeCount = 1; - } - if (!_eventfire) { _logger.LogDebug("Network Address Change Event."); @@ -800,20 +837,14 @@ namespace Jellyfin.Networking.Manager /// private void InitialiseOverrides(ServerConfiguration config) { - string[] overrides = config.PublishedServerUriBySubnet; - if (overrides == null) - { - lock (_intLock) - { - _publishedServerUrls.Clear(); - } - - return; - } - lock (_intLock) { _publishedServerUrls.Clear(); + string[] overrides = config.PublishedServerUriBySubnet; + if (overrides == null) + { + return; + } foreach (var entry in overrides) { @@ -833,9 +864,16 @@ namespace Jellyfin.Networking.Manager { _publishedServerUrls[new IPNetAddress(IPAddress.Any)] = replacement; } - else if (TryParseInterface(parts[0], out IPNetAddress address)) + else if (TryParseInterface(parts[0], out NetCollection? addresses) && addresses != null) + { + foreach (IPNetAddress na in addresses) + { + _publishedServerUrls[na] = replacement; + } + } + else if (IPNetAddress.TryParse(parts[0], out IPNetAddress result)) { - _publishedServerUrls[address] = replacement; + _publishedServerUrls[result] = replacement; } else { @@ -859,6 +897,14 @@ namespace Jellyfin.Networking.Manager // TODO: end fix. + // Add virtual machine interface names to the list of bind exclusions, so that they are auto-excluded. + if (config.IgnoreVirtualInterfaces) + { + var newList = ba.ToList(); + newList.AddRange(config.VirtualInterfaceNames.Split(',').ToList()); + ba = newList.ToArray(); + } + // Read and parse bind addresses and exclusions, removing ones that don't exist. _bindAddresses = CreateIPCollection(ba).Union(_interfaceAddresses); _bindExclusions = CreateIPCollection(ba, true).Union(_interfaceAddresses); diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs index 32c017aee6..8789cd9d81 100644 --- a/MediaBrowser.Common/Net/INetworkManager.cs +++ b/MediaBrowser.Common/Net/INetworkManager.cs @@ -34,13 +34,24 @@ namespace MediaBrowser.Common.Net /// NetCollection RemoteAddressFilter { get; } + /// + /// Gets or sets a value indicating whether iP6 is enabled. + /// + public bool IsIP6Enabled { get; set; } + + /// + /// Gets or sets a value indicating whether iP4 is enabled. + /// + public bool IsIP4Enabled { get; set; } + /// /// Calculates the list of interfaces to use for Kestrel. /// /// A NetCollection object containing all the interfaces to bind. /// If all the interfaces are specified, and none are excluded, it returns zero items /// to represent any address. - NetCollection GetAllBindInterfaces(); + /// When false, return or for all interfaces. + NetCollection GetAllBindInterfaces(bool individualInterfaces = false); /// /// Returns a collection containing the loopback interfaces. @@ -166,9 +177,9 @@ namespace MediaBrowser.Common.Net /// eg. "eth1", or "TP-LINK Wireless USB Adapter". /// /// Token to parse. - /// Resultant object if successful. + /// Resultant object's ip addresses, if successful. /// Success of the operation. - bool TryParseInterface(string token, out IPNetAddress result); + bool TryParseInterface(string token, out NetCollection? result); /// /// Parses an array of strings into a NetCollection. diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 073a629829..dbfab1fad4 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -1,4 +1,4 @@ -#nullable disable +#nullable enable #pragma warning disable CS1591 #pragma warning disable CA1819 @@ -111,7 +111,7 @@ namespace MediaBrowser.Model.Configuration /// /// Gets or sets a value indicating whether detailed ssdp logs are sent to the console/log. - /// If the setting "Emby.Dlna": "Debug" msut be set in logging.default.json for this property to work. + /// "Emby.Dlna": "Debug" must be set in logging.default.json for this property to work. /// public bool EnableSSDPTracing { get; set; } = false; @@ -131,13 +131,23 @@ namespace MediaBrowser.Model.Configuration /// public int UDPSendDelay { get; set; } = 100; + /// + /// Gets or sets a value indicating whether address names that match should be Ignore for the purposes of binding. + /// + public bool IgnoreVirtualInterfaces { get; set; } = true; + + /// + /// Gets or sets a value indicating the interfaces that should be ignored. The list can be comma separated. . + /// + public string VirtualInterfaceNames { get; set; } = "vEthernet*"; + /// /// Gets or sets the time (in seconds) between the pings of SSDP gateway monitor. /// public int GatewayMonitorPeriod { get; set; } = 60; /// - /// Gets a value indicating whether is multi-socket binding available. + /// Gets a value indicating whether multi-socket binding is available. /// public bool EnableMultiSocketBinding { get; } = true; @@ -158,7 +168,7 @@ namespace MediaBrowser.Model.Configuration public string[] PublishedServerUriBySubnet { get; set; } = Array.Empty(); /// - /// Gets or sets a value indicating whether gets or sets Autodiscovery tracing. + /// Gets or sets a value indicating whether Autodiscovery tracing is enabled. /// public bool AutoDiscoveryTracing { get; set; } = false; @@ -216,9 +226,7 @@ namespace MediaBrowser.Model.Configuration /// Gets or sets a value indicating whether quick connect is available for use on this server. /// public bool QuickConnectAvailable { get; set; } = false; - - public bool AutoRunWebApp { get; set; } = true; - + /// /// Gets or sets a value indicating whether access outside of the LAN is permitted. /// @@ -419,6 +427,5 @@ namespace MediaBrowser.Model.Configuration /// Gets or sets the known proxies. /// public string[] KnownProxies { get; set; } = Array.Empty(); - } } -- cgit v1.2.3 From 3b64171cde969a7cfca60d28c7782774a173758b Mon Sep 17 00:00:00 2001 From: Jim Cartlidge Date: Wed, 30 Sep 2020 18:02:36 +0100 Subject: Minor change to get it to compile. --- Emby.Server.Implementations/ApplicationHost.cs | 2 +- Jellyfin.Api/Controllers/StartupController.cs | 6 +++--- MediaBrowser.Model/Configuration/ServerConfiguration.cs | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) (limited to 'MediaBrowser.Model/Configuration') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index a95cfea07c..66e48ddc65 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1319,7 +1319,7 @@ namespace Emby.Server.Implementations /// public string GetLoopbackHttpApiUrl() { - if (NetworkManager.IsIP6Enabled) + if (NetManager.IsIP6Enabled) { return GetLocalApiUrl("::1", Uri.UriSchemeHttp, HttpPort); } diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index 9c259cc198..e59c6e1ddf 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -72,9 +72,9 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult UpdateInitialConfiguration([FromBody, Required] StartupConfigurationDto startupConfiguration) { - _config.Configuration.UICulture = startupConfiguration.UICulture; - _config.Configuration.MetadataCountryCode = startupConfiguration.MetadataCountryCode; - _config.Configuration.PreferredMetadataLanguage = startupConfiguration.PreferredMetadataLanguage; + _config.Configuration.UICulture = startupConfiguration.UICulture ?? string.Empty; + _config.Configuration.MetadataCountryCode = startupConfiguration.MetadataCountryCode ?? string.Empty; + _config.Configuration.PreferredMetadataLanguage = startupConfiguration.PreferredMetadataLanguage ?? string.Empty; _config.SaveConfiguration(); return NoContent(); } diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index dbfab1fad4..86b7c4c3d6 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -1,4 +1,3 @@ -#nullable enable #pragma warning disable CS1591 #pragma warning disable CA1819 -- cgit v1.2.3 From c2276b17cb196de44f1bbb42b91b000cde8fdc7f Mon Sep 17 00:00:00 2001 From: Gary Wilber Date: Wed, 30 Sep 2020 19:33:34 -0700 Subject: Increase library scan and metadata refresh speed --- Emby.Server.Implementations/ApplicationHost.cs | 1 + MediaBrowser.Controller/Entities/Folder.cs | 208 +++++++++++++-------- MediaBrowser.Controller/Library/TaskMethods.cs | 133 +++++++++++++ .../Configuration/ServerConfiguration.cs | 12 ++ 4 files changed, 277 insertions(+), 77 deletions(-) create mode 100644 MediaBrowser.Controller/Library/TaskMethods.cs (limited to 'MediaBrowser.Model/Configuration') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 7a46fdf2e7..6d7239f724 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -758,6 +758,7 @@ namespace Emby.Server.Implementations BaseItem.FileSystem = _fileSystemManager; BaseItem.UserDataManager = Resolve(); BaseItem.ChannelManager = Resolve(); + TaskMethods.ConfigurationManager = ServerConfigurationManager; Video.LiveTvManager = Resolve(); Folder.UserViewManager = Resolve(); UserView.TVSeriesManager = Resolve(); diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 901ea875bc..666455cff4 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -35,6 +35,46 @@ namespace MediaBrowser.Controller.Entities /// public class Folder : BaseItem { + /// + /// Contains constants used when reporting scan progress. + /// + private static class ProgressHelpers + { + /// + /// Reported after the folders immediate children are retrieved. + /// + public const int RetrievedChildren = 5; + + /// + /// Reported after add, updating, or deleting child items from the LibraryManager. + /// + public const int UpdatedChildItems = 10; + + /// + /// Reported once subfolders are scanned. + /// When scanning subfolders, the progress will be between [UpdatedItems, ScannedSubfolders]. + /// + public const int ScannedSubfolders = 50; + + /// + /// Reported once metadata is refreshed. + /// When refreshing metadata, the progress will be between [ScannedSubfolders, MetadataRefreshed]. + /// + public const int RefreshedMetadata = 100; + + /// + /// Gets the current progress given the previous step, next step, and progress in between. + /// + /// The previous progress step. + /// The next progress step. + /// The current progress step. + /// The progress. + public static double GetProgress(int previousProgressStep, int nextProgressStep, double currentProgress) + { + return previousProgressStep + ((nextProgressStep - previousProgressStep) * (currentProgress / 100)); + } + } + public static IUserViewManager UserViewManager { get; set; } /// @@ -327,11 +367,11 @@ namespace MediaBrowser.Controller.Entities return; } - progress.Report(5); + progress.Report(ProgressHelpers.RetrievedChildren); if (recursive) { - ProviderManager.OnRefreshProgress(this, 5); + ProviderManager.OnRefreshProgress(this, ProgressHelpers.RetrievedChildren); } // Build a dictionary of the current children we have now by Id so we can compare quickly and easily @@ -392,11 +432,11 @@ namespace MediaBrowser.Controller.Entities validChildrenNeedGeneration = true; } - progress.Report(10); + progress.Report(ProgressHelpers.UpdatedChildItems); if (recursive) { - ProviderManager.OnRefreshProgress(this, 10); + ProviderManager.OnRefreshProgress(this, ProgressHelpers.UpdatedChildItems); } cancellationToken.ThrowIfCancellationRequested(); @@ -406,11 +446,13 @@ namespace MediaBrowser.Controller.Entities var innerProgress = new ActionableProgress(); var folder = this; - innerProgress.RegisterAction(p => + innerProgress.RegisterAction(innerPercent => { - double newPct = 0.80 * p + 10; - progress.Report(newPct); - ProviderManager.OnRefreshProgress(folder, newPct); + var percent = ProgressHelpers.GetProgress(ProgressHelpers.UpdatedChildItems, ProgressHelpers.ScannedSubfolders, innerPercent); + + progress.Report(percent); + + ProviderManager.OnRefreshProgress(folder, percent); }); if (validChildrenNeedGeneration) @@ -424,11 +466,11 @@ namespace MediaBrowser.Controller.Entities if (refreshChildMetadata) { - progress.Report(90); + progress.Report(ProgressHelpers.ScannedSubfolders); if (recursive) { - ProviderManager.OnRefreshProgress(this, 90); + ProviderManager.OnRefreshProgress(this, ProgressHelpers.ScannedSubfolders); } var container = this as IMetadataContainer; @@ -436,13 +478,15 @@ namespace MediaBrowser.Controller.Entities var innerProgress = new ActionableProgress(); var folder = this; - innerProgress.RegisterAction(p => + innerProgress.RegisterAction(innerPercent => { - double newPct = 0.10 * p + 90; - progress.Report(newPct); + var percent = ProgressHelpers.GetProgress(ProgressHelpers.ScannedSubfolders, ProgressHelpers.RefreshedMetadata, innerPercent); + + progress.Report(percent); + if (recursive) { - ProviderManager.OnRefreshProgress(folder, newPct); + ProviderManager.OnRefreshProgress(folder, percent); } }); @@ -457,55 +501,37 @@ namespace MediaBrowser.Controller.Entities validChildren = Children.ToList(); } - await RefreshMetadataRecursive(validChildren, refreshOptions, recursive, innerProgress, cancellationToken); + await RefreshMetadataRecursive(validChildren, refreshOptions, recursive, innerProgress, cancellationToken).ConfigureAwait(false); } } } - private async Task RefreshMetadataRecursive(List children, MetadataRefreshOptions refreshOptions, bool recursive, IProgress progress, CancellationToken cancellationToken) + private Task RefreshMetadataRecursive(IList children, MetadataRefreshOptions refreshOptions, bool recursive, IProgress progress, CancellationToken cancellationToken) { - var numComplete = 0; - var count = children.Count; - double currentPercent = 0; - - foreach (var child in children) - { - cancellationToken.ThrowIfCancellationRequested(); - - var innerProgress = new ActionableProgress(); - - // Avoid implicitly captured closure - var currentInnerPercent = currentPercent; - - innerProgress.RegisterAction(p => - { - double innerPercent = currentInnerPercent; - innerPercent += p / count; - progress.Report(innerPercent); - }); - - await RefreshChildMetadata(child, refreshOptions, recursive && child.IsFolder, innerProgress, cancellationToken) - .ConfigureAwait(false); - - numComplete++; - double percent = numComplete; - percent /= count; - percent *= 100; - currentPercent = percent; + var progressableTasks = children + .Select, Task>>(child => + innerProgress => RefreshChildMetadata(child, refreshOptions, recursive && child.IsFolder, innerProgress, cancellationToken)) + .ToList(); - progress.Report(percent); - } + return RunTasks(progressableTasks, progress, cancellationToken); } private async Task RefreshAllMetadataForContainer(IMetadataContainer container, MetadataRefreshOptions refreshOptions, IProgress progress, CancellationToken cancellationToken) { - var series = container as Series; - if (series != null) - { - await series.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); - } + // limit the amount of concurrent metadata refreshes + await TaskMethods.RunThrottled( + TaskMethods.SharedThrottleId.RefreshMetadata, + async () => + { + var series = container as Series; + if (series != null) + { + await series.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); + } - await container.RefreshAllMetadata(refreshOptions, progress, cancellationToken).ConfigureAwait(false); + await container.RefreshAllMetadata(refreshOptions, progress, cancellationToken).ConfigureAwait(false); + }, + cancellationToken).ConfigureAwait(false); } private async Task RefreshChildMetadata(BaseItem child, MetadataRefreshOptions refreshOptions, bool recursive, IProgress progress, CancellationToken cancellationToken) @@ -520,12 +546,16 @@ namespace MediaBrowser.Controller.Entities { if (refreshOptions.RefreshItem(child)) { - await child.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); + // limit the amount of concurrent metadata refreshes + await TaskMethods.RunThrottled( + TaskMethods.SharedThrottleId.RefreshMetadata, + async () => await child.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false), + cancellationToken).ConfigureAwait(false); } if (recursive && child is Folder folder) { - await folder.RefreshMetadataRecursive(folder.Children.ToList(), refreshOptions, true, progress, cancellationToken); + await folder.RefreshMetadataRecursive(folder.Children.ToList(), refreshOptions, true, progress, cancellationToken).ConfigureAwait(false); } } } @@ -538,39 +568,63 @@ namespace MediaBrowser.Controller.Entities /// The progress. /// The cancellation token. /// Task. - private async Task ValidateSubFolders(IList children, IDirectoryService directoryService, IProgress progress, CancellationToken cancellationToken) + private Task ValidateSubFolders(IList children, IDirectoryService directoryService, IProgress progress, CancellationToken cancellationToken) { - var numComplete = 0; - var count = children.Count; - double currentPercent = 0; + var progressableTasks = children + .Select, Task>>(child => + innerProgress => child.ValidateChildrenInternal(innerProgress, cancellationToken, true, false, null, directoryService)) + .ToList(); - foreach (var child in children) - { - cancellationToken.ThrowIfCancellationRequested(); + return RunTasks(progressableTasks, progress, cancellationToken); + } - var innerProgress = new ActionableProgress(); + /// + /// Runs a set of tasks concurrently with progress. + /// + /// A list of tasks. + /// The progress. + /// The cancellation token. + /// Task. + private async Task RunTasks(IList, Task>> tasks, IProgress progress, CancellationToken cancellationToken) + { + var childrenCount = tasks.Count; + var childrenProgress = new double[childrenCount]; + var actions = new Func[childrenCount]; + + void UpdateProgress() + { + progress.Report(childrenProgress.Average()); + } - // Avoid implicitly captured closure - var currentInnerPercent = currentPercent; + for (var i = 0; i < childrenCount; i++) + { + var childIndex = i; + var child = tasks[childIndex]; - innerProgress.RegisterAction(p => + actions[childIndex] = async () => { - double innerPercent = currentInnerPercent; - innerPercent += p / count; - progress.Report(innerPercent); - }); + var innerProgress = new ActionableProgress(); + + innerProgress.RegisterAction(innerPercent => + { + // round the percent and only update progress if it changed to prevent excessive UpdateProgress calls + var innerPercentRounded = Math.Round(innerPercent); + if (childrenProgress[childIndex] != innerPercentRounded) + { + childrenProgress[childIndex] = innerPercentRounded; + UpdateProgress(); + } + }); - await child.ValidateChildrenInternal(innerProgress, cancellationToken, true, false, null, directoryService) - .ConfigureAwait(false); + await tasks[childIndex](innerProgress).ConfigureAwait(false); - numComplete++; - double percent = numComplete; - percent /= count; - percent *= 100; - currentPercent = percent; + childrenProgress[childIndex] = 100; - progress.Report(percent); + UpdateProgress(); + }; } + + await TaskMethods.WhenAllThrottled(TaskMethods.SharedThrottleId.ScanFanout, actions, cancellationToken).ConfigureAwait(false); } /// diff --git a/MediaBrowser.Controller/Library/TaskMethods.cs b/MediaBrowser.Controller/Library/TaskMethods.cs new file mode 100644 index 0000000000..66bfbe0d9e --- /dev/null +++ b/MediaBrowser.Controller/Library/TaskMethods.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Configuration; + +namespace MediaBrowser.Controller.Library +{ + /// + /// Helper methods for running tasks concurrently. + /// + public static class TaskMethods + { + private static readonly int _processorCount = Environment.ProcessorCount; + + private static readonly ConcurrentDictionary _sharedThrottlers = new ConcurrentDictionary(); + + /// + /// Throttle id for sharing a concurrency limit. + /// + public enum SharedThrottleId + { + /// + /// Library scan fan out + /// + ScanFanout, + + /// + /// Refresh metadata + /// + RefreshMetadata, + } + + /// + /// Gets or sets the configuration manager. + /// + public static IServerConfigurationManager ConfigurationManager { get; set; } + + /// + /// Similiar to Task.WhenAll but only allows running a certain amount of tasks at the same time. + /// + /// The throttle id. Multiple calls to this method with the same throttle id will share a concurrency limit. + /// List of actions to run. + /// The cancellation token. + /// A representing the result of the asynchronous operation. + public static async Task WhenAllThrottled(SharedThrottleId throttleId, IEnumerable> actions, CancellationToken cancellationToken) + { + var taskThrottler = throttleId == SharedThrottleId.ScanFanout ? + new SemaphoreSlim(GetConcurrencyLimit(throttleId)) : + _sharedThrottlers.GetOrAdd(throttleId, id => new SemaphoreSlim(GetConcurrencyLimit(id))); + + try + { + var tasks = new List(); + + foreach (var action in actions) + { + await taskThrottler.WaitAsync(cancellationToken).ConfigureAwait(false); + + tasks.Add(Task.Run(async () => + { + try + { + await action().ConfigureAwait(false); + } + finally + { + taskThrottler.Release(); + } + })); + } + + await Task.WhenAll(tasks).ConfigureAwait(false); + } + finally + { + if (throttleId == SharedThrottleId.ScanFanout) + { + taskThrottler.Dispose(); + } + } + } + + /// + /// Runs a task within a given throttler. + /// + /// The throttle id. Multiple calls to this method with the same throttle id will share a concurrency limit. + /// The action to run. + /// The cancellation token. + /// A representing the result of the asynchronous operation. + public static async Task RunThrottled(SharedThrottleId throttleId, Func action, CancellationToken cancellationToken) + { + if (throttleId == SharedThrottleId.ScanFanout) + { + // just await the task instead + throw new InvalidOperationException("Invalid throttle id"); + } + + var taskThrottler = _sharedThrottlers.GetOrAdd(throttleId, id => new SemaphoreSlim(GetConcurrencyLimit(id))); + + await taskThrottler.WaitAsync(cancellationToken).ConfigureAwait(false); + + try + { + await action().ConfigureAwait(false); + } + finally + { + taskThrottler.Release(); + } + } + + /// + /// Get the concurrency limit for the given throttle id. + /// + /// The throttle id. + /// The concurrency limit. + private static int GetConcurrencyLimit(SharedThrottleId throttleId) + { + var concurrency = throttleId == SharedThrottleId.RefreshMetadata ? + ConfigurationManager.Configuration.LibraryMetadataRefreshConcurrency : + ConfigurationManager.Configuration.LibraryScanFanoutConcurrency; + + if (concurrency <= 0) + { + concurrency = _processorCount; + } + + return concurrency; + } + } +} diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 8b78ad842e..14bfcbf9e2 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -271,6 +271,16 @@ namespace MediaBrowser.Model.Configuration /// public string[] KnownProxies { get; set; } + /// + /// Gets or sets the how the library scan fans out. + /// + public int LibraryScanFanoutConcurrency { get; set; } + + /// + /// Gets or sets the how many metadata refreshes can run concurrently. + /// + public int LibraryMetadataRefreshConcurrency { get; set; } + /// /// Initializes a new instance of the class. /// @@ -381,6 +391,8 @@ namespace MediaBrowser.Model.Configuration SlowResponseThresholdMs = 500; CorsHosts = new[] { "*" }; KnownProxies = Array.Empty(); + LibraryMetadataRefreshConcurrency = 0; + LibraryScanFanoutConcurrency = 0; } } -- cgit v1.2.3 From 29133b6452533c6a1ca1e38cd34977e512ac18de Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Sun, 4 Oct 2020 10:04:43 +0100 Subject: Update PathSubstitution.cs --- MediaBrowser.Model/Configuration/PathSubstitution.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'MediaBrowser.Model/Configuration') diff --git a/MediaBrowser.Model/Configuration/PathSubstitution.cs b/MediaBrowser.Model/Configuration/PathSubstitution.cs index 40eb36c2ea..a066886e2b 100644 --- a/MediaBrowser.Model/Configuration/PathSubstitution.cs +++ b/MediaBrowser.Model/Configuration/PathSubstitution.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable #pragma warning disable CS1591 #pragma warning disable CA1819 -- cgit v1.2.3 From 72cd6ab0712a0a532e0dda2b26afe793ad34111b Mon Sep 17 00:00:00 2001 From: cvium Date: Fri, 18 Sep 2020 15:24:56 +0200 Subject: Remove dummy season and missing episode provider in a futile attempt to remove cruft --- MediaBrowser.Model/Configuration/LibraryOptions.cs | 2 - .../Plugins/TheTvdb/TvdbClientManager.cs | 26 -- MediaBrowser.Providers/TV/DummySeasonProvider.cs | 229 ------------ .../TV/MissingEpisodeProvider.cs | 404 --------------------- MediaBrowser.Providers/TV/SeriesMetadataService.cs | 41 +-- 5 files changed, 1 insertion(+), 701 deletions(-) delete mode 100644 MediaBrowser.Providers/TV/DummySeasonProvider.cs delete mode 100644 MediaBrowser.Providers/TV/MissingEpisodeProvider.cs (limited to 'MediaBrowser.Model/Configuration') diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs index 890469d361..54ef49ea62 100644 --- a/MediaBrowser.Model/Configuration/LibraryOptions.cs +++ b/MediaBrowser.Model/Configuration/LibraryOptions.cs @@ -25,8 +25,6 @@ namespace MediaBrowser.Model.Configuration public bool EnableInternetProviders { get; set; } - public bool ImportMissingEpisodes { get; set; } - public bool EnableAutomaticSeriesGrouping { get; set; } public bool EnableEmbeddedTitles { get; set; } diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs index f22d484abc..5e9a4a2252 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs @@ -80,32 +80,6 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb return TryGetValue(cacheKey, language, () => TvDbClient.Episodes.GetAsync(episodeTvdbId, cancellationToken)); } - public async Task> GetAllEpisodesAsync(int tvdbId, string language, - CancellationToken cancellationToken) - { - // Traverse all episode pages and join them together - var episodes = new List(); - var episodePage = await GetEpisodesPageAsync(tvdbId, new EpisodeQuery(), language, cancellationToken) - .ConfigureAwait(false); - episodes.AddRange(episodePage.Data); - if (!episodePage.Links.Next.HasValue || !episodePage.Links.Last.HasValue) - { - return episodes; - } - - int next = episodePage.Links.Next.Value; - int last = episodePage.Links.Last.Value; - - for (var page = next; page <= last; ++page) - { - episodePage = await GetEpisodesPageAsync(tvdbId, page, new EpisodeQuery(), language, cancellationToken) - .ConfigureAwait(false); - episodes.AddRange(episodePage.Data); - } - - return episodes; - } - public Task> GetSeriesByImdbIdAsync( string imdbId, string language, diff --git a/MediaBrowser.Providers/TV/DummySeasonProvider.cs b/MediaBrowser.Providers/TV/DummySeasonProvider.cs deleted file mode 100644 index 905cbefd32..0000000000 --- a/MediaBrowser.Providers/TV/DummySeasonProvider.cs +++ /dev/null @@ -1,229 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.IO; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Providers.TV -{ - public class DummySeasonProvider - { - private readonly ILogger _logger; - private readonly ILocalizationManager _localization; - private readonly ILibraryManager _libraryManager; - private readonly IFileSystem _fileSystem; - - public DummySeasonProvider( - ILogger logger, - ILocalizationManager localization, - ILibraryManager libraryManager, - IFileSystem fileSystem) - { - _logger = logger; - _localization = localization; - _libraryManager = libraryManager; - _fileSystem = fileSystem; - } - - public async Task Run(Series series, CancellationToken cancellationToken) - { - var seasonsRemoved = RemoveObsoleteSeasons(series); - - var hasNewSeasons = await AddDummySeasonFolders(series, cancellationToken).ConfigureAwait(false); - - if (hasNewSeasons) - { - // var directoryService = new DirectoryService(_fileSystem); - - // await series.RefreshMetadata(new MetadataRefreshOptions(directoryService), cancellationToken).ConfigureAwait(false); - - // await series.ValidateChildren(new SimpleProgress(), cancellationToken, new MetadataRefreshOptions(directoryService)) - // .ConfigureAwait(false); - } - - return seasonsRemoved || hasNewSeasons; - } - - private async Task AddDummySeasonFolders(Series series, CancellationToken cancellationToken) - { - var episodesInSeriesFolder = series.GetRecursiveChildren(i => i is Episode) - .Cast() - .Where(i => !i.IsInSeasonFolder) - .ToList(); - - var hasChanges = false; - - List seasons = null; - - // Loop through the unique season numbers - foreach (var seasonNumber in episodesInSeriesFolder.Select(i => i.ParentIndexNumber ?? -1) - .Where(i => i >= 0) - .Distinct() - .ToList()) - { - if (seasons == null) - { - seasons = series.Children.OfType().ToList(); - } - - var existingSeason = seasons - .FirstOrDefault(i => i.IndexNumber.HasValue && i.IndexNumber.Value == seasonNumber); - - if (existingSeason == null) - { - await AddSeason(series, seasonNumber, false, cancellationToken).ConfigureAwait(false); - hasChanges = true; - seasons = null; - } - else if (existingSeason.IsVirtualItem) - { - existingSeason.IsVirtualItem = false; - await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false); - seasons = null; - } - } - - // Unknown season - create a dummy season to put these under - if (episodesInSeriesFolder.Any(i => !i.ParentIndexNumber.HasValue)) - { - if (seasons == null) - { - seasons = series.Children.OfType().ToList(); - } - - var existingSeason = seasons - .FirstOrDefault(i => !i.IndexNumber.HasValue); - - if (existingSeason == null) - { - await AddSeason(series, null, false, cancellationToken).ConfigureAwait(false); - - hasChanges = true; - seasons = null; - } - else if (existingSeason.IsVirtualItem) - { - existingSeason.IsVirtualItem = false; - await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false); - seasons = null; - } - } - - return hasChanges; - } - - /// - /// Adds the season. - /// - public async Task AddSeason( - Series series, - int? seasonNumber, - bool isVirtualItem, - CancellationToken cancellationToken) - { - string seasonName; - if (seasonNumber == null) - { - seasonName = _localization.GetLocalizedString("NameSeasonUnknown"); - } - else if (seasonNumber == 0) - { - seasonName = _libraryManager.GetLibraryOptions(series).SeasonZeroDisplayName; - } - else - { - seasonName = string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("NameSeasonNumber"), - seasonNumber.Value); - } - - _logger.LogInformation("Creating Season {0} entry for {1}", seasonName, series.Name); - - var season = new Season - { - Name = seasonName, - IndexNumber = seasonNumber, - Id = _libraryManager.GetNewItemId( - series.Id + (seasonNumber ?? -1).ToString(CultureInfo.InvariantCulture) + seasonName, - typeof(Season)), - IsVirtualItem = isVirtualItem, - SeriesId = series.Id, - SeriesName = series.Name - }; - - season.SetParent(series); - - series.AddChild(season, cancellationToken); - - await season.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)), cancellationToken).ConfigureAwait(false); - - return season; - } - - private bool RemoveObsoleteSeasons(Series series) - { - var existingSeasons = series.Children.OfType().ToList(); - - var physicalSeasons = existingSeasons - .Where(i => i.LocationType != LocationType.Virtual) - .ToList(); - - var virtualSeasons = existingSeasons - .Where(i => i.LocationType == LocationType.Virtual) - .ToList(); - - var seasonsToRemove = virtualSeasons - .Where(i => - { - if (i.IndexNumber.HasValue) - { - var seasonNumber = i.IndexNumber.Value; - - // If there's a physical season with the same number, delete it - if (physicalSeasons.Any(p => p.IndexNumber.HasValue && (p.IndexNumber.Value == seasonNumber))) - { - return true; - } - } - - // If there are no episodes with this season number, delete it - if (!i.GetEpisodes().Any()) - { - return true; - } - - return false; - }) - .ToList(); - - var hasChanges = false; - - foreach (var seasonToRemove in seasonsToRemove) - { - _logger.LogInformation("Removing virtual season {0} {1}", series.Name, seasonToRemove.IndexNumber); - - _libraryManager.DeleteItem( - seasonToRemove, - new DeleteOptions - { - DeleteFileLocation = true - }, - false); - - hasChanges = true; - } - - return hasChanges; - } - } -} diff --git a/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs b/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs deleted file mode 100644 index c833b12271..0000000000 --- a/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs +++ /dev/null @@ -1,404 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.IO; -using MediaBrowser.Providers.Plugins.TheTvdb; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Providers.TV -{ - public class MissingEpisodeProvider - { - private const double UnairedEpisodeThresholdDays = 2; - - private readonly IServerConfigurationManager _config; - private readonly ILogger _logger; - private readonly ILibraryManager _libraryManager; - private readonly ILocalizationManager _localization; - private readonly IFileSystem _fileSystem; - private readonly TvdbClientManager _tvdbClientManager; - - public MissingEpisodeProvider( - ILogger logger, - IServerConfigurationManager config, - ILibraryManager libraryManager, - ILocalizationManager localization, - IFileSystem fileSystem, - TvdbClientManager tvdbClientManager) - { - _logger = logger; - _config = config; - _libraryManager = libraryManager; - _localization = localization; - _fileSystem = fileSystem; - _tvdbClientManager = tvdbClientManager; - } - - public async Task Run(Series series, bool addNewItems, CancellationToken cancellationToken) - { - var tvdbIdString = series.GetProviderId(MetadataProvider.Tvdb); - if (string.IsNullOrEmpty(tvdbIdString)) - { - return false; - } - - var episodes = await _tvdbClientManager.GetAllEpisodesAsync( - int.Parse(tvdbIdString, CultureInfo.InvariantCulture), - series.GetPreferredMetadataLanguage(), - cancellationToken).ConfigureAwait(false); - - var episodeLookup = episodes - .Select(i => - { - if (!DateTime.TryParse(i.FirstAired, out var firstAired)) - { - firstAired = default; - } - - var seasonNumber = i.AiredSeason.GetValueOrDefault(-1); - var episodeNumber = i.AiredEpisodeNumber.GetValueOrDefault(-1); - return (seasonNumber, episodeNumber, firstAired); - }) - .Where(i => i.seasonNumber != -1 && i.episodeNumber != -1) - .OrderBy(i => i.seasonNumber) - .ThenBy(i => i.episodeNumber) - .ToList(); - - var allRecursiveChildren = series.GetRecursiveChildren(); - - var hasBadData = HasInvalidContent(allRecursiveChildren); - - // Be conservative here to avoid creating missing episodes for ones they already have - var addMissingEpisodes = !hasBadData && _libraryManager.GetLibraryOptions(series).ImportMissingEpisodes; - - var anySeasonsRemoved = RemoveObsoleteOrMissingSeasons(allRecursiveChildren, episodeLookup); - - if (anySeasonsRemoved) - { - // refresh this - allRecursiveChildren = series.GetRecursiveChildren(); - } - - var anyEpisodesRemoved = RemoveObsoleteOrMissingEpisodes(allRecursiveChildren, episodeLookup, addMissingEpisodes); - - if (anyEpisodesRemoved) - { - // refresh this - allRecursiveChildren = series.GetRecursiveChildren(); - } - - var hasNewEpisodes = false; - - if (addNewItems && series.IsMetadataFetcherEnabled(_libraryManager.GetLibraryOptions(series), TvdbSeriesProvider.Current.Name)) - { - hasNewEpisodes = await AddMissingEpisodes(series, allRecursiveChildren, addMissingEpisodes, episodeLookup, cancellationToken) - .ConfigureAwait(false); - } - - if (hasNewEpisodes || anySeasonsRemoved || anyEpisodesRemoved) - { - return true; - } - - return false; - } - - /// - /// Returns true if a series has any seasons or episodes without season or episode numbers - /// If this data is missing no virtual items will be added in order to prevent possible duplicates. - /// - private bool HasInvalidContent(IList allItems) - { - return allItems.OfType().Any(i => !i.IndexNumber.HasValue) || - allItems.OfType().Any(i => - { - if (!i.ParentIndexNumber.HasValue) - { - return true; - } - - // You could have episodes under season 0 with no number - return false; - }); - } - - private async Task AddMissingEpisodes( - Series series, - IEnumerable allItems, - bool addMissingEpisodes, - IReadOnlyCollection<(int seasonNumber, int episodenumber, DateTime firstAired)> episodeLookup, - CancellationToken cancellationToken) - { - var existingEpisodes = allItems.OfType().ToList(); - - var seasonCounts = episodeLookup.GroupBy(e => e.seasonNumber).ToDictionary(g => g.Key, g => g.Count()); - - var hasChanges = false; - - foreach (var tuple in episodeLookup) - { - if (tuple.seasonNumber <= 0 || tuple.episodenumber <= 0) - { - // Ignore episode/season zeros - continue; - } - - var existingEpisode = GetExistingEpisode(existingEpisodes, seasonCounts, tuple); - - if (existingEpisode != null) - { - continue; - } - - var airDate = tuple.firstAired; - - var now = DateTime.UtcNow.AddDays(-UnairedEpisodeThresholdDays); - - if ((airDate < now && addMissingEpisodes) || airDate > now) - { - // tvdb has a lot of nearly blank episodes - _logger.LogInformation("Creating virtual missing/unaired episode {0} {1}x{2}", series.Name, tuple.seasonNumber, tuple.episodenumber); - await AddEpisode(series, tuple.seasonNumber, tuple.episodenumber, cancellationToken).ConfigureAwait(false); - - hasChanges = true; - } - } - - return hasChanges; - } - - /// - /// Removes the virtual entry after a corresponding physical version has been added. - /// - private bool RemoveObsoleteOrMissingEpisodes( - IEnumerable allRecursiveChildren, - IEnumerable<(int seasonNumber, int episodeNumber, DateTime firstAired)> episodeLookup, - bool allowMissingEpisodes) - { - var existingEpisodes = allRecursiveChildren.OfType(); - - var physicalEpisodes = new List(); - var virtualEpisodes = new List(); - foreach (var episode in existingEpisodes) - { - if (episode.LocationType == LocationType.Virtual) - { - virtualEpisodes.Add(episode); - } - else - { - physicalEpisodes.Add(episode); - } - } - - var episodesToRemove = virtualEpisodes - .Where(i => - { - if (!i.IndexNumber.HasValue || !i.ParentIndexNumber.HasValue) - { - return true; - } - - var seasonNumber = i.ParentIndexNumber.Value; - var episodeNumber = i.IndexNumber.Value; - - // If there's a physical episode with the same season and episode number, delete it - if (physicalEpisodes.Any(p => - p.ParentIndexNumber.HasValue && p.ParentIndexNumber.Value == seasonNumber && - p.ContainsEpisodeNumber(episodeNumber))) - { - return true; - } - - // If the episode no longer exists in the remote lookup, delete it - if (!episodeLookup.Any(e => e.seasonNumber == seasonNumber && e.episodeNumber == episodeNumber)) - { - return true; - } - - // If it's missing, but not unaired, remove it - return !allowMissingEpisodes && i.IsMissingEpisode && - (!i.PremiereDate.HasValue || - i.PremiereDate.Value.ToLocalTime().Date.AddDays(UnairedEpisodeThresholdDays) < - DateTime.Now.Date); - }); - - var hasChanges = false; - - foreach (var episodeToRemove in episodesToRemove) - { - _libraryManager.DeleteItem( - episodeToRemove, - new DeleteOptions - { - DeleteFileLocation = true - }, - false); - - hasChanges = true; - } - - return hasChanges; - } - - /// - /// Removes the obsolete or missing seasons. - /// - /// All recursive children. - /// The episode lookup. - /// . - private bool RemoveObsoleteOrMissingSeasons( - IList allRecursiveChildren, - IEnumerable<(int seasonNumber, int episodeNumber, DateTime firstAired)> episodeLookup) - { - var existingSeasons = allRecursiveChildren.OfType().ToList(); - - var physicalSeasons = new List(); - var virtualSeasons = new List(); - foreach (var season in existingSeasons) - { - if (season.LocationType == LocationType.Virtual) - { - virtualSeasons.Add(season); - } - else - { - physicalSeasons.Add(season); - } - } - - var allEpisodes = allRecursiveChildren.OfType().ToList(); - - var seasonsToRemove = virtualSeasons - .Where(i => - { - if (i.IndexNumber.HasValue) - { - var seasonNumber = i.IndexNumber.Value; - - // If there's a physical season with the same number, delete it - if (physicalSeasons.Any(p => p.IndexNumber.HasValue && p.IndexNumber.Value == seasonNumber && string.Equals(p.Series.PresentationUniqueKey, i.Series.PresentationUniqueKey, StringComparison.Ordinal))) - { - return true; - } - - // If the season no longer exists in the remote lookup, delete it, but only if an existing episode doesn't require it - return episodeLookup.All(e => e.seasonNumber != seasonNumber) && allEpisodes.All(s => s.ParentIndexNumber != seasonNumber || s.IsInSeasonFolder); - } - - // Season does not have a number - // Remove if there are no episodes directly in series without a season number - return allEpisodes.All(s => s.ParentIndexNumber.HasValue || s.IsInSeasonFolder); - }); - - var hasChanges = false; - - foreach (var seasonToRemove in seasonsToRemove) - { - _libraryManager.DeleteItem( - seasonToRemove, - new DeleteOptions - { - DeleteFileLocation = true - }, - false); - - hasChanges = true; - } - - return hasChanges; - } - - /// - /// Adds the episode. - /// - /// The series. - /// The season number. - /// The episode number. - /// The cancellation token. - /// Task. - private async Task AddEpisode(Series series, int seasonNumber, int episodeNumber, CancellationToken cancellationToken) - { - var season = series.Children.OfType() - .FirstOrDefault(i => i.IndexNumber.HasValue && i.IndexNumber.Value == seasonNumber); - - if (season == null) - { - var provider = new DummySeasonProvider(_logger, _localization, _libraryManager, _fileSystem); - season = await provider.AddSeason(series, seasonNumber, true, cancellationToken).ConfigureAwait(false); - } - - var name = "Episode " + episodeNumber.ToString(CultureInfo.InvariantCulture); - - var episode = new Episode - { - Name = name, - IndexNumber = episodeNumber, - ParentIndexNumber = seasonNumber, - Id = _libraryManager.GetNewItemId( - series.Id + seasonNumber.ToString(CultureInfo.InvariantCulture) + name, - typeof(Episode)), - IsVirtualItem = true, - SeasonId = season?.Id ?? Guid.Empty, - SeriesId = series.Id - }; - - season.AddChild(episode, cancellationToken); - - await episode.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)), cancellationToken).ConfigureAwait(false); - } - - /// - /// Gets the existing episode. - /// - /// The existing episodes. - /// - /// - /// Episode. - private Episode GetExistingEpisode( - IEnumerable existingEpisodes, - IReadOnlyDictionary seasonCounts, - (int seasonNumber, int episodeNumber, DateTime firstAired) episodeTuple) - { - var seasonNumber = episodeTuple.seasonNumber; - var episodeNumber = episodeTuple.episodeNumber; - - while (true) - { - var episode = GetExistingEpisode(existingEpisodes, seasonNumber, episodeNumber); - if (episode != null) - { - return episode; - } - - seasonNumber--; - - if (seasonCounts.ContainsKey(seasonNumber)) - { - episodeNumber += seasonCounts[seasonNumber]; - } - else - { - break; - } - } - - return null; - } - - private Episode GetExistingEpisode(IEnumerable existingEpisodes, int season, int episode) - => existingEpisodes.FirstOrDefault(i => i.ParentIndexNumber == season && i.ContainsEpisodeNumber(episode)); - } -} diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs index a2c0e62c19..c8fc568a22 100644 --- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs +++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs @@ -1,65 +1,26 @@ #pragma warning disable CS1591 -using System; -using System.Threading; -using System.Threading.Tasks; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Providers.Manager; -using MediaBrowser.Providers.Plugins.TheTvdb; using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.TV { public class SeriesMetadataService : MetadataService { - private readonly ILocalizationManager _localization; - private readonly TvdbClientManager _tvdbClientManager; - public SeriesMetadataService( IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, - ILibraryManager libraryManager, - ILocalizationManager localization, - TvdbClientManager tvdbClientManager) + ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) { - _localization = localization; - _tvdbClientManager = tvdbClientManager; - } - - /// - protected override async Task AfterMetadataRefresh(Series item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken) - { - await base.AfterMetadataRefresh(item, refreshOptions, cancellationToken).ConfigureAwait(false); - - var seasonProvider = new DummySeasonProvider(Logger, _localization, LibraryManager, FileSystem); - await seasonProvider.Run(item, cancellationToken).ConfigureAwait(false); - - // TODO why does it not register this itself omg - var provider = new MissingEpisodeProvider( - Logger, - ServerConfigurationManager, - LibraryManager, - _localization, - FileSystem, - _tvdbClientManager); - - try - { - await provider.Run(item, true, CancellationToken.None).ConfigureAwait(false); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error in DummySeasonProvider for {ItemPath}", item.Path); - } } /// -- cgit v1.2.3 From 39754b840dc818f6c809c37813208e1ff06a50a9 Mon Sep 17 00:00:00 2001 From: Greenback Date: Thu, 8 Oct 2020 19:16:47 +0100 Subject: minor fixes --- .../Configuration/NetworkConfiguration.cs | 4 ++-- Jellyfin.Networking/Manager/NetworkManager.cs | 19 ++++++++++--------- .../Configuration/ServerConfiguration.cs | 4 ++-- 3 files changed, 14 insertions(+), 13 deletions(-) (limited to 'MediaBrowser.Model/Configuration') diff --git a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs index 38f11153ab..aa75ac305d 100644 --- a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs +++ b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs @@ -105,12 +105,12 @@ namespace Jellyfin.Networking.Configuration public string UDPPortRange { get; set; } = string.Empty; /// - /// Gets or sets a value indicating whether gets or sets IPV6 capability.. + /// Gets or sets a value indicating whether IPV6 capability is enabled. /// public bool EnableIPV6 { get; set; } = false; /// - /// Gets or sets a value indicating whether gets or sets IPV4 capability.. + /// Gets or sets a value indicating whether IPV6 capability is enabled. /// public bool EnableIPV4 { get; set; } = true; diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 491fe824da..a094212d37 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -346,7 +346,7 @@ namespace Jellyfin.Networking.Manager if (!IsIP4Enabled && source.AddressFamily == AddressFamily.InterNetwork) { - _logger.LogWarning("IPv4 is disabled in JellyFin, but enabled in the OS. This may affect how the interface is selected."); + _logger.LogWarning("IPv4 is disabled in Jellyfin, but enabled in the OS. This may affect how the interface is selected."); } isExternal = !IsInLocalNetwork(source); @@ -872,28 +872,29 @@ namespace Jellyfin.Networking.Manager private void InitialiseBind(NetworkConfiguration config) { - string[] ba = config.LocalNetworkAddresses; + string[] lanAddresses = config.LocalNetworkAddresses; // TODO: remove when bug fixed: https://github.com/jellyfin/jellyfin-web/issues/1334 - if (ba.Length == 1 && ba[0].IndexOf(',', StringComparison.OrdinalIgnoreCase) != -1) + if (lanAddresses.Length == 1 && lanAddresses[0].IndexOf(',', StringComparison.OrdinalIgnoreCase) != -1) { - ba = ba[0].Split(','); + lanAddresses = lanAddresses[0].Split(','); } + // TODO: end fix. // TODO: end fix. // Add virtual machine interface names to the list of bind exclusions, so that they are auto-excluded. if (config.IgnoreVirtualInterfaces) { - var newList = ba.ToList(); + var newList = lanAddresses.ToList(); newList.AddRange(config.VirtualInterfaceNames.Split(',').ToList()); - ba = newList.ToArray(); + lanAddresses = newList.ToArray(); } // Read and parse bind addresses and exclusions, removing ones that don't exist. - _bindAddresses = CreateIPCollection(ba).Union(_interfaceAddresses); - _bindExclusions = CreateIPCollection(ba, true).Union(_interfaceAddresses); + _bindAddresses = CreateIPCollection(lanAddresses).Union(_interfaceAddresses); + _bindExclusions = CreateIPCollection(lanAddresses, true).Union(_interfaceAddresses); _logger.LogInformation("Using bind addresses: {0}", _bindAddresses); _logger.LogInformation("Using bind exclusions: {0}", _bindExclusions); } @@ -923,7 +924,7 @@ namespace Jellyfin.Networking.Manager // If no LAN addresses are specified - all private subnets are deemed to be the LAN _usingPrivateAddresses = _lanSubnets.Count == 0; - // NOTE: The order of the commands in this statement matters. + // NOTE: The order of the commands in this statement matters, otherwise the lists won't initialise correctly. if (_usingPrivateAddresses) { _logger.LogDebug("Using LAN interface addresses as user provided no LAN details."); diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 86b7c4c3d6..a40105212b 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -99,12 +99,12 @@ namespace MediaBrowser.Model.Configuration public string UDPPortRange { get; set; } = string.Empty; /// - /// Gets or sets a value indicating whether gets or sets IPV6 capability. + /// Gets or sets a value indicating whether IPV6 capability is enabled. /// public bool EnableIPV6 { get; set; } = false; /// - /// Gets or sets a value indicating whether gets or sets IPV4 capability. + /// Gets or sets a value indicating whether IPV4 capability is enabled. /// public bool EnableIPV4 { get; set; } = true; -- cgit v1.2.3 From ceecc80bb3816ca41d470e3b537a1bf7b632e8e2 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 1 Nov 2020 18:32:41 -0700 Subject: Allow configuration of ActivityLogRetention --- .../ScheduledTasks/Tasks/CleanActivityLogTask.cs | 16 +++++++++++++--- MediaBrowser.Model/Configuration/ServerConfiguration.cs | 6 ++++++ 2 files changed, 19 insertions(+), 3 deletions(-) (limited to 'MediaBrowser.Model/Configuration') diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs index 50bc091c83..4abbf784b2 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Model.Activity; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Tasks; @@ -16,18 +17,22 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks { private readonly ILocalizationManager _localization; private readonly IActivityManager _activityManager; + private readonly IServerConfigurationManager _serverConfigurationManager; /// /// Initializes a new instance of the class. /// /// Instance of the interface. /// Instance of the interface. + /// Instance of the interface. public CleanActivityLogTask( ILocalizationManager localization, - IActivityManager activityManager) + IActivityManager activityManager, + IServerConfigurationManager serverConfigurationManager) { _localization = localization; _activityManager = activityManager; + _serverConfigurationManager = serverConfigurationManager; } /// @@ -54,8 +59,13 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks /// public Task Execute(CancellationToken cancellationToken, IProgress progress) { - // TODO allow configure - var startDate = DateTime.UtcNow.AddDays(-30); + var retentionDays = _serverConfigurationManager.Configuration.ActivityLogRetentionDays; + if (!retentionDays.HasValue || retentionDays <= 0) + { + throw new Exception($"Activity Log Retention days must be at least 0. Currently: {retentionDays}"); + } + + var startDate = DateTime.UtcNow.AddDays(retentionDays.Value * -1); return _activityManager.CleanAsync(startDate); } diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 8b78ad842e..23a5201f7a 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -271,6 +271,11 @@ namespace MediaBrowser.Model.Configuration /// public string[] KnownProxies { get; set; } + /// + /// Gets or sets the number of days we should retain activity logs. + /// + public int? ActivityLogRetentionDays { get; set; } + /// /// Initializes a new instance of the class. /// @@ -381,6 +386,7 @@ namespace MediaBrowser.Model.Configuration SlowResponseThresholdMs = 500; CorsHosts = new[] { "*" }; KnownProxies = Array.Empty(); + ActivityLogRetentionDays = 30; } } -- cgit v1.2.3 From f680a0fbbed346a5a5e98b2066604c463791190c Mon Sep 17 00:00:00 2001 From: Gary Wilber Date: Sun, 8 Nov 2020 11:55:38 -0800 Subject: fix merge --- MediaBrowser.Model/Configuration/ServerConfiguration.cs | 1 + 1 file changed, 1 insertion(+) (limited to 'MediaBrowser.Model/Configuration') diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 4d24e45b0c..be313bde1b 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -271,6 +271,7 @@ namespace MediaBrowser.Model.Configuration /// public string[] KnownProxies { get; set; } + /// /// Gets or sets the number of days we should retain activity logs. /// public int? ActivityLogRetentionDays { get; set; } -- cgit v1.2.3 From 5bd0c2b69d0f4fccd09866a67c741742710372dc Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Thu, 12 Nov 2020 11:02:56 +0800 Subject: add an option to disable hevc encoding --- Jellyfin.Api/Helpers/StreamingHelpers.cs | 4 +- Jellyfin.Api/Helpers/TranscodingJobHelper.cs | 3 +- .../MediaEncoding/EncodingHelper.cs | 43 +++++++++++++++++++++- .../Configuration/EncodingOptions.cs | 3 ++ 4 files changed, 50 insertions(+), 3 deletions(-) (limited to 'MediaBrowser.Model/Configuration') diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index 5bd347846d..be85d5eb83 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -165,7 +165,9 @@ namespace Jellyfin.Api.Helpers state.DirectStreamProvider = liveStreamInfo.Item2; } - encodingHelper.AttachMediaSourceInfo(state, mediaSource, url); + var encodingOptions = serverConfigurationManager.GetEncodingOptions(); + + encodingHelper.AttachMediaSourceInfo(state, encodingOptions, mediaSource, url); string? containerInternal = Path.GetExtension(state.RequestedUrl); diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index 0db1fabffe..f93c95fb55 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -770,8 +770,9 @@ namespace Jellyfin.Api.Helpers new LiveStreamRequest { OpenToken = state.MediaSource.OpenToken }, cancellationTokenSource.Token) .ConfigureAwait(false); + var encodingOptions = _serverConfigurationManager.GetEncodingOptions(); - _encodingHelper.AttachMediaSourceInfo(state, liveStreamResponse.MediaSource, state.RequestedUrl); + _encodingHelper.AttachMediaSourceInfo(state, encodingOptions, liveStreamResponse.MediaSource, state.RequestedUrl); if (state.VideoRequest != null) { diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index caf3ef1694..1074f876c7 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -2631,6 +2631,7 @@ namespace MediaBrowser.Controller.MediaEncoding public void AttachMediaSourceInfo( EncodingJobInfo state, + EncodingOptions encodingOptions, MediaSourceInfo mediaSource, string requestedUrl) { @@ -2761,11 +2762,23 @@ namespace MediaBrowser.Controller.MediaEncoding request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(i => _mediaEncoder.CanEncodeToAudioCodec(i)) ?? state.SupportedAudioCodecs.FirstOrDefault(); } + + var supportedVideoCodecs = state.SupportedVideoCodecs; + if (request != null && supportedVideoCodecs != null && supportedVideoCodecs.Length > 0) + { + var supportedVideoCodecsList = supportedVideoCodecs.ToList(); + + ShiftVideoCodecsIfNeeded(supportedVideoCodecsList, encodingOptions); + + state.SupportedVideoCodecs = supportedVideoCodecsList.ToArray(); + + request.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault(); + } } private void ShiftAudioCodecsIfNeeded(List audioCodecs, MediaStream audioStream) { - // Nothing to do here + // No need to shift if there is only one supported audio codec. if (audioCodecs.Count < 2) { return; @@ -2793,6 +2806,34 @@ namespace MediaBrowser.Controller.MediaEncoding } } + private void ShiftVideoCodecsIfNeeded(List videoCodecs, EncodingOptions encodingOptions) + { + // Shift hevc/h265 to the end of list if hevc encoding is not allowed. + if (encodingOptions.AllowHevcEncoding) + { + return; + } + + // No need to shift if there is only one supported video codec. + if (videoCodecs.Count < 2) + { + return; + } + + var shiftVideoCodecs = new[] { "hevc", "h265" }; + if (videoCodecs.All(i => shiftVideoCodecs.Contains(i, StringComparer.OrdinalIgnoreCase))) + { + return; + } + + while (shiftVideoCodecs.Contains(videoCodecs[0], StringComparer.OrdinalIgnoreCase)) + { + var removed = shiftVideoCodecs[0]; + videoCodecs.RemoveAt(0); + videoCodecs.Add(removed); + } + } + private void NormalizeSubtitleEmbed(EncodingJobInfo state) { if (state.SubtitleStream == null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Embed) diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index 2cd637c5b0..2d46889724 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -63,6 +63,8 @@ namespace MediaBrowser.Model.Configuration public bool EnableHardwareEncoding { get; set; } + public bool AllowHevcEncoding { get; set; } + public bool EnableSubtitleExtraction { get; set; } public string[] HardwareDecodingCodecs { get; set; } @@ -94,6 +96,7 @@ namespace MediaBrowser.Model.Configuration EnableDecodingColorDepth10Hevc = true; EnableDecodingColorDepth10Vp9 = true; EnableHardwareEncoding = true; + AllowHevcEncoding = true; EnableSubtitleExtraction = true; HardwareDecodingCodecs = new string[] { "h264", "vc1" }; } -- cgit v1.2.3 From 124bd4c2c03874f5b4de241e3d89ac95f6048b9a Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Sat, 21 Nov 2020 12:59:14 +0000 Subject: Networking: 1 - Network Manager (#4124) * NetworkManager * Config file with additional options. * Update Jellyfin.Networking/Manager/INetworkManager.cs Co-authored-by: Cody Robibero * Update Jellyfin.Networking/Manager/INetworkManager.cs Co-authored-by: Cody Robibero * Update MediaBrowser.Model/Configuration/ServerConfiguration.cs Co-authored-by: Cody Robibero * Update MediaBrowser.Model/Configuration/ServerConfiguration.cs Co-authored-by: Cody Robibero * Update MediaBrowser.Model/Configuration/ServerConfiguration.cs Co-authored-by: Cody Robibero * Update Jellyfin.Networking/Manager/NetworkManager.cs Co-authored-by: Cody Robibero * Split function. * Update Jellyfin.Networking/Manager/INetworkManager.cs Co-authored-by: Cody Robibero * fixed iterations * Update Jellyfin.Networking.csproj * Update NetworkManager.cs * Updated to NetCollection 1.03. * Update ServerConfiguration.cs * Update StartupController.cs * Update INetworkManager.cs Removed public * Update INetworkManager.cs * Update Jellyfin.Networking/Manager/NetworkManager.cs Co-authored-by: Claus Vium * Updated comment * Update Jellyfin.Networking/Manager/NetworkManager.cs Co-authored-by: Claus Vium * Update Jellyfin.Networking/Manager/NetworkManager.cs Co-authored-by: Claus Vium * Update Jellyfin.Networking/Manager/NetworkManager.cs Co-authored-by: Claus Vium * Update Jellyfin.Networking/Manager/NetworkManager.cs Co-authored-by: Claus Vium * Update Jellyfin.Networking/Manager/INetworkManager.cs Co-authored-by: Claus Vium * Remove mono code. Removed forced chromecast option * Inverted if * Update Jellyfin.Networking/Manager/NetworkManager.cs Co-authored-by: Cody Robibero * Update Jellyfin.Networking/Manager/NetworkManager.cs Co-authored-by: Cody Robibero * Update Jellyfin.Networking/Manager/NetworkManager.cs Co-authored-by: Cody Robibero * Update Jellyfin.Networking/Manager/NetworkManager.cs Co-authored-by: Cody Robibero * Update Jellyfin.Networking/Manager/NetworkManager.cs Co-authored-by: Cody Robibero * Update Jellyfin.Networking/Manager/NetworkManager.cs Co-authored-by: Cody Robibero * Moved config into a separate container * Update Jellyfin.Networking/Manager/NetworkManager.cs Co-authored-by: Cody Robibero * Changed sortedList to dictionary. * Update INetworkManager.cs Changed UpdateSettings param type * Update NetworkManager.cs * Update NetworkManager.cs * Update NetworkManager.cs * Update NetworkConfiguration.cs * Update INetworkManager.cs * Update Jellyfin.Networking/Manager/NetworkManager.cs Co-authored-by: Claus Vium * Update Jellyfin.Networking/Manager/NetworkManager.cs Co-authored-by: Claus Vium * Update MediaBrowser.Model/Configuration/ServerConfiguration.cs Co-authored-by: Cody Robibero * Update MediaBrowser.Model/Configuration/ServerConfiguration.cs Co-authored-by: Cody Robibero * Updated changes github didn't update. * Fixed compilation. * Update Jellyfin.Networking/Manager/NetworkManager.cs Co-authored-by: Cody Robibero * Update Jellyfin.Networking/Manager/NetworkManager.cs Co-authored-by: Cody Robibero * Update Jellyfin.Networking/Manager/NetworkManager.cs Co-authored-by: Cody Robibero * Removed read locking. * Update NetworkManager.cs Changed configuration event to NamedConfigurationUpdated * updated comment * removed whitespace * Updated NetCollection to 1.0.6 Updated DXCopAnalyser to 3.3.1 * removed NetCollection * Update NetworkManager.cs * Update NetworkExtensions.cs * Update NetworkExtensions.cs Removed function. * Update NetworkExtensions.cs * Update NetworkManager.cs Changed ToString() to AsString() as native collection formats incorrectly. * Update Jellyfin.Networking/Manager/NetworkManager.cs Co-authored-by: Cody Robibero * Update NetworkExtensions.cs * Update Jellyfin.Networking/Configuration/NetworkConfiguration.cs Co-authored-by: h1dden-da3m0n <33120068+h1dden-da3m0n@users.noreply.github.com> * Update Jellyfin.Networking/Manager/NetworkManager.cs Co-authored-by: h1dden-da3m0n <33120068+h1dden-da3m0n@users.noreply.github.com> * Update Jellyfin.Networking/Configuration/NetworkConfiguration.cs Co-authored-by: h1dden-da3m0n <33120068+h1dden-da3m0n@users.noreply.github.com> * Update Jellyfin.Networking/Configuration/NetworkConfiguration.cs Co-authored-by: h1dden-da3m0n <33120068+h1dden-da3m0n@users.noreply.github.com> * Update MediaBrowser.Common/Net/IPObject.cs Co-authored-by: h1dden-da3m0n <33120068+h1dden-da3m0n@users.noreply.github.com> * updated * Replaced NetCollection with Collection * Update MediaBrowser.Common/Net/NetworkExtensions.cs Co-authored-by: Cody Robibero * Update MediaBrowser.Model/Configuration/PathSubstitution.cs Co-authored-by: Cody Robibero * Update MediaBrowser.Common/Net/NetworkExtensions.cs Co-authored-by: Cody Robibero * Update Jellyfin.Networking/Manager/NetworkManager.cs Co-authored-by: Cody Robibero * Update Jellyfin.Networking/Manager/NetworkManager.cs Co-authored-by: Cody Robibero * Update Jellyfin.Networking/Manager/NetworkManager.cs Co-authored-by: Cody Robibero * Update Jellyfin.Networking/Manager/NetworkManager.cs Co-authored-by: Cody Robibero * Update Jellyfin.Networking/Manager/NetworkManager.cs Co-authored-by: Cody Robibero * Update MediaBrowser.Common/Net/IPObject.cs Co-authored-by: Cody Robibero * Update MediaBrowser.Common/Net/IPObject.cs Co-authored-by: Cody Robibero * Update MediaBrowser.Common/Net/IPObject.cs Co-authored-by: Cody Robibero * Update MediaBrowser.Common/Net/IPHost.cs Co-authored-by: Cody Robibero * Update MediaBrowser.Common/Net/IPHost.cs Co-authored-by: Cody Robibero * Update MediaBrowser.Common/Net/IPHost.cs Co-authored-by: Cody Robibero * Update MediaBrowser.Common/Net/IPHost.cs Co-authored-by: Cody Robibero * Update MediaBrowser.Common/Net/IPHost.cs Co-authored-by: Cody Robibero * Update MediaBrowser.Common/Net/IPHost.cs Co-authored-by: Cody Robibero * Update Jellyfin.Networking/Manager/NetworkManager.cs Co-authored-by: Cody Robibero * Update Jellyfin.Networking/Manager/NetworkManager.cs Co-authored-by: Cody Robibero * Update MediaBrowser.Common/Net/IPHost.cs Co-authored-by: Cody Robibero * Update MediaBrowser.Common/Net/IPObject.cs Co-authored-by: Cody Robibero * Update MediaBrowser.Common/Net/IPObject.cs Co-authored-by: Cody Robibero * updated comments. * Updated comments / changes as suggested by @crobibero. * Split function as suggested * Fixed null ref. * Updated comment. * Updated cs to .net5 * Fixed issue with PublishedServerUrl * Fixes * Update Jellyfin.Networking/Manager/NetworkManager.cs Co-authored-by: Claus Vium * Restored locking * optimisation * Added comment * updates. * updated. * updates * updated. * Update IPHost.cs * Update Jellyfin.Networking/Manager/NetworkManager.cs Co-authored-by: Claus Vium * Update NetworkManager.cs * Removed whitespace. * Added debug logging * Added debug. * Update Jellyfin.Networking/Manager/NetworkManager.cs Co-authored-by: Bond-009 * Replaced GetAddressBytes Co-authored-by: Cody Robibero Co-authored-by: Claus Vium Co-authored-by: h1dden-da3m0n <33120068+h1dden-da3m0n@users.noreply.github.com> Co-authored-by: Bond-009 --- .../Networking/NetworkManager.cs | 30 +- Jellyfin.Api/Controllers/StartupController.cs | 6 +- .../Configuration/NetworkConfiguration.cs | 221 ++++ .../NetworkConfigurationExtensions.cs | 21 + .../Configuration/NetworkConfigurationFactory.cs | 27 + Jellyfin.Networking/Jellyfin.Networking.csproj | 30 + Jellyfin.Networking/Manager/INetworkManager.cs | 234 ++++ Jellyfin.Networking/Manager/NetworkManager.cs | 1319 ++++++++++++++++++++ MediaBrowser.Common/Net/IPHost.cs | 445 +++++++ MediaBrowser.Common/Net/IPNetAddress.cs | 277 ++++ MediaBrowser.Common/Net/IPObject.cs | 406 ++++++ MediaBrowser.Common/Net/NetworkExtensions.cs | 262 ++++ .../Configuration/PathSubstitution.cs | 20 + .../Configuration/ServerConfiguration.cs | 410 +++--- MediaBrowser.sln | 34 +- 15 files changed, 3532 insertions(+), 210 deletions(-) create mode 100644 Jellyfin.Networking/Configuration/NetworkConfiguration.cs create mode 100644 Jellyfin.Networking/Configuration/NetworkConfigurationExtensions.cs create mode 100644 Jellyfin.Networking/Configuration/NetworkConfigurationFactory.cs create mode 100644 Jellyfin.Networking/Jellyfin.Networking.csproj create mode 100644 Jellyfin.Networking/Manager/INetworkManager.cs create mode 100644 Jellyfin.Networking/Manager/NetworkManager.cs create mode 100644 MediaBrowser.Common/Net/IPHost.cs create mode 100644 MediaBrowser.Common/Net/IPNetAddress.cs create mode 100644 MediaBrowser.Common/Net/IPObject.cs create mode 100644 MediaBrowser.Common/Net/NetworkExtensions.cs create mode 100644 MediaBrowser.Model/Configuration/PathSubstitution.cs (limited to 'MediaBrowser.Model/Configuration') diff --git a/Emby.Server.Implementations/Networking/NetworkManager.cs b/Emby.Server.Implementations/Networking/NetworkManager.cs index 089ec30e6b..ff0a2a3617 100644 --- a/Emby.Server.Implementations/Networking/NetworkManager.cs +++ b/Emby.Server.Implementations/Networking/NetworkManager.cs @@ -18,13 +18,12 @@ namespace Emby.Server.Implementations.Networking public class NetworkManager : INetworkManager { private readonly ILogger _logger; - - private IPAddress[] _localIpAddresses; private readonly object _localIpAddressSyncLock = new object(); - private readonly object _subnetLookupLock = new object(); private readonly Dictionary> _subnetLookup = new Dictionary>(StringComparer.Ordinal); + private IPAddress[] _localIpAddresses; + private List _macAddresses; /// @@ -157,7 +156,9 @@ namespace Emby.Server.Implementations.Networking return false; } - byte[] octet = ipAddress.GetAddressBytes(); + // GetAddressBytes + Span octet = stackalloc byte[ipAddress.AddressFamily == AddressFamily.InterNetwork ? 4 : 16]; + ipAddress.TryWriteBytes(octet, out _); if ((octet[0] == 10) || (octet[0] == 172 && (octet[1] >= 16 && octet[1] <= 31)) || // RFC1918 @@ -260,7 +261,9 @@ namespace Emby.Server.Implementations.Networking /// public bool IsAddressInSubnets(IPAddress address, bool excludeInterfaces, bool excludeRFC) { - byte[] octet = address.GetAddressBytes(); + // GetAddressBytes + Span octet = stackalloc byte[address.AddressFamily == AddressFamily.InterNetwork ? 4 : 16]; + address.TryWriteBytes(octet, out _); if ((octet[0] == 127) || // RFC1122 (octet[0] == 169 && octet[1] == 254)) // RFC3927 @@ -503,18 +506,25 @@ namespace Emby.Server.Implementations.Networking private IPAddress GetNetworkAddress(IPAddress address, IPAddress subnetMask) { - byte[] ipAdressBytes = address.GetAddressBytes(); - byte[] subnetMaskBytes = subnetMask.GetAddressBytes(); + int size = address.AddressFamily == AddressFamily.InterNetwork ? 4 : 16; + + // GetAddressBytes + Span ipAddressBytes = stackalloc byte[size]; + address.TryWriteBytes(ipAddressBytes, out _); + + // GetAddressBytes + Span subnetMaskBytes = stackalloc byte[size]; + subnetMask.TryWriteBytes(subnetMaskBytes, out _); - if (ipAdressBytes.Length != subnetMaskBytes.Length) + if (ipAddressBytes.Length != subnetMaskBytes.Length) { throw new ArgumentException("Lengths of IP address and subnet mask do not match."); } - byte[] broadcastAddress = new byte[ipAdressBytes.Length]; + byte[] broadcastAddress = new byte[ipAddressBytes.Length]; for (int i = 0; i < broadcastAddress.Length; i++) { - broadcastAddress[i] = (byte)(ipAdressBytes[i] & subnetMaskBytes[i]); + broadcastAddress[i] = (byte)(ipAddressBytes[i] & subnetMaskBytes[i]); } return new IPAddress(broadcastAddress); diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index 9c259cc198..e59c6e1ddf 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -72,9 +72,9 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult UpdateInitialConfiguration([FromBody, Required] StartupConfigurationDto startupConfiguration) { - _config.Configuration.UICulture = startupConfiguration.UICulture; - _config.Configuration.MetadataCountryCode = startupConfiguration.MetadataCountryCode; - _config.Configuration.PreferredMetadataLanguage = startupConfiguration.PreferredMetadataLanguage; + _config.Configuration.UICulture = startupConfiguration.UICulture ?? string.Empty; + _config.Configuration.MetadataCountryCode = startupConfiguration.MetadataCountryCode ?? string.Empty; + _config.Configuration.PreferredMetadataLanguage = startupConfiguration.PreferredMetadataLanguage ?? string.Empty; _config.SaveConfiguration(); return NoContent(); } diff --git a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs new file mode 100644 index 0000000000..df420f48a2 --- /dev/null +++ b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs @@ -0,0 +1,221 @@ +#pragma warning disable CA1819 // Properties should not return arrays + +using System; +using MediaBrowser.Model.Configuration; + +namespace Jellyfin.Networking.Configuration +{ + /// + /// Defines the . + /// + public class NetworkConfiguration + { + /// + /// The default value for . + /// + public const int DefaultHttpPort = 8096; + + /// + /// The default value for and . + /// + public const int DefaultHttpsPort = 8920; + + private string _baseUrl = string.Empty; + + /// + /// Gets or sets a value indicating whether the server should force connections over HTTPS. + /// + public bool RequireHttps { get; set; } + + /// + /// Gets or sets a value used to specify the URL prefix that your Jellyfin instance can be accessed at. + /// + public string BaseUrl + { + get => _baseUrl; + set + { + // Normalize the start of the string + if (string.IsNullOrWhiteSpace(value)) + { + // If baseUrl is empty, set an empty prefix string + _baseUrl = string.Empty; + return; + } + + if (value[0] != '/') + { + // If baseUrl was not configured with a leading slash, append one for consistency + value = "/" + value; + } + + // Normalize the end of the string + if (value[^1] == '/') + { + // If baseUrl was configured with a trailing slash, remove it for consistency + value = value.Remove(value.Length - 1); + } + + _baseUrl = value; + } + } + + /// + /// Gets or sets the public HTTPS port. + /// + /// The public HTTPS port. + public int PublicHttpsPort { get; set; } = DefaultHttpsPort; + + /// + /// Gets or sets the HTTP server port number. + /// + /// The HTTP server port number. + public int HttpServerPortNumber { get; set; } = DefaultHttpPort; + + /// + /// Gets or sets the HTTPS server port number. + /// + /// The HTTPS server port number. + public int HttpsPortNumber { get; set; } = DefaultHttpsPort; + + /// + /// Gets or sets a value indicating whether to use HTTPS. + /// + /// + /// In order for HTTPS to be used, in addition to setting this to true, valid values must also be + /// provided for and . + /// + public bool EnableHttps { get; set; } + + /// + /// Gets or sets the public mapped port. + /// + /// The public mapped port. + public int PublicPort { get; set; } = DefaultHttpPort; + + /// + /// Gets or sets a value indicating whether the http port should be mapped as part of UPnP automatic port forwarding. + /// + public bool UPnPCreateHttpPortMap { get; set; } + + /// + /// Gets or sets the UDPPortRange. + /// + public string UDPPortRange { get; set; } = string.Empty; + + /// + /// Gets or sets a value indicating whether gets or sets IPV6 capability. + /// + public bool EnableIPV6 { get; set; } + + /// + /// Gets or sets a value indicating whether gets or sets IPV4 capability. + /// + public bool EnableIPV4 { get; set; } = true; + + /// + /// Gets or sets a value indicating whether detailed SSDP logs are sent to the console/log. + /// "Emby.Dlna": "Debug" must be set in logging.default.json for this property to have any effect. + /// + public bool EnableSSDPTracing { get; set; } + + /// + /// Gets or sets the SSDPTracingFilter + /// Gets or sets a value indicating whether an IP address is to be used to filter the detailed ssdp logs that are being sent to the console/log. + /// If the setting "Emby.Dlna": "Debug" msut be set in logging.default.json for this property to work. + /// + public string SSDPTracingFilter { get; set; } = string.Empty; + + /// + /// Gets or sets the number of times SSDP UDP messages are sent. + /// + public int UDPSendCount { get; set; } = 2; + + /// + /// Gets or sets the delay between each groups of SSDP messages (in ms). + /// + public int UDPSendDelay { get; set; } = 100; + + /// + /// Gets or sets a value indicating whether address names that match should be Ignore for the purposes of binding. + /// + public bool IgnoreVirtualInterfaces { get; set; } = true; + + /// + /// Gets or sets a value indicating the interfaces that should be ignored. The list can be comma separated. . + /// + public string VirtualInterfaceNames { get; set; } = "vEthernet*"; + + /// + /// Gets or sets the time (in seconds) between the pings of SSDP gateway monitor. + /// + public int GatewayMonitorPeriod { get; set; } = 60; + + /// + /// Gets a value indicating whether multi-socket binding is available. + /// + public bool EnableMultiSocketBinding { get; } = true; + + /// + /// Gets or sets a value indicating whether all IPv6 interfaces should be treated as on the internal network. + /// Depending on the address range implemented ULA ranges might not be used. + /// + public bool TrustAllIP6Interfaces { get; set; } + + /// + /// Gets or sets the ports that HDHomerun uses. + /// + public string HDHomerunPortRange { get; set; } = string.Empty; + + /// + /// Gets or sets the PublishedServerUriBySubnet + /// Gets or sets PublishedServerUri to advertise for specific subnets. + /// + public string[] PublishedServerUriBySubnet { get; set; } = Array.Empty(); + + /// + /// Gets or sets a value indicating whether Autodiscovery tracing is enabled. + /// + public bool AutoDiscoveryTracing { get; set; } + + /// + /// Gets or sets a value indicating whether Autodiscovery is enabled. + /// + public bool AutoDiscovery { get; set; } = true; + + /// + /// Gets or sets the filter for remote IP connectivity. Used in conjuntion with . + /// + public string[] RemoteIPFilter { get; set; } = Array.Empty(); + + /// + /// Gets or sets a value indicating whether contains a blacklist or a whitelist. Default is a whitelist. + /// + public bool IsRemoteIPFilterBlacklist { get; set; } + + /// + /// Gets or sets a value indicating whether to enable automatic port forwarding. + /// + public bool EnableUPnP { get; set; } + + /// + /// Gets or sets a value indicating whether access outside of the LAN is permitted. + /// + public bool EnableRemoteAccess { get; set; } = true; + + /// + /// Gets or sets the subnets that are deemed to make up the LAN. + /// + public string[] LocalNetworkSubnets { get; set; } = Array.Empty(); + + /// + /// Gets or sets the interface addresses which Jellyfin will bind to. If empty, all interfaces will be used. + /// + public string[] LocalNetworkAddresses { get; set; } = Array.Empty(); + + /// + /// Gets or sets the known proxies. + /// + public string[] KnownProxies { get; set; } = Array.Empty(); + } +} diff --git a/Jellyfin.Networking/Configuration/NetworkConfigurationExtensions.cs b/Jellyfin.Networking/Configuration/NetworkConfigurationExtensions.cs new file mode 100644 index 0000000000..e77b17ba92 --- /dev/null +++ b/Jellyfin.Networking/Configuration/NetworkConfigurationExtensions.cs @@ -0,0 +1,21 @@ +using Jellyfin.Networking.Configuration; +using MediaBrowser.Common.Configuration; + +namespace Jellyfin.Networking.Configuration +{ + /// + /// Defines the . + /// + public static class NetworkConfigurationExtensions + { + /// + /// Retrieves the network configuration. + /// + /// The . + /// The . + public static NetworkConfiguration GetNetworkConfiguration(this IConfigurationManager config) + { + return config.GetConfiguration("network"); + } + } +} diff --git a/Jellyfin.Networking/Configuration/NetworkConfigurationFactory.cs b/Jellyfin.Networking/Configuration/NetworkConfigurationFactory.cs new file mode 100644 index 0000000000..ac0485d871 --- /dev/null +++ b/Jellyfin.Networking/Configuration/NetworkConfigurationFactory.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using MediaBrowser.Common.Configuration; + +namespace Jellyfin.Networking.Configuration +{ + /// + /// Defines the . + /// + public class NetworkConfigurationFactory : IConfigurationFactory + { + /// + /// The GetConfigurations. + /// + /// The . + public IEnumerable GetConfigurations() + { + return new[] + { + new ConfigurationStore + { + Key = "network", + ConfigurationType = typeof(NetworkConfiguration) + } + }; + } + } +} diff --git a/Jellyfin.Networking/Jellyfin.Networking.csproj b/Jellyfin.Networking/Jellyfin.Networking.csproj new file mode 100644 index 0000000000..cbda74361f --- /dev/null +++ b/Jellyfin.Networking/Jellyfin.Networking.csproj @@ -0,0 +1,30 @@ + + + net5.0 + false + true + true + enable + + + + + + + + + + + + + + + + ../jellyfin.ruleset + + + + + + + diff --git a/Jellyfin.Networking/Manager/INetworkManager.cs b/Jellyfin.Networking/Manager/INetworkManager.cs new file mode 100644 index 0000000000..eababa6a90 --- /dev/null +++ b/Jellyfin.Networking/Manager/INetworkManager.cs @@ -0,0 +1,234 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Net; +using System.Net.NetworkInformation; +using Jellyfin.Networking.Configuration; +using MediaBrowser.Common.Net; +using Microsoft.AspNetCore.Http; + +namespace Jellyfin.Networking.Manager +{ + /// + /// Interface for the NetworkManager class. + /// + public interface INetworkManager + { + /// + /// Event triggered on network changes. + /// + event EventHandler NetworkChanged; + + /// + /// Gets the published server urls list. + /// + Dictionary PublishedServerUrls { get; } + + /// + /// Gets a value indicating whether is all IPv6 interfaces are trusted as internal. + /// + bool TrustAllIP6Interfaces { get; } + + /// + /// Gets the remote address filter. + /// + Collection RemoteAddressFilter { get; } + + /// + /// Gets or sets a value indicating whether iP6 is enabled. + /// + bool IsIP6Enabled { get; set; } + + /// + /// Gets or sets a value indicating whether iP4 is enabled. + /// + bool IsIP4Enabled { get; set; } + + /// + /// Calculates the list of interfaces to use for Kestrel. + /// + /// A Collection{IPObject} object containing all the interfaces to bind. + /// If all the interfaces are specified, and none are excluded, it returns zero items + /// to represent any address. + /// When false, return or for all interfaces. + Collection GetAllBindInterfaces(bool individualInterfaces = false); + + /// + /// Returns a collection containing the loopback interfaces. + /// + /// Collection{IPObject}. + Collection GetLoopbacks(); + + /// + /// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo) + /// If no bind addresses are specified, an internal interface address is selected. + /// The priority of selection is as follows:- + /// + /// The value contained in the startup parameter --published-server-url. + /// + /// If the user specified custom subnet overrides, the correct subnet for the source address. + /// + /// If the user specified bind interfaces to use:- + /// The bind interface that contains the source subnet. + /// The first bind interface specified that suits best first the source's endpoint. eg. external or internal. + /// + /// If the source is from a public subnet address range and the user hasn't specified any bind addresses:- + /// The first public interface that isn't a loopback and contains the source subnet. + /// The first public interface that isn't a loopback. Priority is given to interfaces with gateways. + /// An internal interface if there are no public ip addresses. + /// + /// If the source is from a private subnet address range and the user hasn't specified any bind addresses:- + /// The first private interface that contains the source subnet. + /// The first private interface that isn't a loopback. Priority is given to interfaces with gateways. + /// + /// If no interfaces meet any of these criteria, then a loopback address is returned. + /// + /// Interface that have been specifically excluded from binding are not used in any of the calculations. + /// + /// Source of the request. + /// Optional port returned, if it's part of an override. + /// IP Address to use, or loopback address if all else fails. + string GetBindInterface(IPObject source, out int? port); + + /// + /// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo) + /// If no bind addresses are specified, an internal interface address is selected. + /// (See . + /// + /// Source of the request. + /// Optional port returned, if it's part of an override. + /// IP Address to use, or loopback address if all else fails. + string GetBindInterface(HttpRequest source, out int? port); + + /// + /// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo) + /// If no bind addresses are specified, an internal interface address is selected. + /// (See . + /// + /// IP address of the request. + /// Optional port returned, if it's part of an override. + /// IP Address to use, or loopback address if all else fails. + string GetBindInterface(IPAddress source, out int? port); + + /// + /// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo) + /// If no bind addresses are specified, an internal interface address is selected. + /// (See . + /// + /// Source of the request. + /// Optional port returned, if it's part of an override. + /// IP Address to use, or loopback address if all else fails. + string GetBindInterface(string source, out int? port); + + /// + /// Checks to see if the ip address is specifically excluded in LocalNetworkAddresses. + /// + /// IP address to check. + /// True if it is. + bool IsExcludedInterface(IPAddress address); + + /// + /// Get a list of all the MAC addresses associated with active interfaces. + /// + /// List of MAC addresses. + IReadOnlyCollection GetMacAddresses(); + + /// + /// Checks to see if the IP Address provided matches an interface that has a gateway. + /// + /// IP to check. Can be an IPAddress or an IPObject. + /// Result of the check. + bool IsGatewayInterface(IPObject? addressObj); + + /// + /// Checks to see if the IP Address provided matches an interface that has a gateway. + /// + /// IP to check. Can be an IPAddress or an IPObject. + /// Result of the check. + bool IsGatewayInterface(IPAddress? addressObj); + + /// + /// Returns true if the address is a private address. + /// The config option TrustIP6Interfaces overrides this functions behaviour. + /// + /// Address to check. + /// True or False. + bool IsPrivateAddressRange(IPObject address); + + /// + /// Returns true if the address is part of the user defined LAN. + /// The config option TrustIP6Interfaces overrides this functions behaviour. + /// + /// IP to check. + /// True if endpoint is within the LAN range. + bool IsInLocalNetwork(string address); + + /// + /// Returns true if the address is part of the user defined LAN. + /// The config option TrustIP6Interfaces overrides this functions behaviour. + /// + /// IP to check. + /// True if endpoint is within the LAN range. + bool IsInLocalNetwork(IPObject address); + + /// + /// Returns true if the address is part of the user defined LAN. + /// The config option TrustIP6Interfaces overrides this functions behaviour. + /// + /// IP to check. + /// True if endpoint is within the LAN range. + bool IsInLocalNetwork(IPAddress address); + + /// + /// Attempts to convert the token to an IP address, permitting for interface descriptions and indexes. + /// eg. "eth1", or "TP-LINK Wireless USB Adapter". + /// + /// Token to parse. + /// Resultant object's ip addresses, if successful. + /// Success of the operation. + bool TryParseInterface(string token, out Collection? result); + + /// + /// Parses an array of strings into a Collection{IPObject}. + /// + /// Values to parse. + /// When true, only include values in []. When false, ignore bracketed values. + /// IPCollection object containing the value strings. + Collection CreateIPCollection(string[] values, bool bracketed = false); + + /// + /// Returns all the internal Bind interface addresses. + /// + /// An internal list of interfaces addresses. + Collection GetInternalBindAddresses(); + + /// + /// Checks to see if an IP address is still a valid interface address. + /// + /// IP address to check. + /// True if it is. + bool IsValidInterfaceAddress(IPAddress address); + + /// + /// Returns true if the IP address is in the excluded list. + /// + /// IP to check. + /// True if excluded. + bool IsExcluded(IPAddress ip); + + /// + /// Returns true if the IP address is in the excluded list. + /// + /// IP to check. + /// True if excluded. + bool IsExcluded(EndPoint ip); + + /// + /// Gets the filtered LAN ip addresses. + /// + /// Optional filter for the list. + /// Returns a filtered list of LAN addresses. + Collection GetFilteredLANSubnets(Collection? filter = null); + } +} diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs new file mode 100644 index 0000000000..515ae669a1 --- /dev/null +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -0,0 +1,1319 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Threading.Tasks; +using Jellyfin.Networking.Configuration; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Networking.Manager +{ + /// + /// Class to take care of network interface management. + /// Note: The normal collection methods and properties will not work with Collection{IPObject}. . + /// + public class NetworkManager : INetworkManager, IDisposable + { + /// + /// Contains the description of the interface along with its index. + /// + private readonly Dictionary _interfaceNames; + + /// + /// Threading lock for network properties. + /// + private readonly object _intLock = new object(); + + /// + /// List of all interface addresses and masks. + /// + private readonly Collection _interfaceAddresses; + + /// + /// List of all interface MAC addresses. + /// + private readonly List _macAddresses; + + private readonly ILogger _logger; + + private readonly IConfigurationManager _configurationManager; + + private readonly object _eventFireLock; + + /// + /// Holds the bind address overrides. + /// + private readonly Dictionary _publishedServerUrls; + + /// + /// Used to stop "event-racing conditions". + /// + private bool _eventfire; + + /// + /// Unfiltered user defined LAN subnets. () + /// or internal interface network subnets if undefined by user. + /// + private Collection _lanSubnets; + + /// + /// User defined list of subnets to excluded from the LAN. + /// + private Collection _excludedSubnets; + + /// + /// List of interface addresses to bind the WS. + /// + private Collection _bindAddresses; + + /// + /// List of interface addresses to exclude from bind. + /// + private Collection _bindExclusions; + + /// + /// Caches list of all internal filtered interface addresses and masks. + /// + private Collection _internalInterfaces; + + /// + /// Flag set when no custom LAN has been defined in the config. + /// + private bool _usingPrivateAddresses; + + /// + /// True if this object is disposed. + /// + private bool _disposed; + + /// + /// Initializes a new instance of the class. + /// + /// IServerConfigurationManager instance. + /// Logger to use for messages. +#pragma warning disable CS8618 // Non-nullable field is uninitialized. : Values are set in UpdateSettings function. Compiler doesn't yet recognise this. + public NetworkManager(IConfigurationManager configurationManager, ILogger logger) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _configurationManager = configurationManager ?? throw new ArgumentNullException(nameof(configurationManager)); + + _interfaceAddresses = new Collection(); + _macAddresses = new List(); + _interfaceNames = new Dictionary(); + _publishedServerUrls = new Dictionary(); + _eventFireLock = new object(); + + UpdateSettings(_configurationManager.GetNetworkConfiguration()); + + NetworkChange.NetworkAddressChanged += OnNetworkAddressChanged; + NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged; + + _configurationManager.NamedConfigurationUpdated += ConfigurationUpdated; + } +#pragma warning restore CS8618 // Non-nullable field is uninitialized. + + /// + /// Event triggered on network changes. + /// + public event EventHandler? NetworkChanged; + + /// + /// Gets or sets a value indicating whether testing is taking place. + /// + public static string MockNetworkSettings { get; set; } = string.Empty; + + /// + /// Gets or sets a value indicating whether IP6 is enabled. + /// + public bool IsIP6Enabled { get; set; } + + /// + /// Gets or sets a value indicating whether IP4 is enabled. + /// + public bool IsIP4Enabled { get; set; } + + /// + public Collection RemoteAddressFilter { get; private set; } + + /// + /// Gets a value indicating whether is all IPv6 interfaces are trusted as internal. + /// + public bool TrustAllIP6Interfaces { get; internal set; } + + /// + /// Gets the Published server override list. + /// + public Dictionary PublishedServerUrls => _publishedServerUrls; + + /// + /// Creates a new network collection. + /// + /// Items to assign the collection, or null. + /// The collection created. + public static Collection CreateCollection(IEnumerable? source = null) + { + var result = new Collection(); + if (source != null) + { + foreach (var item in source) + { + result.AddItem(item); + } + } + + return result; + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + public IReadOnlyCollection GetMacAddresses() + { + // Populated in construction - so always has values. + return _macAddresses; + } + + /// + public bool IsGatewayInterface(IPObject? addressObj) + { + var address = addressObj?.Address ?? IPAddress.None; + return _internalInterfaces.Any(i => i.Address.Equals(address) && i.Tag < 0); + } + + /// + public bool IsGatewayInterface(IPAddress? addressObj) + { + return _internalInterfaces.Any(i => i.Address.Equals(addressObj ?? IPAddress.None) && i.Tag < 0); + } + + /// + public Collection GetLoopbacks() + { + Collection nc = new Collection(); + if (IsIP4Enabled) + { + nc.AddItem(IPAddress.Loopback); + } + + if (IsIP6Enabled) + { + nc.AddItem(IPAddress.IPv6Loopback); + } + + return nc; + } + + /// + public bool IsExcluded(IPAddress ip) + { + return _excludedSubnets.ContainsAddress(ip); + } + + /// + public bool IsExcluded(EndPoint ip) + { + return ip != null && IsExcluded(((IPEndPoint)ip).Address); + } + + /// + public Collection CreateIPCollection(string[] values, bool bracketed = false) + { + Collection col = new Collection(); + if (values == null) + { + return col; + } + + for (int a = 0; a < values.Length; a++) + { + string v = values[a].Trim(); + + try + { + if (v.StartsWith('[') && v.EndsWith(']')) + { + if (bracketed) + { + AddToCollection(col, v[1..^1]); + } + } + else if (v.StartsWith('!')) + { + if (bracketed) + { + AddToCollection(col, v[1..]); + } + } + else if (!bracketed) + { + AddToCollection(col, v); + } + } + catch (ArgumentException e) + { + _logger.LogWarning(e, "Ignoring LAN value {value}.", v); + } + } + + return col; + } + + /// + public Collection GetAllBindInterfaces(bool individualInterfaces = false) + { + int count = _bindAddresses.Count; + + if (count == 0) + { + if (_bindExclusions.Count > 0) + { + // Return all the interfaces except the ones specifically excluded. + return _interfaceAddresses.Exclude(_bindExclusions); + } + + if (individualInterfaces) + { + return new Collection(_interfaceAddresses); + } + + // No bind address and no exclusions, so listen on all interfaces. + Collection result = new Collection(); + + if (IsIP4Enabled) + { + result.AddItem(IPAddress.Any); + } + + if (IsIP6Enabled) + { + result.AddItem(IPAddress.IPv6Any); + } + + return result; + } + + // Remove any excluded bind interfaces. + return _bindAddresses.Exclude(_bindExclusions); + } + + /// + public string GetBindInterface(string source, out int? port) + { + if (!string.IsNullOrEmpty(source) && IPHost.TryParse(source, out IPHost host)) + { + return GetBindInterface(host, out port); + } + + return GetBindInterface(IPHost.None, out port); + } + + /// + public string GetBindInterface(IPAddress source, out int? port) + { + return GetBindInterface(new IPNetAddress(source), out port); + } + + /// + public string GetBindInterface(HttpRequest source, out int? port) + { + string result; + + if (source != null && IPHost.TryParse(source.Host.Host, out IPHost host)) + { + result = GetBindInterface(host, out port); + port ??= source.Host.Port; + } + else + { + result = GetBindInterface(IPNetAddress.None, out port); + port ??= source?.Host.Port; + } + + return result; + } + + /// + public string GetBindInterface(IPObject source, out int? port) + { + port = null; + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + // Do we have a source? + bool haveSource = !source.Address.Equals(IPAddress.None); + bool isExternal = false; + + if (haveSource) + { + if (!IsIP6Enabled && source.AddressFamily == AddressFamily.InterNetworkV6) + { + _logger.LogWarning("IPv6 is disabled in Jellyfin, but enabled in the OS. This may affect how the interface is selected."); + } + + if (!IsIP4Enabled && source.AddressFamily == AddressFamily.InterNetwork) + { + _logger.LogWarning("IPv4 is disabled in Jellyfin, but enabled in the OS. This may affect how the interface is selected."); + } + + isExternal = !IsInLocalNetwork(source); + + if (MatchesPublishedServerUrl(source, isExternal, out string res, out port)) + { + _logger.LogInformation("{Source}: Using BindAddress {Address}:{Port}", source, res, port); + return res; + } + } + + _logger.LogDebug("GetBindInterface: Source: {HaveSource}, External: {IsExternal}:", haveSource, isExternal); + + // No preference given, so move on to bind addresses. + if (MatchesBindInterface(source, isExternal, out string result)) + { + return result; + } + + if (isExternal && MatchesExternalInterface(source, out result)) + { + return result; + } + + // Get the first LAN interface address that isn't a loopback. + var interfaces = CreateCollection(_interfaceAddresses + .Exclude(_bindExclusions) + .Where(p => IsInLocalNetwork(p)) + .OrderBy(p => p.Tag)); + + if (interfaces.Count > 0) + { + if (haveSource) + { + // Does the request originate in one of the interface subnets? + // (For systems with multiple internal network cards, and multiple subnets) + foreach (var intf in interfaces) + { + if (intf.Contains(source)) + { + result = FormatIP6String(intf.Address); + _logger.LogDebug("{Source}: GetBindInterface: Has source, matched best internal interface on range. {Result}", source, result); + return result; + } + } + } + + result = FormatIP6String(interfaces.First().Address); + _logger.LogDebug("{Source}: GetBindInterface: Matched first internal interface. {Result}", source, result); + return result; + } + + // There isn't any others, so we'll use the loopback. + result = IsIP6Enabled ? "::" : "127.0.0.1"; + _logger.LogWarning("{Source}: GetBindInterface: Loopback {Result} returned.", source, result); + return result; + } + + /// + public Collection GetInternalBindAddresses() + { + int count = _bindAddresses.Count; + + if (count == 0) + { + if (_bindExclusions.Count > 0) + { + // Return all the internal interfaces except the ones excluded. + return CreateCollection(_internalInterfaces.Where(p => !_bindExclusions.ContainsAddress(p))); + } + + // No bind address, so return all internal interfaces. + return CreateCollection(_internalInterfaces.Where(p => !p.IsLoopback())); + } + + return new Collection(_bindAddresses); + } + + /// + public bool IsInLocalNetwork(IPObject address) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + if (address.Equals(IPAddress.None)) + { + return false; + } + + // See conversation at https://github.com/jellyfin/jellyfin/pull/3515. + if (TrustAllIP6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6) + { + return true; + } + + // As private addresses can be redefined by Configuration.LocalNetworkAddresses + return _lanSubnets.ContainsAddress(address) && !_excludedSubnets.ContainsAddress(address); + } + + /// + public bool IsInLocalNetwork(string address) + { + if (IPHost.TryParse(address, out IPHost ep)) + { + return _lanSubnets.ContainsAddress(ep) && !_excludedSubnets.ContainsAddress(ep); + } + + return false; + } + + /// + public bool IsInLocalNetwork(IPAddress address) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + // See conversation at https://github.com/jellyfin/jellyfin/pull/3515. + if (TrustAllIP6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6) + { + return true; + } + + // As private addresses can be redefined by Configuration.LocalNetworkAddresses + return _lanSubnets.ContainsAddress(address) && !_excludedSubnets.ContainsAddress(address); + } + + /// + public bool IsPrivateAddressRange(IPObject address) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + // See conversation at https://github.com/jellyfin/jellyfin/pull/3515. + if (TrustAllIP6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6) + { + return true; + } + else + { + return address.IsPrivateAddressRange(); + } + } + + /// + public bool IsExcludedInterface(IPAddress address) + { + return _bindExclusions.ContainsAddress(address); + } + + /// + public Collection GetFilteredLANSubnets(Collection? filter = null) + { + if (filter == null) + { + return _lanSubnets.Exclude(_excludedSubnets).AsNetworks(); + } + + return _lanSubnets.Exclude(filter); + } + + /// + public bool IsValidInterfaceAddress(IPAddress address) + { + return _interfaceAddresses.ContainsAddress(address); + } + + /// + public bool TryParseInterface(string token, out Collection? result) + { + result = null; + if (string.IsNullOrEmpty(token)) + { + return false; + } + + if (_interfaceNames != null && _interfaceNames.TryGetValue(token.ToLower(CultureInfo.InvariantCulture), out int index)) + { + result = new Collection(); + + _logger.LogInformation("Interface {Token} used in settings. Using its interface addresses.", token); + + // Replace interface tags with the interface IP's. + foreach (IPNetAddress iface in _interfaceAddresses) + { + if (Math.Abs(iface.Tag) == index + && ((IsIP4Enabled && iface.Address.AddressFamily == AddressFamily.InterNetwork) + || (IsIP6Enabled && iface.Address.AddressFamily == AddressFamily.InterNetworkV6))) + { + result.AddItem(iface); + } + } + + return true; + } + + return false; + } + + /// + /// Reloads all settings and re-initialises the instance. + /// + /// The to use. + public void UpdateSettings(object configuration) + { + NetworkConfiguration config = (NetworkConfiguration)configuration ?? throw new ArgumentNullException(nameof(configuration)); + + IsIP4Enabled = Socket.OSSupportsIPv4 && config.EnableIPV4; + IsIP6Enabled = Socket.OSSupportsIPv6 && config.EnableIPV6; + + if (!IsIP6Enabled && !IsIP4Enabled) + { + _logger.LogError("IPv4 and IPv6 cannot both be disabled."); + IsIP4Enabled = true; + } + + TrustAllIP6Interfaces = config.TrustAllIP6Interfaces; + // UdpHelper.EnableMultiSocketBinding = config.EnableMultiSocketBinding; + + if (string.IsNullOrEmpty(MockNetworkSettings)) + { + InitialiseInterfaces(); + } + else // Used in testing only. + { + // Format is ,,: . Set index to -ve to simulate a gateway. + var interfaceList = MockNetworkSettings.Split(':'); + foreach (var details in interfaceList) + { + var parts = details.Split(','); + var address = IPNetAddress.Parse(parts[0]); + var index = int.Parse(parts[1], CultureInfo.InvariantCulture); + address.Tag = index; + _interfaceAddresses.AddItem(address); + _interfaceNames.Add(parts[2], Math.Abs(index)); + } + } + + InitialiseLAN(config); + InitialiseBind(config); + InitialiseRemote(config); + InitialiseOverrides(config); + } + + /// + /// Protected implementation of Dispose pattern. + /// + /// True to dispose the managed state. + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _configurationManager.NamedConfigurationUpdated -= ConfigurationUpdated; + NetworkChange.NetworkAddressChanged -= OnNetworkAddressChanged; + NetworkChange.NetworkAvailabilityChanged -= OnNetworkAvailabilityChanged; + } + + _disposed = true; + } + } + + /// + /// Tries to identify the string and return an object of that class. + /// + /// String to parse. + /// IPObject to return. + /// true if the value parsed successfully, false otherwise. + private static bool TryParse(string addr, out IPObject result) + { + if (!string.IsNullOrEmpty(addr)) + { + // Is it an IP address + if (IPNetAddress.TryParse(addr, out IPNetAddress nw)) + { + result = nw; + return true; + } + + if (IPHost.TryParse(addr, out IPHost h)) + { + result = h; + return true; + } + } + + result = IPNetAddress.None; + return false; + } + + /// + /// Converts an IPAddress into a string. + /// Ipv6 addresses are returned in [ ], with their scope removed. + /// + /// Address to convert. + /// URI safe conversion of the address. + private static string FormatIP6String(IPAddress address) + { + var str = address.ToString(); + if (address.AddressFamily == AddressFamily.InterNetworkV6) + { + int i = str.IndexOf("%", StringComparison.OrdinalIgnoreCase); + + if (i != -1) + { + str = str.Substring(0, i); + } + + return $"[{str}]"; + } + + return str; + } + + private void ConfigurationUpdated(object? sender, ConfigurationUpdateEventArgs evt) + { + if (evt.Key.Equals("network", StringComparison.Ordinal)) + { + UpdateSettings((NetworkConfiguration)evt.NewConfiguration); + } + } + + /// + /// Checks the string to see if it matches any interface names. + /// + /// String to check. + /// Interface index number. + /// true if an interface name matches the token, False otherwise. + private bool IsInterface(string token, out int index) + { + index = -1; + + // Is it the name of an interface (windows) eg, Wireless LAN adapter Wireless Network Connection 1. + // Null check required here for automated testing. + if (_interfaceNames != null && token.Length > 1) + { + bool partial = token[^1] == '*'; + if (partial) + { + token = token[0..^1]; + } + + foreach ((string interfc, int interfcIndex) in _interfaceNames) + { + if ((!partial && string.Equals(interfc, token, StringComparison.OrdinalIgnoreCase)) + || (partial && interfc.StartsWith(token, true, CultureInfo.InvariantCulture))) + { + index = interfcIndex; + return true; + } + } + } + + return false; + } + + /// + /// Parses a string and adds it into the the collection, replacing any interface references. + /// + /// Collection. + /// String value to parse. + private void AddToCollection(Collection col, string token) + { + // Is it the name of an interface (windows) eg, Wireless LAN adapter Wireless Network Connection 1. + // Null check required here for automated testing. + if (IsInterface(token, out int index)) + { + _logger.LogInformation("Interface {Token} used in settings. Using its interface addresses.", token); + + // Replace interface tags with the interface IP's. + foreach (IPNetAddress iface in _interfaceAddresses) + { + if (Math.Abs(iface.Tag) == index + && ((IsIP4Enabled && iface.Address.AddressFamily == AddressFamily.InterNetwork) + || (IsIP6Enabled && iface.Address.AddressFamily == AddressFamily.InterNetworkV6))) + { + col.AddItem(iface); + } + } + } + else if (TryParse(token, out IPObject obj)) + { + if (!IsIP6Enabled) + { + // Remove IP6 addresses from multi-homed IPHosts. + obj.Remove(AddressFamily.InterNetworkV6); + if (!obj.IsIP6()) + { + col.AddItem(obj); + } + } + else if (!IsIP4Enabled) + { + // Remove IP4 addresses from multi-homed IPHosts. + obj.Remove(AddressFamily.InterNetwork); + if (obj.IsIP6()) + { + col.AddItem(obj); + } + } + else + { + col.AddItem(obj); + } + } + else + { + _logger.LogDebug("Invalid or unknown network {Token}.", token); + } + } + + /// + /// Handler for network change events. + /// + /// Sender. + /// A containing network availability information. + private void OnNetworkAvailabilityChanged(object? sender, NetworkAvailabilityEventArgs e) + { + _logger.LogDebug("Network availability changed."); + OnNetworkChanged(); + } + + /// + /// Handler for network change events. + /// + /// Sender. + /// An . + private void OnNetworkAddressChanged(object? sender, EventArgs e) + { + _logger.LogDebug("Network address change detected."); + OnNetworkChanged(); + } + + /// + /// Async task that waits for 2 seconds before re-initialising the settings, as typically these events fire multiple times in succession. + /// + /// A representing the asynchronous operation. + private async Task OnNetworkChangeAsync() + { + try + { + await Task.Delay(2000).ConfigureAwait(false); + InitialiseInterfaces(); + // Recalculate LAN caches. + InitialiseLAN(_configurationManager.GetNetworkConfiguration()); + + NetworkChanged?.Invoke(this, EventArgs.Empty); + } + finally + { + _eventfire = false; + } + } + + /// + /// Triggers our event, and re-loads interface information. + /// + private void OnNetworkChanged() + { + lock (_eventFireLock) + { + if (!_eventfire) + { + _logger.LogDebug("Network Address Change Event."); + // As network events tend to fire one after the other only fire once every second. + _eventfire = true; + OnNetworkChangeAsync().GetAwaiter().GetResult(); + } + } + } + + /// + /// Parses the user defined overrides into the dictionary object. + /// Overrides are the equivalent of localised publishedServerUrl, enabling + /// different addresses to be advertised over different subnets. + /// format is subnet=ipaddress|host|uri + /// when subnet = 0.0.0.0, any external address matches. + /// + private void InitialiseOverrides(NetworkConfiguration config) + { + lock (_intLock) + { + _publishedServerUrls.Clear(); + string[] overrides = config.PublishedServerUriBySubnet; + if (overrides == null) + { + return; + } + + foreach (var entry in overrides) + { + var parts = entry.Split('='); + if (parts.Length != 2) + { + _logger.LogError("Unable to parse bind override: {Entry}", entry); + } + else + { + var replacement = parts[1].Trim(); + if (string.Equals(parts[0], "remaining", StringComparison.OrdinalIgnoreCase)) + { + _publishedServerUrls[new IPNetAddress(IPAddress.Broadcast)] = replacement; + } + else if (string.Equals(parts[0], "external", StringComparison.OrdinalIgnoreCase)) + { + _publishedServerUrls[new IPNetAddress(IPAddress.Any)] = replacement; + } + else if (TryParseInterface(parts[0], out Collection? addresses) && addresses != null) + { + foreach (IPNetAddress na in addresses) + { + _publishedServerUrls[na] = replacement; + } + } + else if (IPNetAddress.TryParse(parts[0], out IPNetAddress result)) + { + _publishedServerUrls[result] = replacement; + } + else + { + _logger.LogError("Unable to parse bind ip address. {Parts}", parts[1]); + } + } + } + } + } + + /// + /// Initialises the network bind addresses. + /// + private void InitialiseBind(NetworkConfiguration config) + { + lock (_intLock) + { + string[] lanAddresses = config.LocalNetworkAddresses; + + // TODO: remove when bug fixed: https://github.com/jellyfin/jellyfin-web/issues/1334 + + if (lanAddresses.Length == 1 && lanAddresses[0].IndexOf(',', StringComparison.OrdinalIgnoreCase) != -1) + { + lanAddresses = lanAddresses[0].Split(','); + } + + // TODO: end fix: https://github.com/jellyfin/jellyfin-web/issues/1334 + + // Add virtual machine interface names to the list of bind exclusions, so that they are auto-excluded. + if (config.IgnoreVirtualInterfaces) + { + var virtualInterfaceNames = config.VirtualInterfaceNames.Split(','); + var newList = new string[lanAddresses.Length + virtualInterfaceNames.Length]; + Array.Copy(lanAddresses, newList, lanAddresses.Length); + Array.Copy(virtualInterfaceNames, 0, newList, lanAddresses.Length, virtualInterfaceNames.Length); + lanAddresses = newList; + } + + // Read and parse bind addresses and exclusions, removing ones that don't exist. + _bindAddresses = CreateIPCollection(lanAddresses).Union(_interfaceAddresses); + _bindExclusions = CreateIPCollection(lanAddresses, true).Union(_interfaceAddresses); + _logger.LogInformation("Using bind addresses: {0}", _bindAddresses.AsString()); + _logger.LogInformation("Using bind exclusions: {0}", _bindExclusions.AsString()); + } + } + + /// + /// Initialises the remote address values. + /// + private void InitialiseRemote(NetworkConfiguration config) + { + lock (_intLock) + { + RemoteAddressFilter = CreateIPCollection(config.RemoteIPFilter); + } + } + + /// + /// Initialises internal LAN cache settings. + /// + private void InitialiseLAN(NetworkConfiguration config) + { + lock (_intLock) + { + _logger.LogDebug("Refreshing LAN information."); + + // Get config options. + string[] subnets = config.LocalNetworkSubnets; + + // Create lists from user settings. + + _lanSubnets = CreateIPCollection(subnets); + _excludedSubnets = CreateIPCollection(subnets, true).AsNetworks(); + + // If no LAN addresses are specified - all private subnets are deemed to be the LAN + _usingPrivateAddresses = _lanSubnets.Count == 0; + + // NOTE: The order of the commands generating the collection in this statement matters. + // Altering the order will cause the collections to be created incorrectly. + if (_usingPrivateAddresses) + { + _logger.LogDebug("Using LAN interface addresses as user provided no LAN details."); + // Internal interfaces must be private and not excluded. + _internalInterfaces = CreateCollection(_interfaceAddresses.Where(i => IsPrivateAddressRange(i) && !_excludedSubnets.ContainsAddress(i))); + + // Subnets are the same as the calculated internal interface. + _lanSubnets = new Collection(); + + // We must listen on loopback for LiveTV to function regardless of the settings. + if (IsIP6Enabled) + { + _lanSubnets.AddItem(IPNetAddress.IP6Loopback); + _lanSubnets.AddItem(IPNetAddress.Parse("fc00::/7")); // ULA + _lanSubnets.AddItem(IPNetAddress.Parse("fe80::/10")); // Site local + } + + if (IsIP4Enabled) + { + _lanSubnets.AddItem(IPNetAddress.IP4Loopback); + _lanSubnets.AddItem(IPNetAddress.Parse("10.0.0.0/8")); + _lanSubnets.AddItem(IPNetAddress.Parse("172.16.0.0/12")); + _lanSubnets.AddItem(IPNetAddress.Parse("192.168.0.0/16")); + } + } + else + { + // We must listen on loopback for LiveTV to function regardless of the settings. + if (IsIP6Enabled) + { + _lanSubnets.AddItem(IPNetAddress.IP6Loopback); + } + + if (IsIP4Enabled) + { + _lanSubnets.AddItem(IPNetAddress.IP4Loopback); + } + + // Internal interfaces must be private, not excluded and part of the LocalNetworkSubnet. + _internalInterfaces = CreateCollection(_interfaceAddresses.Where(i => IsInLocalNetwork(i))); + } + + _logger.LogInformation("Defined LAN addresses : {0}", _lanSubnets.AsString()); + _logger.LogInformation("Defined LAN exclusions : {0}", _excludedSubnets.AsString()); + _logger.LogInformation("Using LAN addresses: {0}", _lanSubnets.Exclude(_excludedSubnets).AsNetworks().AsString()); + } + } + + /// + /// Generate a list of all the interface ip addresses and submasks where that are in the active/unknown state. + /// Generate a list of all active mac addresses that aren't loopback addresses. + /// + private void InitialiseInterfaces() + { + lock (_intLock) + { + _logger.LogDebug("Refreshing interfaces."); + + _interfaceNames.Clear(); + _interfaceAddresses.Clear(); + _macAddresses.Clear(); + + try + { + IEnumerable nics = NetworkInterface.GetAllNetworkInterfaces() + .Where(i => i.SupportsMulticast && i.OperationalStatus == OperationalStatus.Up); + + foreach (NetworkInterface adapter in nics) + { + try + { + IPInterfaceProperties ipProperties = adapter.GetIPProperties(); + PhysicalAddress mac = adapter.GetPhysicalAddress(); + + // populate mac list + if (adapter.NetworkInterfaceType != NetworkInterfaceType.Loopback && mac != null && mac != PhysicalAddress.None) + { + _macAddresses.Add(mac); + } + + // populate interface address list + foreach (UnicastIPAddressInformation info in ipProperties.UnicastAddresses) + { + if (IsIP4Enabled && info.Address.AddressFamily == AddressFamily.InterNetwork) + { + IPNetAddress nw = new IPNetAddress(info.Address, IPObject.MaskToCidr(info.IPv4Mask)) + { + // Keep the number of gateways on this interface, along with its index. + Tag = ipProperties.GetIPv4Properties().Index + }; + + int tag = nw.Tag; + if (ipProperties.GatewayAddresses.Count > 0 && !nw.IsLoopback()) + { + // -ve Tags signify the interface has a gateway. + nw.Tag *= -1; + } + + _interfaceAddresses.AddItem(nw); + + // Store interface name so we can use the name in Collections. + _interfaceNames[adapter.Description.ToLower(CultureInfo.InvariantCulture)] = tag; + _interfaceNames["eth" + tag.ToString(CultureInfo.InvariantCulture)] = tag; + } + else if (IsIP6Enabled && info.Address.AddressFamily == AddressFamily.InterNetworkV6) + { + IPNetAddress nw = new IPNetAddress(info.Address, (byte)info.PrefixLength) + { + // Keep the number of gateways on this interface, along with its index. + Tag = ipProperties.GetIPv6Properties().Index + }; + + int tag = nw.Tag; + if (ipProperties.GatewayAddresses.Count > 0 && !nw.IsLoopback()) + { + // -ve Tags signify the interface has a gateway. + nw.Tag *= -1; + } + + _interfaceAddresses.AddItem(nw); + + // Store interface name so we can use the name in Collections. + _interfaceNames[adapter.Description.ToLower(CultureInfo.InvariantCulture)] = tag; + _interfaceNames["eth" + tag.ToString(CultureInfo.InvariantCulture)] = tag; + } + } + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception ex) + { + // Ignore error, and attempt to continue. + _logger.LogError(ex, "Error encountered parsing interfaces."); + } +#pragma warning restore CA1031 // Do not catch general exception types + } + + _logger.LogDebug("Discovered {0} interfaces.", _interfaceAddresses.Count); + _logger.LogDebug("Interfaces addresses : {0}", _interfaceAddresses.AsString()); + + // If for some reason we don't have an interface info, resolve our DNS name. + if (_interfaceAddresses.Count == 0) + { + _logger.LogError("No interfaces information available. Resolving DNS name."); + IPHost host = new IPHost(Dns.GetHostName()); + foreach (var a in host.GetAddresses()) + { + _interfaceAddresses.AddItem(a); + } + + if (_interfaceAddresses.Count == 0) + { + _logger.LogWarning("No interfaces information available. Using loopback."); + // Last ditch attempt - use loopback address. + _interfaceAddresses.AddItem(IPNetAddress.IP4Loopback); + if (IsIP6Enabled) + { + _interfaceAddresses.AddItem(IPNetAddress.IP6Loopback); + } + } + } + } + catch (NetworkInformationException ex) + { + _logger.LogError(ex, "Error in InitialiseInterfaces."); + } + } + } + + /// + /// Attempts to match the source against a user defined bind interface. + /// + /// IP source address to use. + /// True if the source is in the external subnet. + /// The published server url that matches the source address. + /// The resultant port, if one exists. + /// true if a match is found, false otherwise. + private bool MatchesPublishedServerUrl(IPObject source, bool isInExternalSubnet, out string bindPreference, out int? port) + { + bindPreference = string.Empty; + port = null; + + // Check for user override. + foreach (var addr in _publishedServerUrls) + { + // Remaining. Match anything. + if (addr.Key.Address.Equals(IPAddress.Broadcast)) + { + bindPreference = addr.Value; + break; + } + else if ((addr.Key.Address.Equals(IPAddress.Any) || addr.Key.Address.Equals(IPAddress.IPv6Any)) && isInExternalSubnet) + { + // External. + bindPreference = addr.Value; + break; + } + else if (addr.Key.Contains(source)) + { + // Match ip address. + bindPreference = addr.Value; + break; + } + } + + if (string.IsNullOrEmpty(bindPreference)) + { + return false; + } + + // Has it got a port defined? + var parts = bindPreference.Split(':'); + if (parts.Length > 1) + { + if (int.TryParse(parts[1], out int p)) + { + bindPreference = parts[0]; + port = p; + } + } + + return true; + } + + /// + /// Attempts to match the source against a user defined bind interface. + /// + /// IP source address to use. + /// True if the source is in the external subnet. + /// The result, if a match is found. + /// true if a match is found, false otherwise. + private bool MatchesBindInterface(IPObject source, bool isInExternalSubnet, out string result) + { + result = string.Empty; + var addresses = _bindAddresses.Exclude(_bindExclusions); + + int count = addresses.Count; + if (count == 1 && (_bindAddresses[0].Equals(IPAddress.Any) || _bindAddresses[0].Equals(IPAddress.IPv6Any))) + { + // Ignore IPAny addresses. + count = 0; + } + + if (count != 0) + { + // Check to see if any of the bind interfaces are in the same subnet. + + IPAddress? defaultGateway = null; + IPAddress? bindAddress = null; + + if (isInExternalSubnet) + { + // Find all external bind addresses. Store the default gateway, but check to see if there is a better match first. + foreach (var addr in addresses.OrderBy(p => p.Tag)) + { + if (defaultGateway == null && !IsInLocalNetwork(addr)) + { + defaultGateway = addr.Address; + } + + if (bindAddress == null && addr.Contains(source)) + { + bindAddress = addr.Address; + } + + if (defaultGateway != null && bindAddress != null) + { + break; + } + } + } + else + { + // Look for the best internal address. + bindAddress = addresses + .Where(p => IsInLocalNetwork(p) && (p.Contains(source) || p.Equals(IPAddress.None))) + .OrderBy(p => p.Tag) + .FirstOrDefault()?.Address; + } + + if (bindAddress != null) + { + result = FormatIP6String(bindAddress); + _logger.LogDebug("{Source}: GetBindInterface: Has source, found a match bind interface subnets. {Result}", source, result); + return true; + } + + if (isInExternalSubnet && defaultGateway != null) + { + result = FormatIP6String(defaultGateway); + _logger.LogDebug("{Source}: GetBindInterface: Using first user defined external interface. {Result}", source, result); + return true; + } + + result = FormatIP6String(addresses[0].Address); + _logger.LogDebug("{Source}: GetBindInterface: Selected first user defined interface. {Result}", source, result); + + if (isInExternalSubnet) + { + _logger.LogWarning("{Source}: External request received, however, only an internal interface bind found.", source); + } + + return true; + } + + return false; + } + + /// + /// Attempts to match the source against an external interface. + /// + /// IP source address to use. + /// The result, if a match is found. + /// true if a match is found, false otherwise. + private bool MatchesExternalInterface(IPObject source, out string result) + { + result = string.Empty; + // Get the first WAN interface address that isn't a loopback. + var extResult = _interfaceAddresses + .Exclude(_bindExclusions) + .Where(p => !IsInLocalNetwork(p)) + .OrderBy(p => p.Tag); + + if (extResult.Any()) + { + // Does the request originate in one of the interface subnets? + // (For systems with multiple internal network cards, and multiple subnets) + foreach (var intf in extResult) + { + if (!IsInLocalNetwork(intf) && intf.Contains(source)) + { + result = FormatIP6String(intf.Address); + _logger.LogDebug("{Source}: GetBindInterface: Selected best external on interface on range. {Result}", source, result); + return true; + } + } + + result = FormatIP6String(extResult.First().Address); + _logger.LogDebug("{Source}: GetBindInterface: Selected first external interface. {Result}", source, result); + return true; + } + + // Have to return something, so return an internal address + + _logger.LogWarning("{Source}: External request received, however, no WAN interface found.", source); + return false; + } + } +} diff --git a/MediaBrowser.Common/Net/IPHost.cs b/MediaBrowser.Common/Net/IPHost.cs new file mode 100644 index 0000000000..4cede9ab16 --- /dev/null +++ b/MediaBrowser.Common/Net/IPHost.cs @@ -0,0 +1,445 @@ +#nullable enable +using System; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace MediaBrowser.Common.Net +{ + /// + /// Object that holds a host name. + /// + public class IPHost : IPObject + { + /// + /// Gets or sets timeout value before resolve required, in minutes. + /// + public const int Timeout = 30; + + /// + /// Represents an IPHost that has no value. + /// + public static readonly IPHost None = new IPHost(string.Empty, IPAddress.None); + + /// + /// Time when last resolved in ticks. + /// + private DateTime? _lastResolved = null; + + /// + /// Gets the IP Addresses, attempting to resolve the name, if there are none. + /// + private IPAddress[] _addresses; + + /// + /// Initializes a new instance of the class. + /// + /// Host name to assign. + public IPHost(string name) + { + HostName = name ?? throw new ArgumentNullException(nameof(name)); + _addresses = Array.Empty(); + Resolved = false; + } + + /// + /// Initializes a new instance of the class. + /// + /// Host name to assign. + /// Address to assign. + private IPHost(string name, IPAddress address) + { + HostName = name ?? throw new ArgumentNullException(nameof(name)); + _addresses = new IPAddress[] { address ?? throw new ArgumentNullException(nameof(address)) }; + Resolved = !address.Equals(IPAddress.None); + } + + /// + /// Gets or sets the object's first IP address. + /// + public override IPAddress Address + { + get + { + return ResolveHost() ? this[0] : IPAddress.None; + } + + set + { + // Not implemented, as a host's address is determined by DNS. + throw new NotImplementedException("The address of a host is determined by DNS."); + } + } + + /// + /// Gets or sets the object's first IP's subnet prefix. + /// The setter does nothing, but shouldn't raise an exception. + /// + public override byte PrefixLength + { + get + { + return (byte)(ResolveHost() ? 128 : 32); + } + + set + { + // Not implemented, as a host object can only have a prefix length of 128 (IPv6) or 32 (IPv4) prefix length, + // which is automatically determined by it's IP type. Anything else is meaningless. + } + } + + /// + /// Gets a value indicating whether the address has a value. + /// + public bool HasAddress => _addresses.Length != 0; + + /// + /// Gets the host name of this object. + /// + public string HostName { get; } + + /// + /// Gets a value indicating whether this host has attempted to be resolved. + /// + public bool Resolved { get; private set; } + + /// + /// Gets or sets the IP Addresses associated with this object. + /// + /// Index of address. + public IPAddress this[int index] + { + get + { + ResolveHost(); + return index >= 0 && index < _addresses.Length ? _addresses[index] : IPAddress.None; + } + } + + /// + /// Attempts to parse the host string. + /// + /// Host name to parse. + /// Object representing the string, if it has successfully been parsed. + /// true if the parsing is successful, false if not. + public static bool TryParse(string host, out IPHost hostObj) + { + if (!string.IsNullOrEmpty(host)) + { + // See if it's an IPv6 with port address e.g. [::1]:120. + int i = host.IndexOf("]:", StringComparison.OrdinalIgnoreCase); + if (i != -1) + { + return TryParse(host.Remove(i - 1).TrimStart(' ', '['), out hostObj); + } + else + { + // See if it's an IPv6 in [] with no port. + i = host.IndexOf(']', StringComparison.OrdinalIgnoreCase); + if (i != -1) + { + return TryParse(host.Remove(i - 1).TrimStart(' ', '['), out hostObj); + } + + // Is it a host or IPv4 with port? + string[] hosts = host.Split(':'); + + if (hosts.Length > 2) + { + hostObj = new IPHost(string.Empty, IPAddress.None); + return false; + } + + // Remove port from IPv4 if it exists. + host = hosts[0]; + + if (string.Equals("localhost", host, StringComparison.OrdinalIgnoreCase)) + { + hostObj = new IPHost(host, new IPAddress(Ipv4Loopback)); + return true; + } + + if (IPNetAddress.TryParse(host, out IPNetAddress netIP)) + { + // Host name is an ip address, so fake resolve. + hostObj = new IPHost(host, netIP.Address); + return true; + } + } + + // Only thing left is to see if it's a host string. + if (!string.IsNullOrEmpty(host)) + { + // Use regular expression as CheckHostName isn't RFC5892 compliant. + // Modified from gSkinner's expression at https://stackoverflow.com/questions/11809631/fully-qualified-domain-name-validation + Regex re = new Regex(@"^(?!:\/\/)(?=.{1,255}$)((.{1,63}\.){0,127}(?![0-9]*$)[a-z0-9-]+\.?)$", RegexOptions.IgnoreCase | RegexOptions.Multiline); + if (re.Match(host).Success) + { + hostObj = new IPHost(host); + return true; + } + } + } + + hostObj = IPHost.None; + return false; + } + + /// + /// Attempts to parse the host string. + /// + /// Host name to parse. + /// Object representing the string, if it has successfully been parsed. + public static IPHost Parse(string host) + { + if (!string.IsNullOrEmpty(host) && IPHost.TryParse(host, out IPHost res)) + { + return res; + } + + throw new InvalidCastException("Host does not contain a valid value. {host}"); + } + + /// + /// Attempts to parse the host string, ensuring that it resolves only to a specific IP type. + /// + /// Host name to parse. + /// Addressfamily filter. + /// Object representing the string, if it has successfully been parsed. + public static IPHost Parse(string host, AddressFamily family) + { + if (!string.IsNullOrEmpty(host) && IPHost.TryParse(host, out IPHost res)) + { + if (family == AddressFamily.InterNetwork) + { + res.Remove(AddressFamily.InterNetworkV6); + } + else + { + res.Remove(AddressFamily.InterNetwork); + } + + return res; + } + + throw new InvalidCastException("Host does not contain a valid value. {host}"); + } + + /// + /// Returns the Addresses that this item resolved to. + /// + /// IPAddress Array. + public IPAddress[] GetAddresses() + { + ResolveHost(); + return _addresses; + } + + /// + public override bool Contains(IPAddress address) + { + if (address != null && !Address.Equals(IPAddress.None)) + { + if (address.IsIPv4MappedToIPv6) + { + address = address.MapToIPv4(); + } + + foreach (var addr in GetAddresses()) + { + if (address.Equals(addr)) + { + return true; + } + } + } + + return false; + } + + /// + public override bool Equals(IPObject? other) + { + if (other is IPHost otherObj) + { + // Do we have the name Hostname? + if (string.Equals(otherObj.HostName, HostName, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + if (!ResolveHost() || !otherObj.ResolveHost()) + { + return false; + } + + // Do any of our IP addresses match? + foreach (IPAddress addr in _addresses) + { + foreach (IPAddress otherAddress in otherObj._addresses) + { + if (addr.Equals(otherAddress)) + { + return true; + } + } + } + } + + return false; + } + + /// + public override bool IsIP6() + { + // Returns true if interfaces are only IP6. + if (ResolveHost()) + { + foreach (IPAddress i in _addresses) + { + if (i.AddressFamily != AddressFamily.InterNetworkV6) + { + return false; + } + } + + return true; + } + + return false; + } + + /// + public override string ToString() + { + // StringBuilder not optimum here. + string output = string.Empty; + if (_addresses.Length > 0) + { + bool moreThanOne = _addresses.Length > 1; + if (moreThanOne) + { + output = "["; + } + + foreach (var i in _addresses) + { + if (Address.Equals(IPAddress.None) && Address.AddressFamily == AddressFamily.Unspecified) + { + output += HostName + ","; + } + else if (i.Equals(IPAddress.Any)) + { + output += "Any IP4 Address,"; + } + else if (Address.Equals(IPAddress.IPv6Any)) + { + output += "Any IP6 Address,"; + } + else if (i.Equals(IPAddress.Broadcast)) + { + output += "Any Address,"; + } + else + { + output += $"{i}/32,"; + } + } + + output = output[0..^1]; + + if (moreThanOne) + { + output += "]"; + } + } + else + { + output = HostName; + } + + return output; + } + + /// + public override void Remove(AddressFamily family) + { + if (ResolveHost()) + { + _addresses = _addresses.Where(p => p.AddressFamily != family).ToArray(); + } + } + + /// + public override bool Contains(IPObject address) + { + // An IPHost cannot contain another IPObject, it can only be equal. + return Equals(address); + } + + /// + protected override IPObject CalculateNetworkAddress() + { + var netAddr = NetworkAddressOf(this[0], PrefixLength); + return new IPNetAddress(netAddr.Address, netAddr.PrefixLength); + } + + /// + /// Attempt to resolve the ip address of a host. + /// + /// true if any addresses have been resolved, otherwise false. + private bool ResolveHost() + { + // When was the last time we resolved? + if (_lastResolved == null) + { + _lastResolved = DateTime.UtcNow; + } + + // If we haven't resolved before, or our timer has run out... + if ((_addresses.Length == 0 && !Resolved) || (DateTime.UtcNow > _lastResolved?.AddMinutes(Timeout))) + { + _lastResolved = DateTime.UtcNow; + ResolveHostInternal().GetAwaiter().GetResult(); + Resolved = true; + } + + return _addresses.Length > 0; + } + + /// + /// Task that looks up a Host name and returns its IP addresses. + /// + /// A representing the asynchronous operation. + private async Task ResolveHostInternal() + { + if (!string.IsNullOrEmpty(HostName)) + { + // Resolves the host name - so save a DNS lookup. + if (string.Equals(HostName, "localhost", StringComparison.OrdinalIgnoreCase)) + { + _addresses = new IPAddress[] { new IPAddress(Ipv4Loopback), new IPAddress(Ipv6Loopback) }; + return; + } + + if (Uri.CheckHostName(HostName).Equals(UriHostNameType.Dns)) + { + try + { + IPHostEntry ip = await Dns.GetHostEntryAsync(HostName).ConfigureAwait(false); + _addresses = ip.AddressList; + } + catch (SocketException ex) + { + // Log and then ignore socket errors, as the result value will just be an empty array. + Debug.WriteLine("GetHostEntryAsync failed with {Message}.", ex.Message); + } + } + } + } + } +} diff --git a/MediaBrowser.Common/Net/IPNetAddress.cs b/MediaBrowser.Common/Net/IPNetAddress.cs new file mode 100644 index 0000000000..a6f5fe4b37 --- /dev/null +++ b/MediaBrowser.Common/Net/IPNetAddress.cs @@ -0,0 +1,277 @@ +#nullable enable +using System; +using System.Net; +using System.Net.Sockets; + +namespace MediaBrowser.Common.Net +{ + /// + /// An object that holds and IP address and subnet mask. + /// + public class IPNetAddress : IPObject + { + /// + /// Represents an IPNetAddress that has no value. + /// + public static readonly IPNetAddress None = new IPNetAddress(IPAddress.None); + + /// + /// IPv4 multicast address. + /// + public static readonly IPAddress SSDPMulticastIPv4 = IPAddress.Parse("239.255.255.250"); + + /// + /// IPv6 local link multicast address. + /// + public static readonly IPAddress SSDPMulticastIPv6LinkLocal = IPAddress.Parse("ff02::C"); + + /// + /// IPv6 site local multicast address. + /// + public static readonly IPAddress SSDPMulticastIPv6SiteLocal = IPAddress.Parse("ff05::C"); + + /// + /// IP4Loopback address host. + /// + public static readonly IPNetAddress IP4Loopback = IPNetAddress.Parse("127.0.0.1/32"); + + /// + /// IP6Loopback address host. + /// + public static readonly IPNetAddress IP6Loopback = IPNetAddress.Parse("::1"); + + /// + /// Object's IP address. + /// + private IPAddress _address; + + /// + /// Initializes a new instance of the class. + /// + /// Address to assign. + public IPNetAddress(IPAddress address) + { + _address = address ?? throw new ArgumentNullException(nameof(address)); + PrefixLength = (byte)(address.AddressFamily == AddressFamily.InterNetwork ? 32 : 128); + } + + /// + /// Initializes a new instance of the class. + /// + /// IP Address. + /// Mask as a CIDR. + public IPNetAddress(IPAddress address, byte prefixLength) + { + if (address?.IsIPv4MappedToIPv6 ?? throw new ArgumentNullException(nameof(address))) + { + _address = address.MapToIPv4(); + } + else + { + _address = address; + } + + PrefixLength = prefixLength; + } + + /// + /// Gets or sets the object's IP address. + /// + public override IPAddress Address + { + get + { + return _address; + } + + set + { + _address = value ?? IPAddress.None; + } + } + + /// + public override byte PrefixLength { get; set; } + + /// + /// Try to parse the address and subnet strings into an IPNetAddress object. + /// + /// IP address to parse. Can be CIDR or X.X.X.X notation. + /// Resultant object. + /// True if the values parsed successfully. False if not, resulting in the IP being null. + public static bool TryParse(string addr, out IPNetAddress ip) + { + if (!string.IsNullOrEmpty(addr)) + { + addr = addr.Trim(); + + // Try to parse it as is. + if (IPAddress.TryParse(addr, out IPAddress? res)) + { + ip = new IPNetAddress(res); + return true; + } + + // Is it a network? + string[] tokens = addr.Split("/"); + + if (tokens.Length == 2) + { + tokens[0] = tokens[0].TrimEnd(); + tokens[1] = tokens[1].TrimStart(); + + if (IPAddress.TryParse(tokens[0], out res)) + { + // Is the subnet part a cidr? + if (byte.TryParse(tokens[1], out byte cidr)) + { + ip = new IPNetAddress(res, cidr); + return true; + } + + // Is the subnet in x.y.a.b form? + if (IPAddress.TryParse(tokens[1], out IPAddress? mask)) + { + ip = new IPNetAddress(res, MaskToCidr(mask)); + return true; + } + } + } + } + + ip = None; + return false; + } + + /// + /// Parses the string provided, throwing an exception if it is badly formed. + /// + /// String to parse. + /// IPNetAddress object. + public static IPNetAddress Parse(string addr) + { + if (TryParse(addr, out IPNetAddress o)) + { + return o; + } + + throw new ArgumentException("Unable to recognise object :" + addr); + } + + /// + public override bool Contains(IPAddress address) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + if (address.IsIPv4MappedToIPv6) + { + address = address.MapToIPv4(); + } + + var altAddress = NetworkAddressOf(address, PrefixLength); + return NetworkAddress.Address.Equals(altAddress.Address) && NetworkAddress.PrefixLength >= altAddress.PrefixLength; + } + + /// + public override bool Contains(IPObject address) + { + if (address is IPHost addressObj && addressObj.HasAddress) + { + foreach (IPAddress addr in addressObj.GetAddresses()) + { + if (Contains(addr)) + { + return true; + } + } + } + else if (address is IPNetAddress netaddrObj) + { + // Have the same network address, but different subnets? + if (NetworkAddress.Address.Equals(netaddrObj.NetworkAddress.Address)) + { + return NetworkAddress.PrefixLength <= netaddrObj.PrefixLength; + } + + var altAddress = NetworkAddressOf(netaddrObj.Address, PrefixLength); + return NetworkAddress.Address.Equals(altAddress.Address); + } + + return false; + } + + /// + public override bool Equals(IPObject? other) + { + if (other is IPNetAddress otherObj && !Address.Equals(IPAddress.None) && !otherObj.Address.Equals(IPAddress.None)) + { + return Address.Equals(otherObj.Address) && + PrefixLength == otherObj.PrefixLength; + } + + return false; + } + + /// + public override bool Equals(IPAddress address) + { + if (address != null && !address.Equals(IPAddress.None) && !Address.Equals(IPAddress.None)) + { + return address.Equals(Address); + } + + return false; + } + + /// + public override string ToString() + { + return ToString(false); + } + + /// + /// Returns a textual representation of this object. + /// + /// Set to true, if the subnet is to be excluded as part of the address. + /// String representation of this object. + public string ToString(bool shortVersion) + { + if (!Address.Equals(IPAddress.None)) + { + if (Address.Equals(IPAddress.Any)) + { + return "Any IP4 Address"; + } + + if (Address.Equals(IPAddress.IPv6Any)) + { + return "Any IP6 Address"; + } + + if (Address.Equals(IPAddress.Broadcast)) + { + return "Any Address"; + } + + if (shortVersion) + { + return Address.ToString(); + } + + return $"{Address}/{PrefixLength}"; + } + + return string.Empty; + } + + /// + protected override IPObject CalculateNetworkAddress() + { + var value = NetworkAddressOf(_address, PrefixLength); + return new IPNetAddress(value.Address, value.PrefixLength); + } + } +} diff --git a/MediaBrowser.Common/Net/IPObject.cs b/MediaBrowser.Common/Net/IPObject.cs new file mode 100644 index 0000000000..69cd57f8ae --- /dev/null +++ b/MediaBrowser.Common/Net/IPObject.cs @@ -0,0 +1,406 @@ +#nullable enable +using System; +using System.Net; +using System.Net.Sockets; + +namespace MediaBrowser.Common.Net +{ + /// + /// Base network object class. + /// + public abstract class IPObject : IEquatable + { + /// + /// IPv6 Loopback address. + /// + protected static readonly byte[] Ipv6Loopback = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 }; + + /// + /// IPv4 Loopback address. + /// + protected static readonly byte[] Ipv4Loopback = { 127, 0, 0, 1 }; + + /// + /// The network address of this object. + /// + private IPObject? _networkAddress; + + /// + /// Gets or sets a user defined value that is associated with this object. + /// + public int Tag { get; set; } + + /// + /// Gets or sets the object's IP address. + /// + public abstract IPAddress Address { get; set; } + + /// + /// Gets the object's network address. + /// + public IPObject NetworkAddress => _networkAddress ??= CalculateNetworkAddress(); + + /// + /// Gets or sets the object's IP address. + /// + public abstract byte PrefixLength { get; set; } + + /// + /// Gets the AddressFamily of this object. + /// + public AddressFamily AddressFamily + { + get + { + // Keep terms separate as Address performs other functions in inherited objects. + IPAddress address = Address; + return address.Equals(IPAddress.None) ? AddressFamily.Unspecified : address.AddressFamily; + } + } + + /// + /// Returns the network address of an object. + /// + /// IP Address to convert. + /// Subnet prefix. + /// IPAddress. + public static (IPAddress Address, byte PrefixLength) NetworkAddressOf(IPAddress address, byte prefixLength) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + if (address.IsIPv4MappedToIPv6) + { + address = address.MapToIPv4(); + } + + if (IsLoopback(address)) + { + return (Address: address, PrefixLength: prefixLength); + } + + // An ip address is just a list of bytes, each one representing a segment on the network. + // This separates the IP address into octets and calculates how many octets will need to be altered or set to zero dependant upon the + // prefix length value. eg. /16 on a 4 octet ip4 address (192.168.2.240) will result in the 2 and the 240 being zeroed out. + // Where there is not an exact boundary (eg /23), mod is used to calculate how many bits of this value are to be kept. + + // GetAddressBytes + Span addressBytes = stackalloc byte[address.AddressFamily == AddressFamily.InterNetwork ? 4 : 16]; + address.TryWriteBytes(addressBytes, out _); + + int div = prefixLength / 8; + int mod = prefixLength % 8; + if (mod != 0) + { + // Prefix length is counted right to left, so subtract 8 so we know how many bits to clear. + mod = 8 - mod; + + // Shift out the bits from the octet that we don't want, by moving right then back left. + addressBytes[div] = (byte)((int)addressBytes[div] >> mod << mod); + // Move on the next byte. + div++; + } + + // Blank out the remaining octets from mod + 1 to the end of the byte array. (192.168.2.240/16 becomes 192.168.0.0) + for (int octet = div; octet < addressBytes.Length; octet++) + { + addressBytes[octet] = 0; + } + + // Return the network address for the prefix. + return (Address: new IPAddress(addressBytes), PrefixLength: prefixLength); + } + + /// + /// Tests to see if the ip address is a Loopback address. + /// + /// Value to test. + /// True if it is. + public static bool IsLoopback(IPAddress address) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + if (!address.Equals(IPAddress.None)) + { + if (address.IsIPv4MappedToIPv6) + { + address = address.MapToIPv4(); + } + + return address.Equals(IPAddress.Loopback) || address.Equals(IPAddress.IPv6Loopback); + } + + return false; + } + + /// + /// Tests to see if the ip address is an IP6 address. + /// + /// Value to test. + /// True if it is. + public static bool IsIP6(IPAddress address) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + if (address.IsIPv4MappedToIPv6) + { + address = address.MapToIPv4(); + } + + return !address.Equals(IPAddress.None) && (address.AddressFamily == AddressFamily.InterNetworkV6); + } + + /// + /// Tests to see if the address in the private address range. + /// + /// Object to test. + /// True if it contains a private address. + public static bool IsPrivateAddressRange(IPAddress address) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + if (!address.Equals(IPAddress.None)) + { + if (address.IsIPv4MappedToIPv6) + { + address = address.MapToIPv4(); + } + + if (address.AddressFamily == AddressFamily.InterNetwork) + { + // GetAddressBytes + Span octet = stackalloc byte[4]; + address.TryWriteBytes(octet, out _); + + return (octet[0] == 10) + || (octet[0] == 172 && octet[1] >= 16 && octet[1] <= 31) // RFC1918 + || (octet[0] == 192 && octet[1] == 168) // RFC1918 + || (octet[0] == 127); // RFC1122 + } + else + { + // GetAddressBytes + Span octet = stackalloc byte[16]; + address.TryWriteBytes(octet, out _); + + uint word = (uint)(octet[0] << 8) + octet[1]; + + return (word >= 0xfe80 && word <= 0xfebf) // fe80::/10 :Local link. + || (word >= 0xfc00 && word <= 0xfdff); // fc00::/7 :Unique local address. + } + } + + return false; + } + + /// + /// Returns true if the IPAddress contains an IP6 Local link address. + /// + /// IPAddress object to check. + /// True if it is a local link address. + /// + /// See https://stackoverflow.com/questions/6459928/explain-the-instance-properties-of-system-net-ipaddress + /// it appears that the IPAddress.IsIPv6LinkLocal is out of date. + /// + public static bool IsIPv6LinkLocal(IPAddress address) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + if (address.IsIPv4MappedToIPv6) + { + address = address.MapToIPv4(); + } + + if (address.AddressFamily != AddressFamily.InterNetworkV6) + { + return false; + } + + // GetAddressBytes + Span octet = stackalloc byte[16]; + address.TryWriteBytes(octet, out _); + uint word = (uint)(octet[0] << 8) + octet[1]; + + return word >= 0xfe80 && word <= 0xfebf; // fe80::/10 :Local link. + } + + /// + /// Convert a subnet mask in CIDR notation to a dotted decimal string value. IPv4 only. + /// + /// Subnet mask in CIDR notation. + /// IPv4 or IPv6 family. + /// String value of the subnet mask in dotted decimal notation. + public static IPAddress CidrToMask(byte cidr, AddressFamily family) + { + uint addr = 0xFFFFFFFF << (family == AddressFamily.InterNetwork ? 32 : 128 - cidr); + addr = ((addr & 0xff000000) >> 24) + | ((addr & 0x00ff0000) >> 8) + | ((addr & 0x0000ff00) << 8) + | ((addr & 0x000000ff) << 24); + return new IPAddress(addr); + } + + /// + /// Convert a mask to a CIDR. IPv4 only. + /// https://stackoverflow.com/questions/36954345/get-cidr-from-netmask. + /// + /// Subnet mask. + /// Byte CIDR representing the mask. + public static byte MaskToCidr(IPAddress mask) + { + if (mask == null) + { + throw new ArgumentNullException(nameof(mask)); + } + + byte cidrnet = 0; + if (!mask.Equals(IPAddress.Any)) + { + // GetAddressBytes + Span bytes = stackalloc byte[mask.AddressFamily == AddressFamily.InterNetwork ? 4 : 16]; + mask.TryWriteBytes(bytes, out _); + + var zeroed = false; + for (var i = 0; i < bytes.Length; i++) + { + for (int v = bytes[i]; (v & 0xFF) != 0; v <<= 1) + { + if (zeroed) + { + // Invalid netmask. + return (byte)~cidrnet; + } + + if ((v & 0x80) == 0) + { + zeroed = true; + } + else + { + cidrnet++; + } + } + } + } + + return cidrnet; + } + + /// + /// Tests to see if this object is a Loopback address. + /// + /// True if it is. + public virtual bool IsLoopback() + { + return IsLoopback(Address); + } + + /// + /// Removes all addresses of a specific type from this object. + /// + /// Type of address to remove. + public virtual void Remove(AddressFamily family) + { + // This method only performs a function in the IPHost implementation of IPObject. + } + + /// + /// Tests to see if this object is an IPv6 address. + /// + /// True if it is. + public virtual bool IsIP6() + { + return IsIP6(Address); + } + + /// + /// Returns true if this IP address is in the RFC private address range. + /// + /// True this object has a private address. + public virtual bool IsPrivateAddressRange() + { + return IsPrivateAddressRange(Address); + } + + /// + /// Compares this to the object passed as a parameter. + /// + /// Object to compare to. + /// Equality result. + public virtual bool Equals(IPAddress ip) + { + if (ip != null) + { + if (ip.IsIPv4MappedToIPv6) + { + ip = ip.MapToIPv4(); + } + + return !Address.Equals(IPAddress.None) && Address.Equals(ip); + } + + return false; + } + + /// + /// Compares this to the object passed as a parameter. + /// + /// Object to compare to. + /// Equality result. + public virtual bool Equals(IPObject? other) + { + if (other != null) + { + return !Address.Equals(IPAddress.None) && Address.Equals(other.Address); + } + + return false; + } + + /// + /// Compares the address in this object and the address in the object passed as a parameter. + /// + /// Object's IP address to compare to. + /// Comparison result. + public abstract bool Contains(IPObject address); + + /// + /// Compares the address in this object and the address in the object passed as a parameter. + /// + /// Object's IP address to compare to. + /// Comparison result. + public abstract bool Contains(IPAddress address); + + /// + public override int GetHashCode() + { + return Address.GetHashCode(); + } + + /// + public override bool Equals(object? obj) + { + return Equals(obj as IPObject); + } + + /// + /// Calculates the network address of this object. + /// + /// Returns the network address of this object. + protected abstract IPObject CalculateNetworkAddress(); + } +} diff --git a/MediaBrowser.Common/Net/NetworkExtensions.cs b/MediaBrowser.Common/Net/NetworkExtensions.cs new file mode 100644 index 0000000000..d07bba249b --- /dev/null +++ b/MediaBrowser.Common/Net/NetworkExtensions.cs @@ -0,0 +1,262 @@ +#pragma warning disable CA1062 // Validate arguments of public methods +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Net; +using System.Runtime.CompilerServices; +using System.Text; + +namespace MediaBrowser.Common.Net +{ + /// + /// Defines the . + /// + public static class NetworkExtensions + { + /// + /// Add an address to the collection. + /// + /// The . + /// Item to add. + public static void AddItem(this Collection source, IPAddress ip) + { + if (!source.ContainsAddress(ip)) + { + source.Add(new IPNetAddress(ip, 32)); + } + } + + /// + /// Adds a network to the collection. + /// + /// The . + /// Item to add. + public static void AddItem(this Collection source, IPObject item) + { + if (!source.ContainsAddress(item)) + { + source.Add(item); + } + } + + /// + /// Converts this object to a string. + /// + /// The . + /// Returns a string representation of this object. + public static string AsString(this Collection source) + { + return $"[{string.Join(',', source)}]"; + } + + /// + /// Returns true if the collection contains an item with the ip address, + /// or the ip address falls within any of the collection's network ranges. + /// + /// The . + /// The item to look for. + /// True if the collection contains the item. + public static bool ContainsAddress(this Collection source, IPAddress item) + { + if (source.Count == 0) + { + return false; + } + + if (item == null) + { + throw new ArgumentNullException(nameof(item)); + } + + if (item.IsIPv4MappedToIPv6) + { + item = item.MapToIPv4(); + } + + foreach (var i in source) + { + if (i.Contains(item)) + { + return true; + } + } + + return false; + } + + /// + /// Returns true if the collection contains an item with the ip address, + /// or the ip address falls within any of the collection's network ranges. + /// + /// The . + /// The item to look for. + /// True if the collection contains the item. + public static bool ContainsAddress(this Collection source, IPObject item) + { + if (source.Count == 0) + { + return false; + } + + if (item == null) + { + throw new ArgumentNullException(nameof(item)); + } + + foreach (var i in source) + { + if (i.Contains(item)) + { + return true; + } + } + + return false; + } + + /// + /// Compares two Collection{IPObject} objects. The order is ignored. + /// + /// The . + /// Item to compare to. + /// True if both are equal. + public static bool Compare(this Collection source, Collection dest) + { + if (dest == null || source.Count != dest.Count) + { + return false; + } + + foreach (var sourceItem in source) + { + bool found = false; + foreach (var destItem in dest) + { + if (sourceItem.Equals(destItem)) + { + found = true; + break; + } + } + + if (!found) + { + return false; + } + } + + return true; + } + + /// + /// Returns a collection containing the subnets of this collection given. + /// + /// The . + /// Collection{IPObject} object containing the subnets. + public static Collection AsNetworks(this Collection source) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + Collection res = new Collection(); + + foreach (IPObject i in source) + { + if (i is IPNetAddress nw) + { + // Add the subnet calculated from the interface address/mask. + var na = nw.NetworkAddress; + na.Tag = i.Tag; + res.AddItem(na); + } + else if (i is IPHost ipHost) + { + // Flatten out IPHost and add all its ip addresses. + foreach (var addr in ipHost.GetAddresses()) + { + IPNetAddress host = new IPNetAddress(addr) + { + Tag = i.Tag + }; + + res.AddItem(host); + } + } + } + + return res; + } + + /// + /// Excludes all the items from this list that are found in excludeList. + /// + /// The . + /// Items to exclude. + /// A new collection, with the items excluded. + public static Collection Exclude(this Collection source, Collection excludeList) + { + if (source.Count == 0 || excludeList == null) + { + return new Collection(source); + } + + Collection results = new Collection(); + + bool found; + foreach (var outer in source) + { + found = false; + + foreach (var inner in excludeList) + { + if (outer.Equals(inner)) + { + found = true; + break; + } + } + + if (!found) + { + results.AddItem(outer); + } + } + + return results; + } + + /// + /// Returns all items that co-exist in this object and target. + /// + /// The . + /// Collection to compare with. + /// A collection containing all the matches. + public static Collection Union(this Collection source, Collection target) + { + if (source.Count == 0) + { + return new Collection(); + } + + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + Collection nc = new Collection(); + + foreach (IPObject i in source) + { + if (target.ContainsAddress(i)) + { + nc.AddItem(i); + } + } + + return nc; + } + } +} diff --git a/MediaBrowser.Model/Configuration/PathSubstitution.cs b/MediaBrowser.Model/Configuration/PathSubstitution.cs new file mode 100644 index 0000000000..bffaa85945 --- /dev/null +++ b/MediaBrowser.Model/Configuration/PathSubstitution.cs @@ -0,0 +1,20 @@ +#nullable enable + +namespace MediaBrowser.Model.Configuration +{ + /// + /// Defines the . + /// + public class PathSubstitution + { + /// + /// Gets or sets the value to substitute. + /// + public string From { get; set; } = string.Empty; + + /// + /// Gets or sets the value to substitution with. + /// + public string To { get; set; } = string.Empty; + } +} diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 23a5201f7a..06985ebf40 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -1,5 +1,5 @@ -#nullable disable #pragma warning disable CS1591 +#pragma warning disable CA1819 using System; using System.Collections.Generic; @@ -13,43 +13,194 @@ namespace MediaBrowser.Model.Configuration /// public class ServerConfiguration : BaseApplicationConfiguration { + /// + /// The default value for . + /// public const int DefaultHttpPort = 8096; + + /// + /// The default value for and . + /// public const int DefaultHttpsPort = 8920; - private string _baseUrl; + + private string _baseUrl = string.Empty; + + /// + /// Initializes a new instance of the class. + /// + public ServerConfiguration() + { + MetadataOptions = new[] + { + new MetadataOptions() + { + ItemType = "Book" + }, + new MetadataOptions() + { + ItemType = "Movie" + }, + new MetadataOptions + { + ItemType = "MusicVideo", + DisabledMetadataFetchers = new[] { "The Open Movie Database" }, + DisabledImageFetchers = new[] { "The Open Movie Database" } + }, + new MetadataOptions + { + ItemType = "Series", + DisabledMetadataFetchers = new[] { "TheMovieDb" }, + DisabledImageFetchers = new[] { "TheMovieDb" } + }, + new MetadataOptions + { + ItemType = "MusicAlbum", + DisabledMetadataFetchers = new[] { "TheAudioDB" } + }, + new MetadataOptions + { + ItemType = "MusicArtist", + DisabledMetadataFetchers = new[] { "TheAudioDB" } + }, + new MetadataOptions + { + ItemType = "BoxSet" + }, + new MetadataOptions + { + ItemType = "Season", + DisabledMetadataFetchers = new[] { "TheMovieDb" }, + }, + new MetadataOptions + { + ItemType = "Episode", + DisabledMetadataFetchers = new[] { "The Open Movie Database", "TheMovieDb" }, + DisabledImageFetchers = new[] { "The Open Movie Database", "TheMovieDb" } + } + }; + } /// /// Gets or sets a value indicating whether to enable automatic port forwarding. /// - public bool EnableUPnP { get; set; } + public bool EnableUPnP { get; set; } = false; /// /// Gets or sets a value indicating whether to enable prometheus metrics exporting. /// - public bool EnableMetrics { get; set; } + public bool EnableMetrics { get; set; } = false; /// /// Gets or sets the public mapped port. /// /// The public mapped port. - public int PublicPort { get; set; } + public int PublicPort { get; set; } = DefaultHttpPort; + + /// + /// Gets or sets a value indicating whether the http port should be mapped as part of UPnP automatic port forwarding. + /// + public bool UPnPCreateHttpPortMap { get; set; } = false; + + /// + /// Gets or sets client udp port range. + /// + public string UDPPortRange { get; set; } = string.Empty; + + /// + /// Gets or sets a value indicating whether IPV6 capability is enabled. + /// + public bool EnableIPV6 { get; set; } = false; + + /// + /// Gets or sets a value indicating whether IPV4 capability is enabled. + /// + public bool EnableIPV4 { get; set; } = true; + + /// + /// Gets or sets a value indicating whether detailed ssdp logs are sent to the console/log. + /// "Emby.Dlna": "Debug" must be set in logging.default.json for this property to work. + /// + public bool EnableSSDPTracing { get; set; } = false; + + /// + /// Gets or sets a value indicating whether an IP address is to be used to filter the detailed ssdp logs that are being sent to the console/log. + /// If the setting "Emby.Dlna": "Debug" msut be set in logging.default.json for this property to work. + /// + public string SSDPTracingFilter { get; set; } = string.Empty; + + /// + /// Gets or sets the number of times SSDP UDP messages are sent. + /// + public int UDPSendCount { get; set; } = 2; + + /// + /// Gets or sets the delay between each groups of SSDP messages (in ms). + /// + public int UDPSendDelay { get; set; } = 100; + + /// + /// Gets or sets a value indicating whether address names that match should be Ignore for the purposes of binding. + /// + public bool IgnoreVirtualInterfaces { get; set; } = true; + + /// + /// Gets or sets a value indicating the interfaces that should be ignored. The list can be comma separated. . + /// + public string VirtualInterfaceNames { get; set; } = "vEthernet*"; + + /// + /// Gets or sets the time (in seconds) between the pings of SSDP gateway monitor. + /// + public int GatewayMonitorPeriod { get; set; } = 60; + + /// + /// Gets a value indicating whether multi-socket binding is available. + /// + public bool EnableMultiSocketBinding { get; } = true; + + /// + /// Gets or sets a value indicating whether all IPv6 interfaces should be treated as on the internal network. + /// Depending on the address range implemented ULA ranges might not be used. + /// + public bool TrustAllIP6Interfaces { get; set; } = false; + + /// + /// Gets or sets the ports that HDHomerun uses. + /// + public string HDHomerunPortRange { get; set; } = string.Empty; + + /// + /// Gets or sets PublishedServerUri to advertise for specific subnets. + /// + public string[] PublishedServerUriBySubnet { get; set; } = Array.Empty(); + + /// + /// Gets or sets a value indicating whether Autodiscovery tracing is enabled. + /// + public bool AutoDiscoveryTracing { get; set; } = false; + + /// + /// Gets or sets a value indicating whether Autodiscovery is enabled. + /// + public bool AutoDiscovery { get; set; } = true; /// /// Gets or sets the public HTTPS port. /// /// The public HTTPS port. - public int PublicHttpsPort { get; set; } + public int PublicHttpsPort { get; set; } = DefaultHttpsPort; /// /// Gets or sets the HTTP server port number. /// /// The HTTP server port number. - public int HttpServerPortNumber { get; set; } + public int HttpServerPortNumber { get; set; } = DefaultHttpPort; /// /// Gets or sets the HTTPS server port number. /// /// The HTTPS server port number. - public int HttpsPortNumber { get; set; } + public int HttpsPortNumber { get; set; } = DefaultHttpsPort; /// /// Gets or sets a value indicating whether to use HTTPS. @@ -58,19 +209,19 @@ namespace MediaBrowser.Model.Configuration /// In order for HTTPS to be used, in addition to setting this to true, valid values must also be /// provided for and . /// - public bool EnableHttps { get; set; } + public bool EnableHttps { get; set; } = false; - public bool EnableNormalizedItemByNameIds { get; set; } + public bool EnableNormalizedItemByNameIds { get; set; } = true; /// /// Gets or sets the filesystem path of an X.509 certificate to use for SSL. /// - public string CertificatePath { get; set; } + public string CertificatePath { get; set; } = string.Empty; /// /// Gets or sets the password required to access the X.509 certificate data in the file specified by . /// - public string CertificatePassword { get; set; } + public string CertificatePassword { get; set; } = string.Empty; /// /// Gets or sets a value indicating whether this instance is port authorized. @@ -79,90 +230,93 @@ namespace MediaBrowser.Model.Configuration public bool IsPortAuthorized { get; set; } /// - /// Gets or sets if quick connect is available for use on this server. + /// Gets or sets a value indicating whether quick connect is available for use on this server. /// - public bool QuickConnectAvailable { get; set; } - - public bool EnableRemoteAccess { get; set; } + public bool QuickConnectAvailable { get; set; } = false; + + /// + /// Gets or sets a value indicating whether access outside of the LAN is permitted. + /// + public bool EnableRemoteAccess { get; set; } = true; /// /// Gets or sets a value indicating whether [enable case sensitive item ids]. /// /// true if [enable case sensitive item ids]; otherwise, false. - public bool EnableCaseSensitiveItemIds { get; set; } + public bool EnableCaseSensitiveItemIds { get; set; } = true; - public bool DisableLiveTvChannelUserDataName { get; set; } + public bool DisableLiveTvChannelUserDataName { get; set; } = true; /// /// Gets or sets the metadata path. /// /// The metadata path. - public string MetadataPath { get; set; } + public string MetadataPath { get; set; } = string.Empty; - public string MetadataNetworkPath { get; set; } + public string MetadataNetworkPath { get; set; } = string.Empty; /// /// Gets or sets the preferred metadata language. /// /// The preferred metadata language. - public string PreferredMetadataLanguage { get; set; } + public string PreferredMetadataLanguage { get; set; } = string.Empty; /// /// Gets or sets the metadata country code. /// /// The metadata country code. - public string MetadataCountryCode { get; set; } + public string MetadataCountryCode { get; set; } = "US"; /// - /// Characters to be replaced with a ' ' in strings to create a sort name. + /// Gets or sets characters to be replaced with a ' ' in strings to create a sort name. /// /// The sort replace characters. - public string[] SortReplaceCharacters { get; set; } + public string[] SortReplaceCharacters { get; set; } = new[] { ".", "+", "%" }; /// - /// Characters to be removed from strings to create a sort name. + /// Gets or sets characters to be removed from strings to create a sort name. /// /// The sort remove characters. - public string[] SortRemoveCharacters { get; set; } + public string[] SortRemoveCharacters { get; set; } = new[] { ",", "&", "-", "{", "}", "'" }; /// - /// Words to be removed from strings to create a sort name. + /// Gets or sets words to be removed from strings to create a sort name. /// /// The sort remove words. - public string[] SortRemoveWords { get; set; } + public string[] SortRemoveWords { get; set; } = new[] { "the", "a", "an" }; /// /// Gets or sets the minimum percentage of an item that must be played in order for playstate to be updated. /// /// The min resume PCT. - public int MinResumePct { get; set; } + public int MinResumePct { get; set; } = 5; /// /// Gets or sets the maximum percentage of an item that can be played while still saving playstate. If this percentage is crossed playstate will be reset to the beginning and the item will be marked watched. /// /// The max resume PCT. - public int MaxResumePct { get; set; } + public int MaxResumePct { get; set; } = 90; /// /// Gets or sets the minimum duration that an item must have in order to be eligible for playstate updates.. /// /// The min resume duration seconds. - public int MinResumeDurationSeconds { get; set; } + public int MinResumeDurationSeconds { get; set; } = 300; /// - /// The delay in seconds that we will wait after a file system change to try and discover what has been added/removed + /// Gets or sets the delay in seconds that we will wait after a file system change to try and discover what has been added/removed /// Some delay is necessary with some items because their creation is not atomic. It involves the creation of several /// different directories and files. /// /// The file watcher delay. - public int LibraryMonitorDelay { get; set; } + public int LibraryMonitorDelay { get; set; } = 60; /// /// Gets or sets a value indicating whether [enable dashboard response caching]. /// Allows potential contributors without visual studio to modify production dashboard code and test changes. /// /// true if [enable dashboard response caching]; otherwise, false. - public bool EnableDashboardResponseCaching { get; set; } + public bool EnableDashboardResponseCaching { get; set; } = true; /// /// Gets or sets the image saving convention. @@ -172,9 +326,9 @@ namespace MediaBrowser.Model.Configuration public MetadataOptions[] MetadataOptions { get; set; } - public bool SkipDeserializationForBasicTypes { get; set; } + public bool SkipDeserializationForBasicTypes { get; set; } = true; - public string ServerName { get; set; } + public string ServerName { get; set; } = string.Empty; public string BaseUrl { @@ -206,194 +360,84 @@ namespace MediaBrowser.Model.Configuration } } - public string UICulture { get; set; } - - public bool SaveMetadataHidden { get; set; } + public string UICulture { get; set; } = "en-US"; - public NameValuePair[] ContentTypes { get; set; } + public bool SaveMetadataHidden { get; set; } = false; - public int RemoteClientBitrateLimit { get; set; } + public NameValuePair[] ContentTypes { get; set; } = Array.Empty(); - public bool EnableFolderView { get; set; } + public int RemoteClientBitrateLimit { get; set; } = 0; - public bool EnableGroupingIntoCollections { get; set; } + public bool EnableFolderView { get; set; } = false; - public bool DisplaySpecialsWithinSeasons { get; set; } + public bool EnableGroupingIntoCollections { get; set; } = false; - public string[] LocalNetworkSubnets { get; set; } + public bool DisplaySpecialsWithinSeasons { get; set; } = true; - public string[] LocalNetworkAddresses { get; set; } + /// + /// Gets or sets the subnets that are deemed to make up the LAN. + /// + public string[] LocalNetworkSubnets { get; set; } = Array.Empty(); - public string[] CodecsUsed { get; set; } + /// + /// Gets or sets the interface addresses which Jellyfin will bind to. If empty, all interfaces will be used. + /// + public string[] LocalNetworkAddresses { get; set; } = Array.Empty(); - public List PluginRepositories { get; set; } + public string[] CodecsUsed { get; set; } = Array.Empty(); - public bool IgnoreVirtualInterfaces { get; set; } + public List PluginRepositories { get; set; } = new List(); - public bool EnableExternalContentInSuggestions { get; set; } + public bool EnableExternalContentInSuggestions { get; set; } = true; /// /// Gets or sets a value indicating whether the server should force connections over HTTPS. /// - public bool RequireHttps { get; set; } + public bool RequireHttps { get; set; } = false; - public bool EnableNewOmdbSupport { get; set; } + public bool EnableNewOmdbSupport { get; set; } = true; - public string[] RemoteIPFilter { get; set; } + /// + /// Gets or sets the filter for remote IP connectivity. Used in conjuntion with . + /// + public string[] RemoteIPFilter { get; set; } = Array.Empty(); - public bool IsRemoteIPFilterBlacklist { get; set; } + /// + /// Gets or sets a value indicating whether contains a blacklist or a whitelist. Default is a whitelist. + /// + public bool IsRemoteIPFilterBlacklist { get; set; } = false; - public int ImageExtractionTimeoutMs { get; set; } + public int ImageExtractionTimeoutMs { get; set; } = 0; - public PathSubstitution[] PathSubstitutions { get; set; } + public PathSubstitution[] PathSubstitutions { get; set; } = Array.Empty(); - public bool EnableSimpleArtistDetection { get; set; } + public bool EnableSimpleArtistDetection { get; set; } = false; - public string[] UninstalledPlugins { get; set; } + public string[] UninstalledPlugins { get; set; } = Array.Empty(); /// /// Gets or sets a value indicating whether slow server responses should be logged as a warning. /// - public bool EnableSlowResponseWarning { get; set; } + public bool EnableSlowResponseWarning { get; set; } = true; /// /// Gets or sets the threshold for the slow response time warning in ms. /// - public long SlowResponseThresholdMs { get; set; } + public long SlowResponseThresholdMs { get; set; } = 500; /// /// Gets or sets the cors hosts. /// - public string[] CorsHosts { get; set; } + public string[] CorsHosts { get; set; } = new[] { "*" }; /// /// Gets or sets the known proxies. /// - public string[] KnownProxies { get; set; } + public string[] KnownProxies { get; set; } = Array.Empty(); /// /// Gets or sets the number of days we should retain activity logs. /// - public int? ActivityLogRetentionDays { get; set; } - - /// - /// Initializes a new instance of the class. - /// - public ServerConfiguration() - { - UninstalledPlugins = Array.Empty(); - RemoteIPFilter = Array.Empty(); - LocalNetworkSubnets = Array.Empty(); - LocalNetworkAddresses = Array.Empty(); - CodecsUsed = Array.Empty(); - PathSubstitutions = Array.Empty(); - IgnoreVirtualInterfaces = false; - EnableSimpleArtistDetection = false; - SkipDeserializationForBasicTypes = true; - - PluginRepositories = new List(); - - DisplaySpecialsWithinSeasons = true; - EnableExternalContentInSuggestions = true; - - ImageSavingConvention = ImageSavingConvention.Compatible; - PublicPort = DefaultHttpPort; - PublicHttpsPort = DefaultHttpsPort; - HttpServerPortNumber = DefaultHttpPort; - HttpsPortNumber = DefaultHttpsPort; - EnableMetrics = false; - EnableHttps = false; - EnableDashboardResponseCaching = true; - EnableCaseSensitiveItemIds = true; - EnableNormalizedItemByNameIds = true; - DisableLiveTvChannelUserDataName = true; - EnableNewOmdbSupport = true; - - EnableRemoteAccess = true; - QuickConnectAvailable = false; - - EnableUPnP = false; - MinResumePct = 5; - MaxResumePct = 90; - - // 5 minutes - MinResumeDurationSeconds = 300; - - LibraryMonitorDelay = 60; - - ContentTypes = Array.Empty(); - - PreferredMetadataLanguage = "en"; - MetadataCountryCode = "US"; - - SortReplaceCharacters = new[] { ".", "+", "%" }; - SortRemoveCharacters = new[] { ",", "&", "-", "{", "}", "'" }; - SortRemoveWords = new[] { "the", "a", "an" }; - - BaseUrl = string.Empty; - UICulture = "en-US"; - - MetadataOptions = new[] - { - new MetadataOptions() - { - ItemType = "Book" - }, - new MetadataOptions() - { - ItemType = "Movie" - }, - new MetadataOptions - { - ItemType = "MusicVideo", - DisabledMetadataFetchers = new[] { "The Open Movie Database" }, - DisabledImageFetchers = new[] { "The Open Movie Database" } - }, - new MetadataOptions - { - ItemType = "Series", - DisabledMetadataFetchers = new[] { "TheMovieDb" }, - DisabledImageFetchers = new[] { "TheMovieDb" } - }, - new MetadataOptions - { - ItemType = "MusicAlbum", - DisabledMetadataFetchers = new[] { "TheAudioDB" } - }, - new MetadataOptions - { - ItemType = "MusicArtist", - DisabledMetadataFetchers = new[] { "TheAudioDB" } - }, - new MetadataOptions - { - ItemType = "BoxSet" - }, - new MetadataOptions - { - ItemType = "Season", - DisabledMetadataFetchers = new[] { "TheMovieDb" }, - }, - new MetadataOptions - { - ItemType = "Episode", - DisabledMetadataFetchers = new[] { "The Open Movie Database", "TheMovieDb" }, - DisabledImageFetchers = new[] { "The Open Movie Database", "TheMovieDb" } - } - }; - - EnableSlowResponseWarning = true; - SlowResponseThresholdMs = 500; - CorsHosts = new[] { "*" }; - KnownProxies = Array.Empty(); - ActivityLogRetentionDays = 30; - } - } - - public class PathSubstitution - { - public string From { get; set; } - - public string To { get; set; } + public int? ActivityLogRetentionDays { get; set; } = 30; } } diff --git a/MediaBrowser.sln b/MediaBrowser.sln index 25402aee13..d460c0ab0c 100644 --- a/MediaBrowser.sln +++ b/MediaBrowser.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26730.3 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30503.244 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server", "Jellyfin.Server\Jellyfin.Server.csproj", "{07E39F42-A2C6-4B32-AF8C-725F957A73FF}" EndProject @@ -66,12 +66,18 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Data", "Jellyfin.D EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server.Implementations", "Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj", "{DAE48069-6D86-4BA6-B148-D1D49B6DDA52}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Networking", "Jellyfin.Networking\Jellyfin.Networking.csproj", "{0A3FCC4D-C714-4072-B90F-E374A15F9FF9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Release|Any CPU.Build.0 = Release|Any CPU {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Debug|Any CPU.Build.0 = Debug|Any CPU {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -132,10 +138,6 @@ Global {960295EE-4AF4-4440-A525-B4C295B01A61}.Debug|Any CPU.Build.0 = Debug|Any CPU {960295EE-4AF4-4440-A525-B4C295B01A61}.Release|Any CPU.ActiveCfg = Release|Any CPU {960295EE-4AF4-4440-A525-B4C295B01A61}.Release|Any CPU.Build.0 = Release|Any CPU - {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Release|Any CPU.Build.0 = Release|Any CPU {154872D9-6C12-4007-96E3-8F70A58386CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {154872D9-6C12-4007-96E3-8F70A58386CE}.Debug|Any CPU.Build.0 = Debug|Any CPU {154872D9-6C12-4007-96E3-8F70A58386CE}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -176,10 +178,22 @@ Global {DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Debug|Any CPU.Build.0 = Debug|Any CPU {DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Release|Any CPU.ActiveCfg = Release|Any CPU {DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Release|Any CPU.Build.0 = Release|Any CPU + {0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {DF194677-DFD3-42AF-9F75-D44D5A416478} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + {28464062-0939-4AA7-9F7B-24DDDA61A7C0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + {3998657B-1CCC-49DD-A19F-275DC8495F57} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + {A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + {2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + {462584F7-5023-4019-9EAC-B98CA458C0A0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE} EndGlobalSection @@ -201,12 +215,4 @@ Global $0.DotNetNamingPolicy = $2 $2.DirectoryNamespaceAssociation = PrefixedHierarchical EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {DF194677-DFD3-42AF-9F75-D44D5A416478} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} - {28464062-0939-4AA7-9F7B-24DDDA61A7C0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} - {3998657B-1CCC-49DD-A19F-275DC8495F57} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} - {A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} - {2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} - {462584F7-5023-4019-9EAC-B98CA458C0A0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} - EndGlobalSection EndGlobal -- cgit v1.2.3 From 086e2fdaa1153588042a38fd06c9b4802127ed71 Mon Sep 17 00:00:00 2001 From: Greenback Date: Sat, 21 Nov 2020 14:09:56 +0000 Subject: Fixed a couple of merge errors. --- MediaBrowser.Model/Configuration/PathSubstitution.cs | 7 ------- MediaBrowser.Model/Configuration/ServerConfiguration.cs | 4 ++-- 2 files changed, 2 insertions(+), 9 deletions(-) (limited to 'MediaBrowser.Model/Configuration') diff --git a/MediaBrowser.Model/Configuration/PathSubstitution.cs b/MediaBrowser.Model/Configuration/PathSubstitution.cs index f47eabd1e9..bffaa85945 100644 --- a/MediaBrowser.Model/Configuration/PathSubstitution.cs +++ b/MediaBrowser.Model/Configuration/PathSubstitution.cs @@ -1,11 +1,4 @@ #nullable enable -#pragma warning disable CS1591 -#pragma warning disable CA1819 - -using System; -using System.Collections.Generic; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Updates; namespace MediaBrowser.Model.Configuration { diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index d3e29d6a4a..830c8bd102 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -233,7 +233,7 @@ namespace MediaBrowser.Model.Configuration /// Gets or sets a value indicating whether quick connect is available for use on this server. /// public bool QuickConnectAvailable { get; set; } = false; - + /// /// Gets or sets a value indicating whether access outside of the LAN is permitted. /// @@ -440,4 +440,4 @@ namespace MediaBrowser.Model.Configuration /// public int? ActivityLogRetentionDays { get; set; } = 30; } -} \ No newline at end of file +} -- cgit v1.2.3 From 7be3276dff834d1a3aa4199815f7e14864b06b5b Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Sat, 12 Dec 2020 18:24:25 +0800 Subject: Fine tune some tone mapping params *Set recommand algorithm to Hable *Set recommand tone mapping peak to 100 --- MediaBrowser.Model/Configuration/EncodingOptions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'MediaBrowser.Model/Configuration') diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index 100756c24f..38b3335107 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -88,11 +88,11 @@ namespace MediaBrowser.Model.Configuration // The left side of the dot is the platform number, and the right side is the device number on the platform. OpenclDevice = "0.0"; EnableTonemapping = false; - TonemappingAlgorithm = "reinhard"; + TonemappingAlgorithm = "hable"; TonemappingRange = "auto"; TonemappingDesat = 0; TonemappingThreshold = 0.8; - TonemappingPeak = 0; + TonemappingPeak = 100; TonemappingParam = 0; H264Crf = 23; H265Crf = 28; -- cgit v1.2.3 From 7986465cf785ca385fd1765326887e550bced033 Mon Sep 17 00:00:00 2001 From: Greenback Date: Sun, 6 Dec 2020 23:48:54 +0000 Subject: Initial upload --- Emby.Server.Implementations/ApplicationHost.cs | 250 ++------ .../Emby.Server.Implementations.csproj | 8 +- Emby.Server.Implementations/Plugins/Active.png | Bin 0 -> 1422 bytes .../Plugins/Malfunction.png | Bin 0 -> 2091 bytes .../Plugins/NotSupported.png | Bin 0 -> 2046 bytes .../Plugins/PluginManager.cs | 674 +++++++++++++++++++++ .../Plugins/PluginManifest.cs | 60 -- .../Plugins/RestartRequired.png | Bin 0 -> 1996 bytes Emby.Server.Implementations/Plugins/Superceded.png | Bin 0 -> 2136 bytes Emby.Server.Implementations/Plugins/blank.png | Bin 0 -> 120 bytes Emby.Server.Implementations/Plugins/disabled.png | Bin 0 -> 1790 bytes .../ScheduledTasks/Tasks/PluginUpdateTask.cs | 2 +- .../Updates/InstallationManager.cs | 456 +++++++------- Jellyfin.Api/Controllers/DashboardController.cs | 20 +- Jellyfin.Api/Controllers/PackageController.cs | 8 +- Jellyfin.Api/Controllers/PluginsController.cs | 288 ++++++--- Jellyfin.Api/Models/ConfigurationPageInfo.cs | 8 +- MediaBrowser.Common/IApplicationHost.cs | 42 +- MediaBrowser.Common/Plugins/BasePlugin.cs | 15 +- .../Plugins/IHasPluginConfiguration.cs | 33 + MediaBrowser.Common/Plugins/IPlugin.cs | 40 +- MediaBrowser.Common/Plugins/IPluginManager.cs | 86 +++ MediaBrowser.Common/Plugins/LocalPlugin.cs | 94 ++- MediaBrowser.Common/Plugins/PluginManifest.cs | 85 +++ .../Updates/IInstallationManager.cs | 32 +- .../Updates/InstallationEventArgs.cs | 11 +- .../Events/Updates/PluginUninstalledEventArgs.cs | 7 +- MediaBrowser.Controller/IServerApplicationHost.cs | 10 - .../Configuration/ServerConfiguration.cs | 10 + MediaBrowser.Model/Plugins/PluginInfo.cs | 39 +- MediaBrowser.Model/Plugins/PluginStatus.cs | 17 + MediaBrowser.Model/Updates/PackageInfo.cs | 43 +- MediaBrowser.Model/Updates/VersionInfo.cs | 42 +- MediaBrowser.sln | 5 +- 34 files changed, 1678 insertions(+), 707 deletions(-) create mode 100644 Emby.Server.Implementations/Plugins/Active.png create mode 100644 Emby.Server.Implementations/Plugins/Malfunction.png create mode 100644 Emby.Server.Implementations/Plugins/NotSupported.png create mode 100644 Emby.Server.Implementations/Plugins/PluginManager.cs delete mode 100644 Emby.Server.Implementations/Plugins/PluginManifest.cs create mode 100644 Emby.Server.Implementations/Plugins/RestartRequired.png create mode 100644 Emby.Server.Implementations/Plugins/Superceded.png create mode 100644 Emby.Server.Implementations/Plugins/blank.png create mode 100644 Emby.Server.Implementations/Plugins/disabled.png create mode 100644 MediaBrowser.Common/Plugins/IHasPluginConfiguration.cs create mode 100644 MediaBrowser.Common/Plugins/IPluginManager.cs create mode 100644 MediaBrowser.Common/Plugins/PluginManifest.cs create mode 100644 MediaBrowser.Model/Plugins/PluginStatus.cs (limited to 'MediaBrowser.Model/Configuration') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index d74ea03520..f88c6c6208 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -34,7 +34,6 @@ using Emby.Server.Implementations.LiveTv; using Emby.Server.Implementations.Localization; using Emby.Server.Implementations.Net; using Emby.Server.Implementations.Playlists; -using Emby.Server.Implementations.Plugins; using Emby.Server.Implementations.QuickConnect; using Emby.Server.Implementations.ScheduledTasks; using Emby.Server.Implementations.Security; @@ -119,7 +118,9 @@ namespace Emby.Server.Implementations private readonly IXmlSerializer _xmlSerializer; private readonly IJsonSerializer _jsonSerializer; private readonly IStartupOptions _startupOptions; + private readonly IPluginManager _pluginManager; + private List _creatingInstances; private IMediaEncoder _mediaEncoder; private ISessionManager _sessionManager; private string[] _urlPrefixes; @@ -181,16 +182,6 @@ namespace Emby.Server.Implementations protected IServiceCollection ServiceCollection { get; } - private IPlugin[] _plugins; - - private IReadOnlyList _pluginsManifests; - - /// - /// Gets the plugins. - /// - /// The plugins. - public IReadOnlyList Plugins => _plugins; - /// /// Gets the logger factory. /// @@ -294,6 +285,14 @@ namespace Emby.Server.Implementations ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version; ApplicationVersionString = ApplicationVersion.ToString(3); ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString; + + _pluginManager = new PluginManager( + LoggerFactory, + this, + ServerConfigurationManager.Configuration, + ApplicationPaths.PluginsPath, + ApplicationPaths.CachePath, + ApplicationVersion); } /// @@ -393,8 +392,26 @@ namespace Emby.Server.Implementations /// System.Object. protected object CreateInstanceSafe(Type type) { + if (_creatingInstances == null) + { + _creatingInstances = new List(); + } + + if (_creatingInstances.IndexOf(type) != -1) + { + Logger.LogError("DI Loop detected."); + Logger.LogError("Attempted creation of {Type}", type.FullName); + foreach (var entry in _creatingInstances) + { + Logger.LogError("Called from: {stack}", entry.FullName); + } + + throw new ExternalException("DI Loop detected."); + } + try { + _creatingInstances.Add(type); Logger.LogDebug("Creating instance of {Type}", type); return ActivatorUtilities.CreateInstance(ServiceProvider, type); } @@ -403,6 +420,10 @@ namespace Emby.Server.Implementations Logger.LogError(ex, "Error creating {Type}", type); return null; } + finally + { + _creatingInstances.Remove(type); + } } /// @@ -412,11 +433,7 @@ namespace Emby.Server.Implementations /// ``0. public T Resolve() => ServiceProvider.GetService(); - /// - /// Gets the export types. - /// - /// The type. - /// IEnumerable{Type}. + /// public IEnumerable GetExportTypes() { var currentType = typeof(T); @@ -445,6 +462,27 @@ namespace Emby.Server.Implementations return parts; } + /// + public IReadOnlyCollection GetExports(CreationDelegate defaultFunc, bool manageLifetime = true) + { + // Convert to list so this isn't executed for each iteration + var parts = GetExportTypes() + .Select(i => defaultFunc(i)) + .Where(i => i != null) + .Cast() + .ToList(); + + if (manageLifetime) + { + lock (_disposableParts) + { + _disposableParts.AddRange(parts.OfType()); + } + } + + return parts; + } + /// /// Runs the startup tasks. /// @@ -509,7 +547,7 @@ namespace Emby.Server.Implementations RegisterServices(); - RegisterPluginServices(); + _pluginManager.RegisterServices(ServiceCollection); } /// @@ -523,7 +561,7 @@ namespace Emby.Server.Implementations ServiceCollection.AddSingleton(ConfigurationManager); ServiceCollection.AddSingleton(this); - + ServiceCollection.AddSingleton(_pluginManager); ServiceCollection.AddSingleton(ApplicationPaths); ServiceCollection.AddSingleton(); @@ -768,34 +806,7 @@ namespace Emby.Server.Implementations } ConfigurationManager.AddParts(GetExports()); - _plugins = GetExports() - .Where(i => i != null) - .ToArray(); - - if (Plugins != null) - { - foreach (var plugin in Plugins) - { - if (_pluginsManifests != null && plugin is IPluginAssembly assemblyPlugin) - { - // Ensure the version number matches the Plugin Manifest information. - foreach (var item in _pluginsManifests) - { - if (Path.GetDirectoryName(plugin.AssemblyFilePath).Equals(item.Path, StringComparison.OrdinalIgnoreCase)) - { - // Update version number to that of the manifest. - assemblyPlugin.SetAttributes( - plugin.AssemblyFilePath, - Path.Combine(ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(plugin.AssemblyFilePath)), - item.Version); - break; - } - } - } - - Logger.LogInformation("Loaded plugin: {PluginName} {PluginVersion}", plugin.Name, plugin.Version); - } - } + _pluginManager.CreatePlugins(); _urlPrefixes = GetUrlPrefixes().ToArray(); @@ -834,22 +845,6 @@ namespace Emby.Server.Implementations _allConcreteTypes = GetTypes(GetComposablePartAssemblies()).ToArray(); } - private void RegisterPluginServices() - { - foreach (var pluginServiceRegistrator in GetExportTypes()) - { - try - { - var instance = (IPluginServiceRegistrator)Activator.CreateInstance(pluginServiceRegistrator); - instance.RegisterServices(ServiceCollection); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error registering plugin services from {Assembly}.", pluginServiceRegistrator.Assembly); - } - } - } - private IEnumerable GetTypes(IEnumerable assemblies) { foreach (var ass in assemblies) @@ -862,11 +857,13 @@ namespace Emby.Server.Implementations catch (FileNotFoundException ex) { Logger.LogError(ex, "Error getting exported types from {Assembly}", ass.FullName); + _pluginManager.FailPlugin(ass); continue; } catch (TypeLoadException ex) { Logger.LogError(ex, "Error loading types from {Assembly}.", ass.FullName); + _pluginManager.FailPlugin(ass); continue; } @@ -1005,129 +1002,15 @@ namespace Emby.Server.Implementations protected abstract void RestartInternal(); - /// - public IEnumerable GetLocalPlugins(string path, bool cleanup = true) - { - var minimumVersion = new Version(0, 0, 0, 1); - var versions = new List(); - if (!Directory.Exists(path)) - { - // Plugin path doesn't exist, don't try to enumerate subfolders. - return Enumerable.Empty(); - } - - var directories = Directory.EnumerateDirectories(path, "*.*", SearchOption.TopDirectoryOnly); - - foreach (var dir in directories) - { - try - { - var metafile = Path.Combine(dir, "meta.json"); - if (File.Exists(metafile)) - { - var manifest = _jsonSerializer.DeserializeFromFile(metafile); - - if (!Version.TryParse(manifest.TargetAbi, out var targetAbi)) - { - targetAbi = minimumVersion; - } - - if (!Version.TryParse(manifest.Version, out var version)) - { - version = minimumVersion; - } - - if (ApplicationVersion >= targetAbi) - { - // Only load Plugins if the plugin is built for this version or below. - versions.Add(new LocalPlugin(manifest.Guid, manifest.Name, version, dir)); - } - } - else - { - // No metafile, so lets see if the folder is versioned. - metafile = dir.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries)[^1]; - - int versionIndex = dir.LastIndexOf('_'); - if (versionIndex != -1 && Version.TryParse(dir.AsSpan()[(versionIndex + 1)..], out Version parsedVersion)) - { - // Versioned folder. - versions.Add(new LocalPlugin(Guid.Empty, metafile, parsedVersion, dir)); - } - else - { - // Un-versioned folder - Add it under the path name and version 0.0.0.1. - versions.Add(new LocalPlugin(Guid.Empty, metafile, minimumVersion, dir)); - } - } - } - catch - { - continue; - } - } - - string lastName = string.Empty; - versions.Sort(LocalPlugin.Compare); - // Traverse backwards through the list. - // The first item will be the latest version. - for (int x = versions.Count - 1; x >= 0; x--) - { - if (!string.Equals(lastName, versions[x].Name, StringComparison.OrdinalIgnoreCase)) - { - versions[x].DllFiles.AddRange(Directory.EnumerateFiles(versions[x].Path, "*.dll", SearchOption.AllDirectories)); - lastName = versions[x].Name; - continue; - } - - if (!string.IsNullOrEmpty(lastName) && cleanup) - { - // Attempt a cleanup of old folders. - try - { - Logger.LogDebug("Deleting {Path}", versions[x].Path); - Directory.Delete(versions[x].Path, true); - } - catch (Exception e) - { - Logger.LogWarning(e, "Unable to delete {Path}", versions[x].Path); - } - - versions.RemoveAt(x); - } - } - - return versions; - } - /// /// Gets the composable part assemblies. /// /// IEnumerable{Assembly}. protected IEnumerable GetComposablePartAssemblies() { - if (Directory.Exists(ApplicationPaths.PluginsPath)) + foreach (var p in _pluginManager.LoadAssemblies()) { - _pluginsManifests = GetLocalPlugins(ApplicationPaths.PluginsPath).ToList(); - foreach (var plugin in _pluginsManifests) - { - foreach (var file in plugin.DllFiles) - { - Assembly plugAss; - try - { - plugAss = Assembly.LoadFrom(file); - } - catch (FileLoadException ex) - { - Logger.LogError(ex, "Failed to load assembly {Path}", file); - continue; - } - - Logger.LogInformation("Loaded assembly {Assembly} from {Path}", plugAss.FullName, file); - yield return plugAss; - } - } + yield return p; } // Include composable parts in the Model assembly @@ -1369,17 +1252,6 @@ namespace Emby.Server.Implementations } } - /// - /// Removes the plugin. - /// - /// The plugin. - public void RemovePlugin(IPlugin plugin) - { - var list = _plugins.ToList(); - list.Remove(plugin); - _plugins = list.ToArray(); - } - public IEnumerable GetApiPluginAssemblies() { var assemblies = _allConcreteTypes diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 91c4648c62..dd60229825 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -73,6 +73,12 @@ + + + + + + + - diff --git a/Emby.Server.Implementations/Plugins/Active.png b/Emby.Server.Implementations/Plugins/Active.png new file mode 100644 index 0000000000..3722ee5200 Binary files /dev/null and b/Emby.Server.Implementations/Plugins/Active.png differ diff --git a/Emby.Server.Implementations/Plugins/Malfunction.png b/Emby.Server.Implementations/Plugins/Malfunction.png new file mode 100644 index 0000000000..d4726150eb Binary files /dev/null and b/Emby.Server.Implementations/Plugins/Malfunction.png differ diff --git a/Emby.Server.Implementations/Plugins/NotSupported.png b/Emby.Server.Implementations/Plugins/NotSupported.png new file mode 100644 index 0000000000..a13c1f7c1c Binary files /dev/null and b/Emby.Server.Implementations/Plugins/NotSupported.png differ diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs new file mode 100644 index 0000000000..8596dfcf8a --- /dev/null +++ b/Emby.Server.Implementations/Plugins/PluginManager.cs @@ -0,0 +1,674 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.Json; +using MediaBrowser.Common; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Json; +using MediaBrowser.Common.Plugins; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Plugins; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Emby.Server.Implementations +{ + /// + /// Defines the . + /// + public class PluginManager : IPluginManager + { + private const int OffsetFromTopRightCorner = 38; + + private readonly string _pluginsPath; + private readonly Version _appVersion; + private readonly JsonSerializerOptions _jsonOptions; + private readonly ILogger _logger; + private readonly IApplicationHost _appHost; + private readonly string _imagesPath; + private readonly ServerConfiguration _config; + private readonly IList _plugins; + private readonly Version _nextVersion; + private readonly Version _minimumVersion; + + /// + /// Initializes a new instance of the class. + /// + /// The . + /// The . + /// The . + /// The plugin path. + /// The image cache path. + /// The application version. + public PluginManager( + ILoggerFactory loggerfactory, + IApplicationHost appHost, + ServerConfiguration config, + string pluginsPath, + string imagesPath, + Version appVersion) + { + _logger = loggerfactory.CreateLogger(); + _pluginsPath = pluginsPath; + _appVersion = appVersion ?? throw new ArgumentNullException(nameof(appVersion)); + _jsonOptions = JsonDefaults.GetOptions(); + _jsonOptions.PropertyNameCaseInsensitive = true; + _jsonOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + _config = config; + _appHost = appHost; + _imagesPath = imagesPath; + _nextVersion = new Version(_appVersion.Major, _appVersion.Minor + 2, _appVersion.Build, _appVersion.Revision); + _minimumVersion = new Version(0, 0, 0, 1); + _plugins = Directory.Exists(_pluginsPath) ? DiscoverPlugins().ToList() : new List(); + } + + /// + /// Gets the Plugins. + /// + public IList Plugins => _plugins; + + /// + /// Returns all the assemblies. + /// + /// An IEnumerable{Assembly}. + public IEnumerable LoadAssemblies() + { + foreach (var plugin in _plugins) + { + foreach (var file in plugin.DllFiles) + { + try + { + plugin.Assembly = Assembly.LoadFrom(file); + } + catch (FileLoadException ex) + { + _logger.LogError(ex, "Failed to load assembly {Path}. Disabling plugin.", file); + ChangePluginState(plugin, PluginStatus.Malfunction); + continue; + } + + _logger.LogInformation("Loaded assembly {Assembly} from {Path}", plugin.Assembly.FullName, file); + yield return plugin.Assembly; + } + } + } + + /// + /// Creates all the plugin instances. + /// + public void CreatePlugins() + { + var createdPlugins = _appHost.GetExports(CreatePluginInstance) + .Where(i => i != null) + .ToArray(); + } + + /// + /// Registers the plugin's services with the DI. + /// Note: DI is not yet instantiated yet. + /// + /// A instance. + public void RegisterServices(IServiceCollection serviceCollection) + { + foreach (var pluginServiceRegistrator in _appHost.GetExportTypes()) + { + var plugin = GetPluginByType(pluginServiceRegistrator.Assembly.GetType()); + if (plugin == null) + { + throw new NullReferenceException(); + } + + CheckIfStillSuperceded(plugin); + if (!plugin.IsEnabledAndSupported) + { + continue; + } + + try + { + var instance = (IPluginServiceRegistrator?)Activator.CreateInstance(pluginServiceRegistrator); + instance?.RegisterServices(serviceCollection); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception ex) +#pragma warning restore CA1031 // Do not catch general exception types + { + _logger.LogError(ex, "Error registering plugin services from {Assembly}.", pluginServiceRegistrator.Assembly.FullName); + if (ChangePluginState(plugin, PluginStatus.Malfunction)) + { + _logger.LogInformation("Disabling plugin {Path}", plugin.Path); + } + } + } + } + + /// + /// Imports a plugin manifest from . + /// + /// Folder of the plugin. + public void ImportPluginFrom(string folder) + { + if (string.IsNullOrEmpty(folder)) + { + throw new ArgumentNullException(nameof(folder)); + } + + // Load the plugin. + var plugin = LoadManifest(folder); + // Make sure we haven't already loaded this. + if (plugin == null || _plugins.Any(p => p.Manifest.Equals(plugin.Manifest))) + { + return; + } + + _plugins.Add(plugin); + EnablePlugin(plugin); + } + + /// + /// Removes the plugin reference '. + /// + /// The plugin. + /// Outcome of the operation. + public bool RemovePlugin(LocalPlugin plugin) + { + if (plugin == null) + { + throw new ArgumentNullException(nameof(plugin)); + } + + plugin.Instance?.OnUninstalling(); + + if (DeletePlugin(plugin)) + { + return true; + } + + // Unable to delete, so disable. + return ChangePluginState(plugin, PluginStatus.Disabled); + } + + /// + /// Attempts to find the plugin with and id of . + /// + /// Id of plugin. + /// The version of the plugin to locate. + /// A if found, otherwise null. + /// Boolean value signifying the success of the search. + public bool TryGetPlugin(Guid id, Version? version, out LocalPlugin? plugin) + { + if (version == null) + { + // If no version is given, return the largest version number. (This is for backwards compatibility). + plugin = _plugins.Where(p => p.Id.Equals(id)).OrderByDescending(p => p.Version).FirstOrDefault(); + } + else + { + plugin = _plugins.FirstOrDefault(p => p.Id.Equals(id) && p.Version.Equals(version)); + } + + return plugin != null; + } + + /// + /// Enables the plugin, disabling all other versions. + /// + /// The of the plug to disable. + public void EnablePlugin(LocalPlugin plugin) + { + if (plugin == null) + { + throw new ArgumentNullException(nameof(plugin)); + } + + if (ChangePluginState(plugin, PluginStatus.Active)) + { + UpdateSuccessors(plugin); + } + } + + /// + /// Disable the plugin. + /// + /// The of the plug to disable. + public void DisablePlugin(LocalPlugin plugin) + { + if (plugin == null) + { + throw new ArgumentNullException(nameof(plugin)); + } + + // Update the manifest on disk + if (ChangePluginState(plugin, PluginStatus.Disabled)) + { + UpdateSuccessors(plugin); + } + } + + /// + /// Changes the status of the other versions of the plugin to "Superceded". + /// + /// The that's master. + private void UpdateSuccessors(LocalPlugin plugin) + { + // This value is memory only - so that the web will show restart required. + plugin.Manifest.Status = PluginStatus.RestartRequired; + + // Detect whether there is another version of this plugin that needs disabling. + var predecessor = _plugins.OrderByDescending(p => p.Version) + .FirstOrDefault( + p => p.Id.Equals(plugin.Id) + && p.Name.Equals(plugin.Name, StringComparison.OrdinalIgnoreCase) + && p.IsEnabledAndSupported + && p.Version != plugin.Version); + + if (predecessor == null) + { + return; + } + + if (!ChangePluginState(predecessor, PluginStatus.Superceded)) + { + _logger.LogError("Unable to disable version {Version} of {Name}", predecessor.Version, predecessor.Name); + } + } + + /// + /// Disable the plugin. + /// + /// The of the plug to disable. + public void FailPlugin(Assembly assembly) + { + // Only save if disabled. + if (assembly == null) + { + throw new ArgumentNullException(nameof(assembly)); + } + + var plugin = _plugins.Where(p => assembly.Equals(p.Assembly)).FirstOrDefault(); + if (plugin == null) + { + // A plugin's assembly didn't cause this issue, so ignore it. + return; + } + + ChangePluginState(plugin, PluginStatus.Malfunction); + } + + /// + /// Saves the manifest back to disk. + /// + /// The to save. + /// The path where to save the manifest. + /// True if successful. + public bool SaveManifest(PluginManifest manifest, string path) + { + if (manifest == null) + { + return false; + } + + try + { + var data = JsonSerializer.Serialize(manifest, _jsonOptions); + File.WriteAllText(Path.Combine(path, "meta.json"), data, Encoding.UTF8); + return true; + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception e) +#pragma warning restore CA1031 // Do not catch general exception types + { + _logger.LogWarning(e, "Unable to save plugin manifest. {Path}", path); + return false; + } + } + + /// + /// Changes a plugin's load status. + /// + /// The instance. + /// The of the plugin. + /// Success of the task. + private bool ChangePluginState(LocalPlugin plugin, PluginStatus state) + { + if (plugin.Manifest.Status == state || string.IsNullOrEmpty(plugin.Path)) + { + // No need to save as the state hasn't changed. + return true; + } + + plugin.Manifest.Status = state; + SaveManifest(plugin.Manifest, plugin.Path); + try + { + var data = JsonSerializer.Serialize(plugin.Manifest, _jsonOptions); + File.WriteAllText(Path.Combine(plugin.Path, "meta.json"), data, Encoding.UTF8); + return true; + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception e) +#pragma warning restore CA1031 // Do not catch general exception types + { + _logger.LogWarning(e, "Unable to disable plugin {Path}", plugin.Path); + return false; + } + } + + /// + /// Finds the plugin record using the type. + /// + /// The being sought. + /// The matching record, or null if not found. + private LocalPlugin? GetPluginByType(Type type) + { + // Find which plugin it is by the path. + return _plugins.FirstOrDefault(p => string.Equals(p.Path, Path.GetDirectoryName(type.Assembly.Location), StringComparison.Ordinal)); + } + + /// + /// Creates the instance safe. + /// + /// The type. + /// System.Object. + private object? CreatePluginInstance(Type type) + { + // Find the record for this plugin. + var plugin = GetPluginByType(type); + + if (plugin != null) + { + CheckIfStillSuperceded(plugin); + + if (plugin.IsEnabledAndSupported == true) + { + _logger.LogInformation("Skipping disabled plugin {Version} of {Name} ", plugin.Version, plugin.Name); + return null; + } + } + + try + { + _logger.LogDebug("Creating instance of {Type}", type); + var instance = ActivatorUtilities.CreateInstance(_appHost.ServiceProvider, type); + if (plugin == null) + { + // Create a dummy record for the providers. + var pInstance = (IPlugin)instance; + plugin = new LocalPlugin( + pInstance.AssemblyFilePath, + true, + new PluginManifest + { + Guid = pInstance.Id, + Status = PluginStatus.Active, + Name = pInstance.Name, + Version = pInstance.Version.ToString(), + MaxAbi = _nextVersion.ToString() + }) + { + Instance = pInstance + }; + + _plugins.Add(plugin); + + plugin.Manifest.Status = PluginStatus.Active; + } + else + { + plugin.Instance = (IPlugin)instance; + var manifest = plugin.Manifest; + var pluginStr = plugin.Instance.Version.ToString(); + if (string.Equals(manifest.Version, pluginStr, StringComparison.Ordinal)) + { + // If a plugin without a manifest failed to load due to an external issue (eg config), + // this updates the manifest to the actual plugin values. + manifest.Version = pluginStr; + manifest.Name = plugin.Instance.Name; + manifest.Description = plugin.Instance.Description; + } + + manifest.Status = PluginStatus.Active; + SaveManifest(manifest, plugin.Path); + } + + _logger.LogInformation("Loaded plugin: {PluginName} {PluginVersion}", plugin.Name, plugin.Version); + + return instance; + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception ex) +#pragma warning restore CA1031 // Do not catch general exception types + { + _logger.LogError(ex, "Error creating {Type}", type.FullName); + if (plugin != null) + { + if (ChangePluginState(plugin, PluginStatus.Malfunction)) + { + _logger.LogInformation("Plugin {Path} has been disabled.", plugin.Path); + return null; + } + } + + _logger.LogDebug("Unable to auto-disable."); + return null; + } + } + + private void CheckIfStillSuperceded(LocalPlugin plugin) + { + if (plugin.Manifest.Status != PluginStatus.Superceded) + { + return; + } + + var predecessor = _plugins.OrderByDescending(p => p.Version) + .FirstOrDefault(p => p.Id.Equals(plugin.Id) && p.IsEnabledAndSupported && p.Version != plugin.Version); + if (predecessor != null) + { + return; + } + + plugin.Manifest.Status = PluginStatus.Active; + } + + /// + /// Attempts to delete a plugin. + /// + /// A instance to delete. + /// True if successful. + private bool DeletePlugin(LocalPlugin plugin) + { + // Attempt a cleanup of old folders. + try + { + _logger.LogDebug("Deleting {Path}", plugin.Path); + Directory.Delete(plugin.Path, true); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception e) +#pragma warning restore CA1031 // Do not catch general exception types + { + _logger.LogWarning(e, "Unable to delete {Path}", plugin.Path); + return false; + } + + return _plugins.Remove(plugin); + } + + private LocalPlugin? LoadManifest(string dir) + { + try + { + Version? version; + PluginManifest? manifest = null; + var metafile = Path.Combine(dir, "meta.json"); + if (File.Exists(metafile)) + { + try + { + var data = File.ReadAllText(metafile, Encoding.UTF8); + manifest = JsonSerializer.Deserialize(data, _jsonOptions); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception ex) +#pragma warning restore CA1031 // Do not catch general exception types + { + _logger.LogError(ex, "Error deserializing {Path}.", dir); + } + } + + if (manifest != null) + { + if (!Version.TryParse(manifest.TargetAbi, out var targetAbi)) + { + targetAbi = _minimumVersion; + } + + if (!Version.TryParse(manifest.MaxAbi, out var maxAbi)) + { + maxAbi = _appVersion; + } + + if (!Version.TryParse(manifest.Version, out version)) + { + manifest.Version = _minimumVersion.ToString(); + } + + return new LocalPlugin(dir, _appVersion >= targetAbi && _appVersion <= maxAbi, manifest); + } + + // No metafile, so lets see if the folder is versioned. + // TODO: Phase this support out in future versions. + metafile = dir.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries)[^1]; + int versionIndex = dir.LastIndexOf('_'); + if (versionIndex != -1) + { + // Get the version number from the filename if possible. + metafile = Path.GetFileName(dir[..versionIndex]) ?? dir[..versionIndex]; + version = Version.TryParse(dir.AsSpan()[(versionIndex + 1)..], out Version? parsedVersion) ? parsedVersion : _appVersion; + } + else + { + // Un-versioned folder - Add it under the path name and version it suitable for this instance. + version = _appVersion; + } + + // Auto-create a plugin manifest, so we can disable it, if it fails to load. + // NOTE: This Plugin is marked as valid for two upgrades, at which point, it can be assumed the + // code base will have changed sufficiently to make it invalid. + manifest = new PluginManifest + { + Status = PluginStatus.RestartRequired, + Name = metafile, + AutoUpdate = false, + Guid = metafile.GetMD5(), + TargetAbi = _appVersion.ToString(), + MaxAbi = _nextVersion.ToString(), + Version = version.ToString() + }; + + return new LocalPlugin(dir, true, manifest); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception ex) +#pragma warning restore CA1031 // Do not catch general exception types + { + _logger.LogError(ex, "Something went wrong!"); + return null; + } + } + + /// + /// Gets the list of local plugins. + /// + /// Enumerable of local plugins. + private IEnumerable DiscoverPlugins() + { + var versions = new List(); + + if (!Directory.Exists(_pluginsPath)) + { + // Plugin path doesn't exist, don't try to enumerate sub-folders. + return Enumerable.Empty(); + } + + var directories = Directory.EnumerateDirectories(_pluginsPath, "*.*", SearchOption.TopDirectoryOnly); + LocalPlugin? entry; + foreach (var dir in directories) + { + entry = LoadManifest(dir); + if (entry != null) + { + versions.Add(entry); + } + } + + string lastName = string.Empty; + versions.Sort(LocalPlugin.Compare); + // Traverse backwards through the list. + // The first item will be the latest version. + for (int x = versions.Count - 1; x >= 0; x--) + { + entry = versions[x]; + if (!string.Equals(lastName, entry.Name, StringComparison.OrdinalIgnoreCase)) + { + entry.DllFiles.AddRange(Directory.EnumerateFiles(entry.Path, "*.dll", SearchOption.AllDirectories)); + if (entry.IsEnabledAndSupported) + { + lastName = entry.Name; + continue; + } + } + + if (string.IsNullOrEmpty(lastName)) + { + continue; + } + + var manifest = entry.Manifest; + var cleaned = false; + var path = entry.Path; + if (_config.RemoveOldPlugins) + { + // Attempt a cleanup of old folders. + try + { + _logger.LogDebug("Deleting {Path}", path); + Directory.Delete(path, true); + cleaned = true; + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception e) +#pragma warning restore CA1031 // Do not catch general exception types + { + _logger.LogWarning(e, "Unable to delete {Path}", path); + } + + versions.RemoveAt(x); + } + + if (!cleaned) + { + if (manifest == null) + { + _logger.LogWarning("Unable to disable plugin {Path}", entry.Path); + continue; + } + + // Update the manifest so its not loaded next time. + manifest.Status = PluginStatus.Disabled; + SaveManifest(manifest, entry.Path); + } + } + + // Only want plugin folders which have files. + return versions.Where(p => p.DllFiles.Count != 0); + } + } +} diff --git a/Emby.Server.Implementations/Plugins/PluginManifest.cs b/Emby.Server.Implementations/Plugins/PluginManifest.cs deleted file mode 100644 index 33762791bc..0000000000 --- a/Emby.Server.Implementations/Plugins/PluginManifest.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; - -namespace Emby.Server.Implementations.Plugins -{ - /// - /// Defines a Plugin manifest file. - /// - public class PluginManifest - { - /// - /// Gets or sets the category of the plugin. - /// - public string Category { get; set; } - - /// - /// Gets or sets the changelog information. - /// - public string Changelog { get; set; } - - /// - /// Gets or sets the description of the plugin. - /// - public string Description { get; set; } - - /// - /// Gets or sets the Global Unique Identifier for the plugin. - /// - public Guid Guid { get; set; } - - /// - /// Gets or sets the Name of the plugin. - /// - public string Name { get; set; } - - /// - /// Gets or sets an overview of the plugin. - /// - public string Overview { get; set; } - - /// - /// Gets or sets the owner of the plugin. - /// - public string Owner { get; set; } - - /// - /// Gets or sets the compatibility version for the plugin. - /// - public string TargetAbi { get; set; } - - /// - /// Gets or sets the timestamp of the plugin. - /// - public DateTime Timestamp { get; set; } - - /// - /// Gets or sets the Version number of the plugin. - /// - public string Version { get; set; } - } -} diff --git a/Emby.Server.Implementations/Plugins/RestartRequired.png b/Emby.Server.Implementations/Plugins/RestartRequired.png new file mode 100644 index 0000000000..65fd102a2c Binary files /dev/null and b/Emby.Server.Implementations/Plugins/RestartRequired.png differ diff --git a/Emby.Server.Implementations/Plugins/Superceded.png b/Emby.Server.Implementations/Plugins/Superceded.png new file mode 100644 index 0000000000..251e70535b Binary files /dev/null and b/Emby.Server.Implementations/Plugins/Superceded.png differ diff --git a/Emby.Server.Implementations/Plugins/blank.png b/Emby.Server.Implementations/Plugins/blank.png new file mode 100644 index 0000000000..f81ae32438 Binary files /dev/null and b/Emby.Server.Implementations/Plugins/blank.png differ diff --git a/Emby.Server.Implementations/Plugins/disabled.png b/Emby.Server.Implementations/Plugins/disabled.png new file mode 100644 index 0000000000..eeb8ffefc2 Binary files /dev/null and b/Emby.Server.Implementations/Plugins/disabled.png differ diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs index 161fa05809..a69380cbb7 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs @@ -8,10 +8,10 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Updates; +using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Net; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; -using MediaBrowser.Model.Globalization; namespace Emby.Server.Implementations.ScheduledTasks { diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index ef346dd5d6..b0a1750bdd 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS1591 +#nullable enable using System; using System.Collections.Concurrent; @@ -12,7 +12,6 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Events; -using MediaBrowser.Common; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Json; using MediaBrowser.Common.Net; @@ -41,17 +40,15 @@ namespace Emby.Server.Implementations.Updates private readonly IEventManager _eventManager; private readonly IHttpClientFactory _httpClientFactory; private readonly IServerConfigurationManager _config; - private readonly IFileSystem _fileSystem; private readonly JsonSerializerOptions _jsonSerializerOptions; + private readonly IPluginManager _pluginManager; /// /// Gets the application host. /// /// The application host. private readonly IServerApplicationHost _applicationHost; - private readonly IZipClient _zipClient; - private readonly object _currentInstallationsLock = new object(); /// @@ -64,6 +61,17 @@ namespace Emby.Server.Implementations.Updates /// private readonly ConcurrentBag _completedInstallationsInternal; + /// + /// Initializes a new instance of the class. + /// + /// The . + /// The . + /// The . + /// The . + /// The . + /// The . + /// The . + /// The . public InstallationManager( ILogger logger, IServerApplicationHost appHost, @@ -71,8 +79,8 @@ namespace Emby.Server.Implementations.Updates IEventManager eventManager, IHttpClientFactory httpClientFactory, IServerConfigurationManager config, - IFileSystem fileSystem, - IZipClient zipClient) + IZipClient zipClient, + IPluginManager pluginManager) { _currentInstallations = new List<(InstallationInfo, CancellationTokenSource)>(); _completedInstallationsInternal = new ConcurrentBag(); @@ -83,16 +91,17 @@ namespace Emby.Server.Implementations.Updates _eventManager = eventManager; _httpClientFactory = httpClientFactory; _config = config; - _fileSystem = fileSystem; _zipClient = zipClient; _jsonSerializerOptions = JsonDefaults.GetOptions(); + _jsonSerializerOptions.PropertyNameCaseInsensitive = true; + _pluginManager = pluginManager; } /// public IEnumerable CompletedInstallations => _completedInstallationsInternal; /// - public async Task> GetPackages(string manifestName, string manifest, CancellationToken cancellationToken = default) + public async Task> GetPackages(string manifestName, string manifest, bool filterIncompatible, CancellationToken cancellationToken = default) { try { @@ -103,13 +112,39 @@ namespace Emby.Server.Implementations.Updates return Array.Empty(); } + var minimumVersion = new Version(0, 0, 0, 1); // Store the repository and repository url with each version, as they may be spread apart. foreach (var entry in packages) { - foreach (var ver in entry.versions) + for (int a = entry.Versions.Count - 1; a >= 0; a--) { - ver.repositoryName = manifestName; - ver.repositoryUrl = manifest; + var ver = entry.Versions[a]; + ver.RepositoryName = manifestName; + ver.RepositoryUrl = manifest; + + if (!filterIncompatible) + { + continue; + } + + if (!Version.TryParse(ver.TargetAbi, out var targetAbi)) + { + targetAbi = minimumVersion; + } + + if (!Version.TryParse(ver.MaxAbi, out var maxAbi)) + { + maxAbi = _applicationHost.ApplicationVersion; + } + + // Only show plugins that fall between targetAbi and maxAbi + if (_applicationHost.ApplicationVersion >= targetAbi && _applicationHost.ApplicationVersion <= maxAbi) + { + continue; + } + + // Not compatible with this version so remove it. + entry.Versions.Remove(ver); } } @@ -132,69 +167,61 @@ namespace Emby.Server.Implementations.Updates } } - private static void MergeSort(IList source, IList dest) - { - int sLength = source.Count - 1; - int dLength = dest.Count; - int s = 0, d = 0; - var sourceVersion = source[0].VersionNumber; - var destVersion = dest[0].VersionNumber; - - while (d < dLength) - { - if (sourceVersion.CompareTo(destVersion) >= 0) - { - if (s < sLength) - { - sourceVersion = source[++s].VersionNumber; - } - else - { - // Append all of destination to the end of source. - while (d < dLength) - { - source.Add(dest[d++]); - } - - break; - } - } - else - { - source.Insert(s++, dest[d++]); - if (d >= dLength) - { - break; - } - - sLength++; - destVersion = dest[d].VersionNumber; - } - } - } - /// public async Task> GetAvailablePackages(CancellationToken cancellationToken = default) { var result = new List(); foreach (RepositoryInfo repository in _config.Configuration.PluginRepositories) { - if (repository.Enabled) + if (repository.Enabled && repository.Url != null) { - // Where repositories have the same content, the details of the first is taken. - foreach (var package in await GetPackages(repository.Name, repository.Url, cancellationToken).ConfigureAwait(true)) + // Where repositories have the same content, the details from the first is taken. + foreach (var package in await GetPackages(repository.Name ?? "Unnamed Repo", repository.Url, true, cancellationToken).ConfigureAwait(true)) { - if (!Guid.TryParse(package.guid, out var packageGuid)) + if (!Guid.TryParse(package.Guid, out var packageGuid)) { // Package doesn't have a valid GUID, skip. continue; } - var existing = FilterPackages(result, package.name, packageGuid).FirstOrDefault(); + var existing = FilterPackages(result, package.Name, packageGuid).FirstOrDefault(); + + // Remove invalid versions from the valid package. + for (var i = package.Versions.Count - 1; i >= 0; i--) + { + var version = package.Versions[i]; + + // Update the manifests, if anything changes. + if (_pluginManager.TryGetPlugin(packageGuid, version.VersionNumber, out LocalPlugin? plugin)) + { + bool noChange = string.Equals(plugin!.Manifest.MaxAbi, version.MaxAbi, StringComparison.Ordinal) + || string.Equals(plugin.Manifest.TargetAbi, version.TargetAbi, StringComparison.Ordinal); + if (!noChange) + { + plugin.Manifest.MaxAbi = version.MaxAbi ?? string.Empty; + plugin.Manifest.TargetAbi = version.TargetAbi ?? string.Empty; + _pluginManager.SaveManifest(plugin.Manifest, plugin.Path); + } + } + + // Remove versions with a target abi that is greater then the current application version. + if ((Version.TryParse(version.TargetAbi, out var targetAbi) && _applicationHost.ApplicationVersion < targetAbi) + || (Version.TryParse(version.MaxAbi, out var maxAbi) && _applicationHost.ApplicationVersion > maxAbi)) + { + package.Versions.RemoveAt(i); + } + } + + // Don't add a package that doesn't have any compatible versions. + if (package.Versions.Count == 0) + { + continue; + } + if (existing != null) { // Assumption is both lists are ordered, so slot these into the correct place. - MergeSort(existing.versions, package.versions); + MergeSortedList(existing.Versions, package.Versions); } else { @@ -210,23 +237,23 @@ namespace Emby.Server.Implementations.Updates /// public IEnumerable FilterPackages( IEnumerable availablePackages, - string name = null, - Guid guid = default, - Version specificVersion = null) + string? name = null, + Guid? guid = default, + Version? specificVersion = null) { if (name != null) { - availablePackages = availablePackages.Where(x => x.name.Equals(name, StringComparison.OrdinalIgnoreCase)); + availablePackages = availablePackages.Where(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); } if (guid != Guid.Empty) { - availablePackages = availablePackages.Where(x => Guid.Parse(x.guid) == guid); + availablePackages = availablePackages.Where(x => Guid.Parse(x.Guid) == guid); } if (specificVersion != null) { - availablePackages = availablePackages.Where(x => x.versions.Where(y => y.VersionNumber.Equals(specificVersion)).Any()); + availablePackages = availablePackages.Where(x => x.Versions.Any(y => y.VersionNumber.Equals(specificVersion))); } return availablePackages; @@ -235,10 +262,10 @@ namespace Emby.Server.Implementations.Updates /// public IEnumerable GetCompatibleVersions( IEnumerable availablePackages, - string name = null, - Guid guid = default, - Version minVersion = null, - Version specificVersion = null) + string? name = null, + Guid? guid = default, + Version? minVersion = null, + Version? specificVersion = null) { var package = FilterPackages(availablePackages, name, guid, specificVersion).FirstOrDefault(); @@ -249,8 +276,9 @@ namespace Emby.Server.Implementations.Updates } var appVer = _applicationHost.ApplicationVersion; - var availableVersions = package.versions - .Where(x => Version.Parse(x.targetAbi) <= appVer); + var availableVersions = package.Versions + .Where(x => (string.IsNullOrEmpty(x.TargetAbi) || Version.Parse(x.TargetAbi) <= appVer) + && (string.IsNullOrEmpty(x.MaxAbi) || Version.Parse(x.MaxAbi) >= appVer)); if (specificVersion != null) { @@ -265,12 +293,12 @@ namespace Emby.Server.Implementations.Updates { yield return new InstallationInfo { - Changelog = v.changelog, - Guid = new Guid(package.guid), - Name = package.name, + Changelog = v.Changelog, + Guid = new Guid(package.Guid), + Name = package.Name, Version = v.VersionNumber, - SourceUrl = v.sourceUrl, - Checksum = v.checksum + SourceUrl = v.SourceUrl, + Checksum = v.Checksum }; } } @@ -282,20 +310,6 @@ namespace Emby.Server.Implementations.Updates return GetAvailablePluginUpdates(catalog); } - private IEnumerable GetAvailablePluginUpdates(IReadOnlyList pluginCatalog) - { - var plugins = _applicationHost.GetLocalPlugins(_appPaths.PluginsPath); - foreach (var plugin in plugins) - { - var compatibleVersions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, minVersion: plugin.Version); - var version = compatibleVersions.FirstOrDefault(y => y.Version > plugin.Version); - if (version != null && CompletedInstallations.All(x => x.Guid != version.Guid)) - { - yield return version; - } - } - } - /// public async Task InstallPackage(InstallationInfo package, CancellationToken cancellationToken) { @@ -373,24 +387,140 @@ namespace Emby.Server.Implementations.Updates } /// - /// Installs the package internal. + /// Uninstalls a plugin. /// - /// The package. - /// The cancellation token. - /// . - private async Task InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken) + /// The to uninstall. + public void UninstallPlugin(LocalPlugin plugin) { - // Set last update time if we were installed before - IPlugin plugin = _applicationHost.Plugins.FirstOrDefault(p => p.Id == package.Guid) - ?? _applicationHost.Plugins.FirstOrDefault(p => p.Name.Equals(package.Name, StringComparison.OrdinalIgnoreCase)); + if (plugin == null) + { + return; + } - // Do the install - await PerformPackageInstallation(package, cancellationToken).ConfigureAwait(false); + if (plugin.Instance?.CanUninstall == false) + { + _logger.LogWarning("Attempt to delete non removable plugin {0}, ignoring request", plugin.Name); + return; + } - // Do plugin-specific processing - _logger.LogInformation(plugin == null ? "New plugin installed: {0} {1}" : "Plugin updated: {0} {1}", package.Name, package.Version); + plugin.Instance?.OnUninstalling(); - return plugin != null; + // Remove it the quick way for now + _pluginManager.RemovePlugin(plugin); + + _eventManager.Publish(new PluginUninstalledEventArgs(plugin.GetPluginInfo())); + + _applicationHost.NotifyPendingRestart(); + } + + /// + public bool CancelInstallation(Guid id) + { + lock (_currentInstallationsLock) + { + var install = _currentInstallations.Find(x => x.info.Guid == id); + if (install == default((InstallationInfo, CancellationTokenSource))) + { + return false; + } + + install.token.Cancel(); + _currentInstallations.Remove(install); + return true; + } + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and optionally managed resources. + /// + /// true to release both managed and unmanaged resources or false to release only unmanaged resources. + protected virtual void Dispose(bool dispose) + { + if (dispose) + { + lock (_currentInstallationsLock) + { + foreach (var (info, token) in _currentInstallations) + { + token.Dispose(); + } + + _currentInstallations.Clear(); + } + } + } + + /// + /// Merges two sorted lists. + /// + /// The source instance to merge. + /// The destination instance to merge with. + private static void MergeSortedList(IList source, IList dest) + { + int sLength = source.Count - 1; + int dLength = dest.Count; + int s = 0, d = 0; + var sourceVersion = source[0].VersionNumber; + var destVersion = dest[0].VersionNumber; + + while (d < dLength) + { + if (sourceVersion.CompareTo(destVersion) >= 0) + { + if (s < sLength) + { + sourceVersion = source[++s].VersionNumber; + } + else + { + // Append all of destination to the end of source. + while (d < dLength) + { + source.Add(dest[d++]); + } + + break; + } + } + else + { + source.Insert(s++, dest[d++]); + if (d >= dLength) + { + break; + } + + sLength++; + destVersion = dest[d].VersionNumber; + } + } + } + + private IEnumerable GetAvailablePluginUpdates(IReadOnlyList pluginCatalog) + { + var plugins = _pluginManager.Plugins; + foreach (var plugin in plugins) + { + if (plugin.Manifest?.AutoUpdate == false) + { + continue; + } + + var compatibleVersions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, minVersion: plugin.Version); + var version = compatibleVersions.FirstOrDefault(y => y.Version > plugin.Version); + + if (version != null && CompletedInstallations.All(x => x.Guid != version.Guid)) + { + yield return version; + } + } } private async Task PerformPackageInstallation(InstallationInfo package, CancellationToken cancellationToken) @@ -434,7 +564,9 @@ namespace Emby.Server.Implementations.Updates { Directory.Delete(targetDir, true); } +#pragma warning disable CA1031 // Do not catch general exception types catch +#pragma warning restore CA1031 // Do not catch general exception types { // Ignore any exceptions. } @@ -442,119 +574,27 @@ namespace Emby.Server.Implementations.Updates stream.Position = 0; _zipClient.ExtractAllFromZip(stream, targetDir, true); - -#pragma warning restore CA5351 + _pluginManager.ImportPluginFrom(targetDir); } - /// - /// Uninstalls a plugin. - /// - /// The plugin. - public void UninstallPlugin(IPlugin plugin) - { - if (!plugin.CanUninstall) - { - _logger.LogWarning("Attempt to delete non removable plugin {0}, ignoring request", plugin.Name); - return; - } - - plugin.OnUninstalling(); - - // Remove it the quick way for now - _applicationHost.RemovePlugin(plugin); - - var path = plugin.AssemblyFilePath; - bool isDirectory = false; - // Check if we have a plugin directory we should remove too - if (Path.GetDirectoryName(plugin.AssemblyFilePath) != _appPaths.PluginsPath) - { - path = Path.GetDirectoryName(plugin.AssemblyFilePath); - isDirectory = true; - } - - // Make this case-insensitive to account for possible incorrect assembly naming - var file = _fileSystem.GetFilePaths(Path.GetDirectoryName(path)) - .FirstOrDefault(i => string.Equals(i, path, StringComparison.OrdinalIgnoreCase)); - - if (!string.IsNullOrWhiteSpace(file)) - { - path = file; - } - - try - { - if (isDirectory) - { - _logger.LogInformation("Deleting plugin directory {0}", path); - Directory.Delete(path, true); - } - else - { - _logger.LogInformation("Deleting plugin file {0}", path); - _fileSystem.DeleteFile(path); - } - } - catch - { - // Ignore file errors. - } - - var list = _config.Configuration.UninstalledPlugins.ToList(); - var filename = Path.GetFileName(path); - if (!list.Contains(filename, StringComparer.OrdinalIgnoreCase)) - { - list.Add(filename); - _config.Configuration.UninstalledPlugins = list.ToArray(); - _config.SaveConfiguration(); - } - - _eventManager.Publish(new PluginUninstalledEventArgs(plugin)); - - _applicationHost.NotifyPendingRestart(); - } - - /// - public bool CancelInstallation(Guid id) + private async Task InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken) { - lock (_currentInstallationsLock) + // Set last update time if we were installed before + LocalPlugin? plugin = _pluginManager.Plugins.FirstOrDefault(p => p.Id.Equals(package.Guid) && p.Version.Equals(package.Version)) + ?? _pluginManager.Plugins.FirstOrDefault(p => p.Name.Equals(package.Name, StringComparison.OrdinalIgnoreCase) && p.Version.Equals(package.Version)); + if (plugin != null) { - var install = _currentInstallations.Find(x => x.info.Guid == id); - if (install == default((InstallationInfo, CancellationTokenSource))) - { - return false; - } - - install.token.Cancel(); - _currentInstallations.Remove(install); - return true; + plugin.Manifest.Timestamp = DateTime.UtcNow; + _pluginManager.SaveManifest(plugin.Manifest, plugin.Path); } - } - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } + // Do the install + await PerformPackageInstallation(package, cancellationToken).ConfigureAwait(false); - /// - /// Releases unmanaged and optionally managed resources. - /// - /// true to release both managed and unmanaged resources or false to release only unmanaged resources. - protected virtual void Dispose(bool dispose) - { - if (dispose) - { - lock (_currentInstallationsLock) - { - foreach (var tuple in _currentInstallations) - { - tuple.token.Dispose(); - } + // Do plugin-specific processing + _logger.LogInformation(plugin == null ? "New plugin installed: {0} {1}" : "Plugin updated: {0} {1}", package.Name, package.Version); - _currentInstallations.Clear(); - } - } + return plugin != null; } } } diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs index ccc81dfc55..b77d79209a 100644 --- a/Jellyfin.Api/Controllers/DashboardController.cs +++ b/Jellyfin.Api/Controllers/DashboardController.cs @@ -29,18 +29,22 @@ namespace Jellyfin.Api.Controllers { private readonly ILogger _logger; private readonly IServerApplicationHost _appHost; + private readonly IPluginManager _pluginManager; /// /// Initializes a new instance of the class. /// /// Instance of interface. /// Instance of interface. + /// Instance of interface. public DashboardController( ILogger logger, - IServerApplicationHost appHost) + IServerApplicationHost appHost, + IPluginManager pluginManager) { _logger = logger; _appHost = appHost; + _pluginManager = pluginManager; } /// @@ -83,7 +87,7 @@ namespace Jellyfin.Api.Controllers .Where(i => i != null) .ToList(); - configPages.AddRange(_appHost.Plugins.SelectMany(GetConfigPages)); + configPages.AddRange(_pluginManager.Plugins.SelectMany(GetConfigPages)); if (pageType.HasValue) { @@ -155,24 +159,24 @@ namespace Jellyfin.Api.Controllers return NotFound(); } - private IEnumerable GetConfigPages(IPlugin plugin) + private IEnumerable GetConfigPages(LocalPlugin plugin) { - return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin, i.Item1)); + return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin.Instance, i.Item1)); } - private IEnumerable> GetPluginPages(IPlugin plugin) + private IEnumerable> GetPluginPages(LocalPlugin? plugin) { - if (!(plugin is IHasWebPages hasWebPages)) + if (plugin?.Instance is not IHasWebPages hasWebPages) { return new List>(); } - return hasWebPages.GetPages().Select(i => new Tuple(i, plugin)); + return hasWebPages.GetPages().Select(i => new Tuple(i, plugin.Instance)); } private IEnumerable> GetPluginPages() { - return _appHost.Plugins.SelectMany(GetPluginPages); + return _pluginManager.Plugins.SelectMany(GetPluginPages); } } } diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs index 6295dfc05d..622a0fe00d 100644 --- a/Jellyfin.Api/Controllers/PackageController.cs +++ b/Jellyfin.Api/Controllers/PackageController.cs @@ -2,8 +2,11 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; +using System.Net.Mime; using System.Threading.Tasks; +using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; +using MediaBrowser.Common.Json; using MediaBrowser.Common.Updates; using MediaBrowser.Controller.Configuration; using MediaBrowser.Model.Updates; @@ -43,6 +46,7 @@ namespace Jellyfin.Api.Controllers /// A containing package information. [HttpGet("Packages/{name}")] [ProducesResponseType(StatusCodes.Status200OK)] + [Produces(JsonDefaults.CamelCaseMediaType)] public async Task> GetPackageInfo( [FromRoute, Required] string name, [FromQuery] Guid? assemblyGuid) @@ -69,6 +73,7 @@ namespace Jellyfin.Api.Controllers /// An containing available packages information. [HttpGet("Packages")] [ProducesResponseType(StatusCodes.Status200OK)] + [Produces(JsonDefaults.CamelCaseMediaType)] public async Task> GetPackages() { IEnumerable packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false); @@ -99,7 +104,7 @@ namespace Jellyfin.Api.Controllers var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false); if (!string.IsNullOrEmpty(repositoryUrl)) { - packages = packages.Where(p => p.versions.Where(q => q.repositoryUrl.Equals(repositoryUrl, StringComparison.OrdinalIgnoreCase)).Any()) + packages = packages.Where(p => p.Versions.Any(q => q.RepositoryUrl.Equals(repositoryUrl, StringComparison.OrdinalIgnoreCase))) .ToList(); } @@ -143,6 +148,7 @@ namespace Jellyfin.Api.Controllers /// An containing the list of package repositories. [HttpGet("Repositories")] [ProducesResponseType(StatusCodes.Status200OK)] + [Produces(JsonDefaults.CamelCaseMediaType)] public ActionResult> GetRepositories() { return _serverConfigurationManager.Configuration.PluginRepositories; diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs index 98f1bc2d23..3f366dd79f 100644 --- a/Jellyfin.Api/Controllers/PluginsController.cs +++ b/Jellyfin.Api/Controllers/PluginsController.cs @@ -1,15 +1,22 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Globalization; +using System.IO; using System.Linq; +using System.Net.Mime; using System.Text.Json; using System.Threading.Tasks; +using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Models.PluginDtos; -using MediaBrowser.Common; +using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Json; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Updates; +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Net; using MediaBrowser.Model.Plugins; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; @@ -23,22 +30,81 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.DefaultAuthorization)] public class PluginsController : BaseJellyfinApiController { - private readonly IApplicationHost _appHost; private readonly IInstallationManager _installationManager; - - private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.GetOptions(); + private readonly IPluginManager _pluginManager; + private readonly IConfigurationManager _config; + private readonly JsonSerializerOptions _serializerOptions; /// /// Initializes a new instance of the class. /// - /// Instance of the interface. /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. public PluginsController( - IApplicationHost appHost, - IInstallationManager installationManager) + IInstallationManager installationManager, + IPluginManager pluginManager, + IConfigurationManager config) { - _appHost = appHost; _installationManager = installationManager; + _pluginManager = pluginManager; + _serializerOptions = JsonDefaults.GetOptions(); + _config = config; + } + + /// + /// Get plugin security info. + /// + /// Plugin security info returned. + /// Plugin security info. + [Obsolete("This endpoint should not be used.")] + [HttpGet("SecurityInfo")] + [ProducesResponseType(StatusCodes.Status200OK)] + public static ActionResult GetPluginSecurityInfo() + { + return new PluginSecurityInfo + { + IsMbSupporter = true, + SupporterKey = "IAmTotallyLegit" + }; + } + + /// + /// Gets registration status for a feature. + /// + /// Feature name. + /// Registration status returned. + /// Mb registration record. + [Obsolete("This endpoint should not be used.")] + [HttpPost("RegistrationRecords/{name}")] + [ProducesResponseType(StatusCodes.Status200OK)] + public static ActionResult GetRegistrationStatus([FromRoute, Required] string name) + { + return new MBRegistrationRecord + { + IsRegistered = true, + RegChecked = true, + TrialVersion = false, + IsValid = true, + RegError = false + }; + } + + /// + /// Gets registration status for a feature. + /// + /// Feature name. + /// Not implemented. + /// Not Implemented. + /// This endpoint is not implemented. + [Obsolete("Paid plugins are not supported")] + [HttpGet("Registrations/{name}")] + [ProducesResponseType(StatusCodes.Status501NotImplemented)] + public static ActionResult GetRegistration([FromRoute, Required] string name) + { + // TODO Once we have proper apps and plugins and decide to break compatibility with paid plugins, + // delete all these registration endpoints. They are only kept for compatibility. + throw new NotImplementedException(); } /// @@ -48,15 +114,65 @@ namespace Jellyfin.Api.Controllers /// List of currently installed plugins. [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesFile(MediaTypeNames.Application.Json)] public ActionResult> GetPlugins() { - return Ok(_appHost.Plugins.OrderBy(p => p.Name).Select(p => p.GetPluginInfo())); + return Ok(_pluginManager.Plugins + .OrderBy(p => p.Name) + .Select(p => p.GetPluginInfo())); + } + + /// + /// Enables a disabled plugin. + /// + /// Plugin id. + /// Plugin version. + /// Plugin enabled. + /// Plugin not found. + /// An on success, or a if the file could not be found. + [HttpPost("{pluginId}/Enable")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult EnablePlugin([FromRoute, Required] Guid pluginId, [FromRoute] Version? version) + { + if (!_pluginManager.TryGetPlugin(pluginId, version, out var plugin)) + { + return NotFound(); + } + + _pluginManager.EnablePlugin(plugin!); + return NoContent(); + } + + /// + /// Disable a plugin. + /// + /// Plugin id. + /// Plugin version. + /// Plugin disabled. + /// Plugin not found. + /// An on success, or a if the file could not be found. + [HttpPost("{pluginId}/Disable")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult DisablePlugin([FromRoute, Required] Guid pluginId, [FromRoute] Version? version) + { + if (!_pluginManager.TryGetPlugin(pluginId, version, out var plugin)) + { + return NotFound(); + } + + _pluginManager.DisablePlugin(plugin!); + return NoContent(); } /// /// Uninstalls a plugin. /// /// Plugin id. + /// Plugin version. /// Plugin uninstalled. /// Plugin not found. /// An on success, or a if the file could not be found. @@ -64,15 +180,14 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult UninstallPlugin([FromRoute, Required] Guid pluginId) + public ActionResult UninstallPlugin([FromRoute, Required] Guid pluginId, Version version) { - var plugin = _appHost.Plugins.FirstOrDefault(p => p.Id == pluginId); - if (plugin == null) + if (!_pluginManager.TryGetPlugin(pluginId, version, out var plugin)) { return NotFound(); } - _installationManager.UninstallPlugin(plugin); + _installationManager.UninstallPlugin(plugin!); return NoContent(); } @@ -80,20 +195,23 @@ namespace Jellyfin.Api.Controllers /// Gets plugin configuration. /// /// Plugin id. + /// Plugin version. /// Plugin configuration returned. /// Plugin not found or plugin configuration not found. /// Plugin configuration. [HttpGet("{pluginId}/Configuration")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GetPluginConfiguration([FromRoute, Required] Guid pluginId) + [ProducesFile(MediaTypeNames.Application.Json)] + public ActionResult GetPluginConfiguration([FromRoute, Required] Guid pluginId, [FromRoute] Version? version) { - if (!(_appHost.Plugins.FirstOrDefault(p => p.Id == pluginId) is IHasPluginConfiguration plugin)) + if (_pluginManager.TryGetPlugin(pluginId, version, out var plugin) + && plugin!.Instance is IHasPluginConfiguration configPlugin) { - return NotFound(); + return configPlugin.Configuration; } - return plugin.Configuration; + return NotFound(); } /// @@ -103,6 +221,7 @@ namespace Jellyfin.Api.Controllers /// Accepts plugin configuration as JSON body. /// /// Plugin id. + /// Plugin version. /// Plugin configuration updated. /// Plugin not found or plugin does not have configuration. /// @@ -113,92 +232,125 @@ namespace Jellyfin.Api.Controllers [HttpPost("{pluginId}/Configuration")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task UpdatePluginConfiguration([FromRoute, Required] Guid pluginId) + public async Task UpdatePluginConfiguration([FromRoute, Required] Guid pluginId, [FromRoute] Version? version) { - if (!(_appHost.Plugins.FirstOrDefault(p => p.Id == pluginId) is IHasPluginConfiguration plugin)) + if (!_pluginManager.TryGetPlugin(pluginId, version, out var plugin) + || plugin?.Instance is not IHasPluginConfiguration configPlugin) { return NotFound(); } - var configuration = (BasePluginConfiguration?)await JsonSerializer.DeserializeAsync(Request.Body, plugin.ConfigurationType, _serializerOptions) + var configuration = (BasePluginConfiguration?)await JsonSerializer.DeserializeAsync(Request.Body, configPlugin.ConfigurationType, _serializerOptions) .ConfigureAwait(false); if (configuration != null) { - plugin.UpdateConfiguration(configuration); + configPlugin.UpdateConfiguration(configuration); } return NoContent(); } /// - /// Get plugin security info. + /// Gets a plugin's image. /// - /// Plugin security info returned. - /// Plugin security info. - [Obsolete("This endpoint should not be used.")] - [HttpGet("SecurityInfo")] + /// Plugin id. + /// Plugin version. + /// Plugin image returned. + /// Plugin's image. + [HttpGet("{pluginId}/Image")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetPluginSecurityInfo() + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesImageFile] + [AllowAnonymous] + public ActionResult GetPluginImage([FromRoute, Required] Guid pluginId, [FromRoute] Version? version) { - return new PluginSecurityInfo + if (!_pluginManager.TryGetPlugin(pluginId, version, out var plugin)) { - IsMbSupporter = true, - SupporterKey = "IAmTotallyLegit" - }; + return NotFound(); + } + + var imgPath = Path.Combine(plugin!.Path, plugin!.Manifest.ImageUrl ?? string.Empty); + if (((ServerConfiguration)_config.CommonConfiguration).DisablePluginImages + || plugin!.Manifest.ImageUrl == null + || !System.IO.File.Exists(imgPath)) + { + // Use a blank image. + var type = GetType(); + var stream = type.Assembly.GetManifestResourceStream(type.Namespace + ".Plugins.blank.png"); + return File(stream, "image/png"); + } + + imgPath = Path.Combine(plugin.Path, plugin.Manifest.ImageUrl); + return PhysicalFile(imgPath, MimeTypes.GetMimeType(imgPath)); } /// - /// Updates plugin security info. + /// Gets a plugin's status image. /// - /// Plugin security info. - /// Plugin security info updated. - /// An . - [Obsolete("This endpoint should not be used.")] - [HttpPost("SecurityInfo")] - [Authorize(Policy = Policies.RequiresElevation)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult UpdatePluginSecurityInfo([FromBody, Required] PluginSecurityInfo pluginSecurityInfo) + /// Plugin id. + /// Plugin version. + /// Plugin image returned. + /// Plugin's image. + [HttpGet("{pluginId}/StatusImage")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesImageFile] + [AllowAnonymous] + public ActionResult GetPluginStatusImage([FromRoute, Required] Guid pluginId, [FromRoute] Version? version) { - return NoContent(); + if (!_pluginManager.TryGetPlugin(pluginId, version, out var plugin)) + { + return NotFound(); + } + + // Icons from http://www.fatcow.com/free-icons + var status = plugin!.Manifest.Status; + + var type = _pluginManager.GetType(); + var stream = type.Assembly.GetManifestResourceStream($"{type.Namespace}.Plugins.{status}.png"); + return File(stream, "image/png"); } /// - /// Gets registration status for a feature. + /// Gets a plugin's manifest. /// - /// Feature name. - /// Registration status returned. - /// Mb registration record. - [Obsolete("This endpoint should not be used.")] - [HttpPost("RegistrationRecords/{name}")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetRegistrationStatus([FromRoute, Required] string name) + /// Plugin id. + /// Plugin version. + /// Plugin manifest returned. + /// Plugin not found. + /// + /// A that represents the asynchronous operation to get the plugin's manifest. + /// The task result contains an indicating success, or + /// when plugin not found. + /// + [HttpPost("{pluginId}/Manifest")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesFile(MediaTypeNames.Application.Json)] + public ActionResult GetPluginManifest([FromRoute, Required] Guid pluginId, [FromRoute] Version? version) { - return new MBRegistrationRecord + if (_pluginManager.TryGetPlugin(pluginId, version, out var plugin)) { - IsRegistered = true, - RegChecked = true, - TrialVersion = false, - IsValid = true, - RegError = false - }; + return Ok(plugin!.Manifest); + } + + return NotFound(); } /// - /// Gets registration status for a feature. + /// Updates plugin security info. /// - /// Feature name. - /// Not implemented. - /// Not Implemented. - /// This endpoint is not implemented. - [Obsolete("Paid plugins are not supported")] - [HttpGet("Registrations/{name}")] - [ProducesResponseType(StatusCodes.Status501NotImplemented)] - public ActionResult GetRegistration([FromRoute, Required] string name) + /// Plugin security info. + /// Plugin security info updated. + /// An . + [Obsolete("This endpoint should not be used.")] + [HttpPost("SecurityInfo")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult UpdatePluginSecurityInfo([FromBody, Required] PluginSecurityInfo pluginSecurityInfo) { - // TODO Once we have proper apps and plugins and decide to break compatibility with paid plugins, - // delete all these registration endpoints. They are only kept for compatibility. - throw new NotImplementedException(); + return NoContent(); } } } diff --git a/Jellyfin.Api/Models/ConfigurationPageInfo.cs b/Jellyfin.Api/Models/ConfigurationPageInfo.cs index 2aa6373aa9..155e116a56 100644 --- a/Jellyfin.Api/Models/ConfigurationPageInfo.cs +++ b/Jellyfin.Api/Models/ConfigurationPageInfo.cs @@ -1,4 +1,4 @@ -using MediaBrowser.Common.Plugins; +using MediaBrowser.Common.Plugins; using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.Plugins; @@ -32,16 +32,16 @@ namespace Jellyfin.Api.Models /// /// Instance of interface. /// Instance of interface. - public ConfigurationPageInfo(IPlugin plugin, PluginPageInfo page) + public ConfigurationPageInfo(IPlugin? plugin, PluginPageInfo page) { Name = page.Name; EnableInMainMenu = page.EnableInMainMenu; MenuSection = page.MenuSection; MenuIcon = page.MenuIcon; - DisplayName = string.IsNullOrWhiteSpace(page.DisplayName) ? plugin.Name : page.DisplayName; + DisplayName = string.IsNullOrWhiteSpace(page.DisplayName) ? plugin?.Name ?? page.DisplayName : page.DisplayName; // Don't use "N" because it needs to match Plugin.Id - PluginId = plugin.Id.ToString(); + PluginId = plugin?.Id.ToString(); } /// diff --git a/MediaBrowser.Common/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs index 849037ac46..ddcf2ac171 100644 --- a/MediaBrowser.Common/IApplicationHost.cs +++ b/MediaBrowser.Common/IApplicationHost.cs @@ -2,11 +2,16 @@ using System; using System.Collections.Generic; using System.Reflection; using System.Threading.Tasks; -using MediaBrowser.Common.Plugins; -using Microsoft.Extensions.DependencyInjection; namespace MediaBrowser.Common { + /// + /// Delegate used with GetExports{T}. + /// + /// Type to create. + /// New instance of type type. + public delegate object CreationDelegate(Type type); + /// /// An interface to be implemented by the applications hosting a kernel. /// @@ -53,6 +58,11 @@ namespace MediaBrowser.Common /// The application version. Version ApplicationVersion { get; } + /// + /// Gets or sets the service provider. + /// + IServiceProvider ServiceProvider { get; set; } + /// /// Gets the application version. /// @@ -71,12 +81,6 @@ namespace MediaBrowser.Common /// string ApplicationUserAgentAddress { get; } - /// - /// Gets the plugins. - /// - /// The plugins. - IReadOnlyList Plugins { get; } - /// /// Gets all plugin assemblies which implement a custom rest api. /// @@ -101,6 +105,22 @@ namespace MediaBrowser.Common /// . IReadOnlyCollection GetExports(bool manageLifetime = true); + /// + /// Gets the exports. + /// + /// The type. + /// Delegate function that gets called to create the object. + /// If set to true [manage lifetime]. + /// . + IReadOnlyCollection GetExports(CreationDelegate defaultFunc, bool manageLifetime = true); + + /// + /// Gets the export types. + /// + /// The type. + /// IEnumerable{Type}. + IEnumerable GetExportTypes(); + /// /// Resolves this instance. /// @@ -114,12 +134,6 @@ namespace MediaBrowser.Common /// A task. Task Shutdown(); - /// - /// Removes the plugin. - /// - /// The plugin. - void RemovePlugin(IPlugin plugin); - /// /// Initializes this instance. /// diff --git a/MediaBrowser.Common/Plugins/BasePlugin.cs b/MediaBrowser.Common/Plugins/BasePlugin.cs index 084e91d500..b918fc4f6d 100644 --- a/MediaBrowser.Common/Plugins/BasePlugin.cs +++ b/MediaBrowser.Common/Plugins/BasePlugin.cs @@ -7,7 +7,6 @@ using System.Runtime.InteropServices; using MediaBrowser.Common.Configuration; using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Serialization; -using Microsoft.Extensions.DependencyInjection; namespace MediaBrowser.Common.Plugins { @@ -64,14 +63,12 @@ namespace MediaBrowser.Common.Plugins /// PluginInfo. public virtual PluginInfo GetPluginInfo() { - var info = new PluginInfo - { - Name = Name, - Version = Version.ToString(), - Description = Description, - Id = Id.ToString(), - CanUninstall = CanUninstall - }; + var info = new PluginInfo( + Name, + Version, + Description, + Id, + CanUninstall); return info; } diff --git a/MediaBrowser.Common/Plugins/IHasPluginConfiguration.cs b/MediaBrowser.Common/Plugins/IHasPluginConfiguration.cs new file mode 100644 index 0000000000..42ad85dd37 --- /dev/null +++ b/MediaBrowser.Common/Plugins/IHasPluginConfiguration.cs @@ -0,0 +1,33 @@ +using System; +using MediaBrowser.Model.Plugins; + +namespace MediaBrowser.Common.Plugins +{ + /// + /// Defines the . + /// + public interface IHasPluginConfiguration + { + /// + /// Gets the type of configuration this plugin uses. + /// + Type ConfigurationType { get; } + + /// + /// Gets the plugin's configuration. + /// + BasePluginConfiguration Configuration { get; } + + /// + /// Completely overwrites the current configuration with a new copy. + /// + /// The configuration. + void UpdateConfiguration(BasePluginConfiguration configuration); + + /// + /// Sets the startup directory creation function. + /// + /// The directory function used to create the configuration folder. + void SetStartupInfo(Action directoryCreateFn); + } +} diff --git a/MediaBrowser.Common/Plugins/IPlugin.cs b/MediaBrowser.Common/Plugins/IPlugin.cs index d583a58878..b2ba1179c1 100644 --- a/MediaBrowser.Common/Plugins/IPlugin.cs +++ b/MediaBrowser.Common/Plugins/IPlugin.cs @@ -1,44 +1,36 @@ -#pragma warning disable CS1591 - using System; using MediaBrowser.Model.Plugins; -using Microsoft.Extensions.DependencyInjection; namespace MediaBrowser.Common.Plugins { /// - /// Interface IPlugin. + /// Defines the . /// public interface IPlugin { /// /// Gets the name of the plugin. /// - /// The name. string Name { get; } /// - /// Gets the description. + /// Gets the Description. /// - /// The description. string Description { get; } /// /// Gets the unique id. /// - /// The unique id. Guid Id { get; } /// /// Gets the plugin version. /// - /// The version. Version Version { get; } /// /// Gets the path to the assembly file. /// - /// The assembly file path. string AssemblyFilePath { get; } /// @@ -49,11 +41,10 @@ namespace MediaBrowser.Common.Plugins /// /// Gets the full path to the data folder, where the plugin can store any miscellaneous files needed. /// - /// The data folder path. string DataFolderPath { get; } /// - /// Gets the plugin info. + /// Gets the . /// /// PluginInfo. PluginInfo GetPluginInfo(); @@ -63,29 +54,4 @@ namespace MediaBrowser.Common.Plugins /// void OnUninstalling(); } - - public interface IHasPluginConfiguration - { - /// - /// Gets the type of configuration this plugin uses. - /// - /// The type of the configuration. - Type ConfigurationType { get; } - - /// - /// Gets the plugin's configuration. - /// - /// The configuration. - BasePluginConfiguration Configuration { get; } - - /// - /// Completely overwrites the current configuration with a new copy - /// Returns true or false indicating success or failure. - /// - /// The configuration. - /// configuration is null. - void UpdateConfiguration(BasePluginConfiguration configuration); - - void SetStartupInfo(Action directoryCreateFn); - } } diff --git a/MediaBrowser.Common/Plugins/IPluginManager.cs b/MediaBrowser.Common/Plugins/IPluginManager.cs new file mode 100644 index 0000000000..071b51969f --- /dev/null +++ b/MediaBrowser.Common/Plugins/IPluginManager.cs @@ -0,0 +1,86 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; + +namespace MediaBrowser.Common.Plugins +{ + /// + /// Defines the . + /// + public interface IPluginManager + { + /// + /// Gets the Plugins. + /// + IList Plugins { get; } + + /// + /// Creates the plugins. + /// + void CreatePlugins(); + + /// + /// Returns all the assemblies. + /// + /// An IEnumerable{Assembly}. + IEnumerable LoadAssemblies(); + + /// + /// Registers the plugin's services with the DI. + /// Note: DI is not yet instantiated yet. + /// + /// A instance. + void RegisterServices(IServiceCollection serviceCollection); + + /// + /// Saves the manifest back to disk. + /// + /// The to save. + /// The path where to save the manifest. + /// True if successful. + bool SaveManifest(PluginManifest manifest, string path); + + /// + /// Imports plugin details from a folder. + /// + /// Folder of the plugin. + void ImportPluginFrom(string folder); + + /// + /// Disable the plugin. + /// + /// The of the plug to disable. + void FailPlugin(Assembly assembly); + + /// + /// Disable the plugin. + /// + /// The of the plug to disable. + void DisablePlugin(LocalPlugin plugin); + + /// + /// Enables the plugin, disabling all other versions. + /// + /// The of the plug to disable. + void EnablePlugin(LocalPlugin plugin); + + /// + /// Attempts to find the plugin with and id of . + /// + /// Id of plugin. + /// The version of the plugin to locate. + /// A if found, otherwise null. + /// Boolean value signifying the success of the search. + bool TryGetPlugin(Guid id, Version? version, out LocalPlugin? plugin); + + /// + /// Removes the plugin. + /// + /// The plugin. + /// Outcome of the operation. + bool RemovePlugin(LocalPlugin plugin); + } +} diff --git a/MediaBrowser.Common/Plugins/LocalPlugin.cs b/MediaBrowser.Common/Plugins/LocalPlugin.cs index c97e75a3b2..ef9ab7a7dc 100644 --- a/MediaBrowser.Common/Plugins/LocalPlugin.cs +++ b/MediaBrowser.Common/Plugins/LocalPlugin.cs @@ -1,6 +1,9 @@ +#nullable enable using System; using System.Collections.Generic; using System.Globalization; +using System.Reflection; +using MediaBrowser.Model.Plugins; namespace MediaBrowser.Common.Plugins { @@ -9,36 +12,48 @@ namespace MediaBrowser.Common.Plugins /// public class LocalPlugin : IEquatable { + private readonly bool _supported; + private Version? _version; + /// /// Initializes a new instance of the class. /// - /// The plugin id. - /// The plugin name. - /// The plugin version. /// The plugin path. - public LocalPlugin(Guid id, string name, Version version, string path) + /// True if Jellyfin supports this version of the plugin. + /// The manifest record for this plugin, or null if one does not exist. + public LocalPlugin(string path, bool isSupported, PluginManifest manifest) { - Id = id; - Name = name; - Version = version; Path = path; DllFiles = new List(); + _supported = isSupported; + Manifest = manifest; } /// /// Gets the plugin id. /// - public Guid Id { get; } + public Guid Id => Manifest.Guid; /// /// Gets the plugin name. /// - public string Name { get; } + public string Name => Manifest.Name; /// /// Gets the plugin version. /// - public Version Version { get; } + public Version Version + { + get + { + if (_version == null) + { + _version = Version.Parse(Manifest.Version); + } + + return _version; + } + } /// /// Gets the plugin path. @@ -51,26 +66,24 @@ namespace MediaBrowser.Common.Plugins public List DllFiles { get; } /// - /// == operator. + /// Gets or sets the instance of this plugin. /// - /// Left item. - /// Right item. - /// Comparison result. - public static bool operator ==(LocalPlugin left, LocalPlugin right) - { - return left.Equals(right); - } + public IPlugin? Instance { get; set; } /// - /// != operator. + /// Gets a value indicating whether Jellyfin supports this version of the plugin, and it's enabled. /// - /// Left item. - /// Right item. - /// Comparison result. - public static bool operator !=(LocalPlugin left, LocalPlugin right) - { - return !left.Equals(right); - } + public bool IsEnabledAndSupported => _supported && Manifest.Status >= PluginStatus.Active; + + /// + /// Gets a value indicating whether the plugin has a manifest. + /// + public PluginManifest Manifest { get; } + + /// + /// Gets or sets a value indicating the assembly of the plugin. + /// + public Assembly? Assembly { get; set; } /// /// Compare two . @@ -80,10 +93,15 @@ namespace MediaBrowser.Common.Plugins /// Comparison result. public static int Compare(LocalPlugin a, LocalPlugin b) { + if (a == null || b == null) + { + throw new ArgumentNullException(a == null ? nameof(a) : nameof(b)); + } + var compare = string.Compare(a.Name, b.Name, true, CultureInfo.InvariantCulture); // Id is not equal but name is. - if (a.Id != b.Id && compare == 0) + if (!a.Id.Equals(b.Id) && compare == 0) { compare = a.Id.CompareTo(b.Id); } @@ -91,8 +109,20 @@ namespace MediaBrowser.Common.Plugins return compare == 0 ? a.Version.CompareTo(b.Version) : compare; } + /// + /// Returns the plugin information. + /// + /// A instance containing the information. + public PluginInfo GetPluginInfo() + { + var inst = Instance?.GetPluginInfo() ?? new PluginInfo(Manifest.Name, Version, Manifest.Description, Manifest.Guid, true); + inst.Status = Manifest.Status; + inst.HasImage = !string.IsNullOrEmpty(Manifest.ImageUrl); + return inst; + } + /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is LocalPlugin other && this.Equals(other); } @@ -104,16 +134,14 @@ namespace MediaBrowser.Common.Plugins } /// - public bool Equals(LocalPlugin other) + public bool Equals(LocalPlugin? other) { - // Do not use == or != for comparison as this class overrides the operators. - if (object.ReferenceEquals(other, null)) + if (other == null) { return false; } - return Name.Equals(other.Name, StringComparison.OrdinalIgnoreCase) - && Id.Equals(other.Id); + return Name.Equals(other.Name, StringComparison.OrdinalIgnoreCase) && Id.Equals(other.Id) && Version.Equals(other.Version); } } } diff --git a/MediaBrowser.Common/Plugins/PluginManifest.cs b/MediaBrowser.Common/Plugins/PluginManifest.cs new file mode 100644 index 0000000000..b88275718a --- /dev/null +++ b/MediaBrowser.Common/Plugins/PluginManifest.cs @@ -0,0 +1,85 @@ +#nullable enable +using System; +using MediaBrowser.Model.Plugins; + +namespace MediaBrowser.Common.Plugins +{ + /// + /// Defines a Plugin manifest file. + /// + public class PluginManifest + { + /// + /// Gets or sets the category of the plugin. + /// + public string Category { get; set; } = string.Empty; + + /// + /// Gets or sets the changelog information. + /// + public string Changelog { get; set; } = string.Empty; + + /// + /// Gets or sets the description of the plugin. + /// + public string Description { get; set; } = string.Empty; + + /// + /// Gets or sets the Global Unique Identifier for the plugin. + /// +#pragma warning disable CA1720 // Identifier contains type name + public Guid Guid { get; set; } +#pragma warning restore CA1720 // Identifier contains type name + + /// + /// Gets or sets the Name of the plugin. + /// + public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets an overview of the plugin. + /// + public string Overview { get; set; } = string.Empty; + + /// + /// Gets or sets the owner of the plugin. + /// + public string Owner { get; set; } = string.Empty; + + /// + /// Gets or sets the compatibility version for the plugin. + /// + public string TargetAbi { get; set; } = string.Empty; + + /// + /// Gets or sets the upper compatibility version for the plugin. + /// + public string MaxAbi { get; set; } = string.Empty; + + /// + /// Gets or sets the timestamp of the plugin. + /// + public DateTime Timestamp { get; set; } + + /// + /// Gets or sets the Version number of the plugin. + /// + public string Version { get; set; } = string.Empty; + + /// + /// Gets or sets a value indicating whether this plugin should be ignored. + /// + public PluginStatus Status { get; set; } + + /// + /// Gets or sets a value indicating whether this plugin should automatically update. + /// + public bool AutoUpdate { get; set; } = true; + + /// + /// Gets or sets a value indicating whether this plugin has an image. + /// Image must be located in the local plugin folder. + /// + public string? ImageUrl { get; set; } + } +} diff --git a/MediaBrowser.Common/Updates/IInstallationManager.cs b/MediaBrowser.Common/Updates/IInstallationManager.cs index 585b1ee19e..dd9e0cc3f9 100644 --- a/MediaBrowser.Common/Updates/IInstallationManager.cs +++ b/MediaBrowser.Common/Updates/IInstallationManager.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS1591 +#nullable enable using System; using System.Collections.Generic; @@ -9,6 +9,9 @@ using MediaBrowser.Model.Updates; namespace MediaBrowser.Common.Updates { + /// + /// Defines the . + /// public interface IInstallationManager : IDisposable { /// @@ -21,12 +24,13 @@ namespace MediaBrowser.Common.Updates /// /// Name of the repository. /// The URL to query. + /// Filter out incompatible plugins. /// The cancellation token. /// Task{IReadOnlyList{PackageInfo}}. - Task> GetPackages(string manifestName, string manifest, CancellationToken cancellationToken = default); + Task> GetPackages(string manifestName, string manifest, bool filterIncompatible, CancellationToken cancellationToken = default); /// - /// Gets all available packages. + /// Gets all available packages that are supported by this version. /// /// The cancellation token. /// Task{IReadOnlyList{PackageInfo}}. @@ -42,9 +46,11 @@ namespace MediaBrowser.Common.Updates /// All plugins matching the requirements. IEnumerable FilterPackages( IEnumerable availablePackages, - string name = null, - Guid guid = default, - Version specificVersion = null); + string? name = null, +#pragma warning disable CA1720 // Identifier contains type name + Guid? guid = default, +#pragma warning restore CA1720 // Identifier contains type name + Version? specificVersion = null); /// /// Returns all compatible versions ordered from newest to oldest. @@ -57,13 +63,15 @@ namespace MediaBrowser.Common.Updates /// All compatible versions ordered from newest to oldest. IEnumerable GetCompatibleVersions( IEnumerable availablePackages, - string name = null, - Guid guid = default, - Version minVersion = null, - Version specificVersion = null); + string? name = null, +#pragma warning disable CA1720 // Identifier contains type name + Guid? guid = default, +#pragma warning restore CA1720 // Identifier contains type name + Version? minVersion = null, + Version? specificVersion = null); /// - /// Returns the available plugin updates. + /// Returns the available compatible plugin updates. /// /// The cancellation token. /// The available plugin updates. @@ -81,7 +89,7 @@ namespace MediaBrowser.Common.Updates /// Uninstalls a plugin. /// /// The plugin. - void UninstallPlugin(IPlugin plugin); + void UninstallPlugin(LocalPlugin plugin); /// /// Cancels the installation. diff --git a/MediaBrowser.Common/Updates/InstallationEventArgs.cs b/MediaBrowser.Common/Updates/InstallationEventArgs.cs index 61178f631c..adf336313f 100644 --- a/MediaBrowser.Common/Updates/InstallationEventArgs.cs +++ b/MediaBrowser.Common/Updates/InstallationEventArgs.cs @@ -1,14 +1,21 @@ -#pragma warning disable CS1591 - using System; using MediaBrowser.Model.Updates; namespace MediaBrowser.Common.Updates { + /// + /// Defines the . + /// public class InstallationEventArgs : EventArgs { + /// + /// Gets or sets the . + /// public InstallationInfo InstallationInfo { get; set; } + /// + /// Gets or sets the . + /// public VersionInfo VersionInfo { get; set; } } } diff --git a/MediaBrowser.Controller/Events/Updates/PluginUninstalledEventArgs.cs b/MediaBrowser.Controller/Events/Updates/PluginUninstalledEventArgs.cs index 7510b62b88..a111e6d829 100644 --- a/MediaBrowser.Controller/Events/Updates/PluginUninstalledEventArgs.cs +++ b/MediaBrowser.Controller/Events/Updates/PluginUninstalledEventArgs.cs @@ -1,18 +1,19 @@ -using Jellyfin.Data.Events; +using Jellyfin.Data.Events; using MediaBrowser.Common.Plugins; +using MediaBrowser.Model.Plugins; namespace MediaBrowser.Controller.Events.Updates { /// /// An event that occurs when a plugin is uninstalled. /// - public class PluginUninstalledEventArgs : GenericEventArgs + public class PluginUninstalledEventArgs : GenericEventArgs { /// /// Initializes a new instance of the class. /// /// The plugin. - public PluginUninstalledEventArgs(IPlugin arg) : base(arg) + public PluginUninstalledEventArgs(PluginInfo arg) : base(arg) { } } diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index 2456da826f..92b2d43ce2 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -19,8 +19,6 @@ namespace MediaBrowser.Controller { event EventHandler HasUpdateAvailableChanged; - IServiceProvider ServiceProvider { get; } - bool CoreStartupHasCompleted { get; } bool CanLaunchWebBrowser { get; } @@ -122,13 +120,5 @@ namespace MediaBrowser.Controller string ExpandVirtualPath(string path); string ReverseVirtualPath(string path); - - /// - /// Gets the list of local plugins. - /// - /// Plugin base directory. - /// Cleanup old plugins. - /// Enumerable of local plugins. - IEnumerable GetLocalPlugins(string path, bool cleanup = true); } } diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 0dbd51bdc1..de3d3b6ff1 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -449,5 +449,15 @@ namespace MediaBrowser.Model.Configuration /// Gets or sets the how many metadata refreshes can run concurrently. /// public int LibraryMetadataRefreshConcurrency { get; set; } + + /// + /// Gets or sets a value indicating whether older plugins should automatically be deleted from the plugin folder. + /// + public bool RemoveOldPlugins { get; set; } + + /// + /// Gets or sets a value indicating whether plugin image should be disabled. + /// + public bool DisablePluginImages { get; set; } } } diff --git a/MediaBrowser.Model/Plugins/PluginInfo.cs b/MediaBrowser.Model/Plugins/PluginInfo.cs index dd215192f9..52c99b9c3e 100644 --- a/MediaBrowser.Model/Plugins/PluginInfo.cs +++ b/MediaBrowser.Model/Plugins/PluginInfo.cs @@ -1,4 +1,7 @@ -#nullable disable +#nullable enable + +using System; + namespace MediaBrowser.Model.Plugins { /// @@ -6,34 +9,46 @@ namespace MediaBrowser.Model.Plugins /// public class PluginInfo { + /// + /// Initializes a new instance of the class. + /// + /// The plugin name. + /// The plugin . + /// The plugin description. + /// The . + /// True if this plugin can be uninstalled. + public PluginInfo(string name, Version version, string description, Guid id, bool canUninstall) + { + Name = name; + Version = version?.ToString() ?? throw new ArgumentNullException(nameof(version)); + Description = description; + Id = id.ToString(); + CanUninstall = canUninstall; + } + /// /// Gets or sets the name. /// - /// The name. public string Name { get; set; } /// /// Gets or sets the version. /// - /// The version. public string Version { get; set; } /// /// Gets or sets the name of the configuration file. /// - /// The name of the configuration file. - public string ConfigurationFileName { get; set; } + public string? ConfigurationFileName { get; set; } /// /// Gets or sets the description. /// - /// The description. public string Description { get; set; } /// /// Gets or sets the unique id. /// - /// The unique id. public string Id { get; set; } /// @@ -42,9 +57,13 @@ namespace MediaBrowser.Model.Plugins public bool CanUninstall { get; set; } /// - /// Gets or sets the image URL. + /// Gets or sets a value indicating whether this plugin has a valid image. + /// + public bool HasImage { get; set; } + + /// + /// Gets or sets a value indicating the status of the plugin. /// - /// The image URL. - public string ImageUrl { get; set; } + public PluginStatus Status { get; set; } } } diff --git a/MediaBrowser.Model/Plugins/PluginStatus.cs b/MediaBrowser.Model/Plugins/PluginStatus.cs new file mode 100644 index 0000000000..439968ba8a --- /dev/null +++ b/MediaBrowser.Model/Plugins/PluginStatus.cs @@ -0,0 +1,17 @@ +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +#pragma warning disable SA1602 // Enumeration items should be documented +namespace MediaBrowser.Model.Plugins +{ + /// + /// Plugin load status. + /// + public enum PluginStatus + { + RestartRequired = 1, + Active = 0, + Disabled = -1, + NotSupported = -2, + Malfunction = -3, + Superceded = -4 + } +} diff --git a/MediaBrowser.Model/Updates/PackageInfo.cs b/MediaBrowser.Model/Updates/PackageInfo.cs index 5e93043639..77e2d8d88a 100644 --- a/MediaBrowser.Model/Updates/PackageInfo.cs +++ b/MediaBrowser.Model/Updates/PackageInfo.cs @@ -1,4 +1,4 @@ -#nullable disable +#nullable enable using System; using System.Collections.Generic; @@ -9,55 +9,70 @@ namespace MediaBrowser.Model.Updates /// public class PackageInfo { + /// + /// Initializes a new instance of the class. + /// + public PackageInfo() + { + Versions = Array.Empty(); + Guid = string.Empty; + Category = string.Empty; + Name = string.Empty; + Overview = string.Empty; + Owner = string.Empty; + Description = string.Empty; + } + /// /// Gets or sets the name. /// /// The name. - public string name { get; set; } + public string Name { get; set; } /// /// Gets or sets a long description of the plugin containing features or helpful explanations. /// /// The description. - public string description { get; set; } + public string Description { get; set; } /// /// Gets or sets a short overview of what the plugin does. /// /// The overview. - public string overview { get; set; } + public string Overview { get; set; } /// /// Gets or sets the owner. /// /// The owner. - public string owner { get; set; } + public string Owner { get; set; } /// /// Gets or sets the category. /// /// The category. - public string category { get; set; } + public string Category { get; set; } /// - /// The guid of the assembly associated with this plugin. + /// Gets or sets the guid of the assembly associated with this plugin. /// This is used to identify the proper item for automatic updates. /// /// The name. - public string guid { get; set; } +#pragma warning disable CA1720 // Identifier contains type name + public string Guid { get; set; } +#pragma warning restore CA1720 // Identifier contains type name /// /// Gets or sets the versions. /// /// The versions. - public IList versions { get; set; } +#pragma warning disable CA2227 // Collection properties should be read only + public IList Versions { get; set; } +#pragma warning restore CA2227 // Collection properties should be read only /// - /// Initializes a new instance of the class. + /// Gets or sets the image url for the package. /// - public PackageInfo() - { - versions = Array.Empty(); - } + public string? ImageUrl { get; set; } } } diff --git a/MediaBrowser.Model/Updates/VersionInfo.cs b/MediaBrowser.Model/Updates/VersionInfo.cs index 844170999a..1e07c9f26b 100644 --- a/MediaBrowser.Model/Updates/VersionInfo.cs +++ b/MediaBrowser.Model/Updates/VersionInfo.cs @@ -1,6 +1,6 @@ -#nullable disable +#nullable enable -using System; +using SysVersion = System.Version; namespace MediaBrowser.Model.Updates { @@ -9,68 +9,68 @@ namespace MediaBrowser.Model.Updates /// public class VersionInfo { - private Version _version; + private SysVersion? _version; /// /// Gets or sets the version. /// /// The version. - public string version + public string Version { - get - { - return _version == null ? string.Empty : _version.ToString(); - } + get => _version == null ? string.Empty : _version.ToString(); - set - { - _version = Version.Parse(value); - } + set => _version = SysVersion.Parse(value); } /// - /// Gets the version as a . + /// Gets the version as a . /// - public Version VersionNumber => _version; + public SysVersion VersionNumber => _version ?? new SysVersion(0, 0, 0); /// /// Gets or sets the changelog for this version. /// /// The changelog. - public string changelog { get; set; } + public string? Changelog { get; set; } /// /// Gets or sets the ABI that this version was built against. /// /// The target ABI version. - public string targetAbi { get; set; } + public string? TargetAbi { get; set; } + + /// + /// Gets or sets the maximum ABI that this version will work with. + /// + /// The target ABI version. + public string? MaxAbi { get; set; } /// /// Gets or sets the source URL. /// /// The source URL. - public string sourceUrl { get; set; } + public string? SourceUrl { get; set; } /// /// Gets or sets a checksum for the binary. /// /// The checksum. - public string checksum { get; set; } + public string? Checksum { get; set; } /// /// Gets or sets a timestamp of when the binary was built. /// /// The timestamp. - public string timestamp { get; set; } + public string? Timestamp { get; set; } /// /// Gets or sets the repository name. /// - public string repositoryName { get; set; } + public string RepositoryName { get; set; } = string.Empty; /// /// Gets or sets the repository url. /// - public string repositoryUrl { get; set; } + public string RepositoryUrl { get; set; } = string.Empty; } } diff --git a/MediaBrowser.sln b/MediaBrowser.sln index 5a807372d7..36518377c4 100644 --- a/MediaBrowser.sln +++ b/MediaBrowser.sln @@ -1,4 +1,5 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 + +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.30503.244 MinimumVisualStudioVersion = 10.0.40219.1 @@ -70,7 +71,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Networking", "Jell EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Networking.Tests", "tests\Jellyfin.Networking.Tests\NetworkTesting\Jellyfin.Networking.Tests.csproj", "{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Dlna.Tests", "tests\Jellyfin.Dlna.Tests\Jellyfin.Dlna.Tests.csproj", "{B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Dlna.Tests", "tests\Jellyfin.Dlna.Tests\Jellyfin.Dlna.Tests.csproj", "{B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution -- cgit v1.2.3 From 48d8536d2f920e768ea71c8005cd934a87805f05 Mon Sep 17 00:00:00 2001 From: MrTimscampi Date: Mon, 28 Dec 2020 09:19:08 +0100 Subject: Enable TMDB and OMDB by default --- MediaBrowser.Model/Configuration/ServerConfiguration.cs | 5 ----- 1 file changed, 5 deletions(-) (limited to 'MediaBrowser.Model/Configuration') diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 0dbd51bdc1..7013cb300e 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -49,8 +49,6 @@ namespace MediaBrowser.Model.Configuration new MetadataOptions { ItemType = "Series", - DisabledMetadataFetchers = new[] { "TheMovieDb" }, - DisabledImageFetchers = new[] { "TheMovieDb" } }, new MetadataOptions { @@ -69,13 +67,10 @@ namespace MediaBrowser.Model.Configuration new MetadataOptions { ItemType = "Season", - DisabledMetadataFetchers = new[] { "TheMovieDb" }, }, new MetadataOptions { ItemType = "Episode", - DisabledMetadataFetchers = new[] { "The Open Movie Database", "TheMovieDb" }, - DisabledImageFetchers = new[] { "The Open Movie Database", "TheMovieDb" } } }; } -- cgit v1.2.3 From cd979e6b6291d426f3e180e36b5a8ba68c208cbd Mon Sep 17 00:00:00 2001 From: artiume Date: Wed, 30 Dec 2020 08:52:11 -0500 Subject: Add default of 5 minutes --- MediaBrowser.Model/Configuration/ServerConfiguration.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) (limited to 'MediaBrowser.Model/Configuration') diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 7013cb300e..9fb978e9be 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -298,6 +298,18 @@ namespace MediaBrowser.Model.Configuration /// The min resume duration seconds. public int MinResumeDurationSeconds { get; set; } = 300; + /// + /// Gets or sets the minimum minutes of a book that must be played in order for playstate to be updated. + /// + /// The min resume in minutes. + public int MinAudiobookResume { get; set; } = 5; + + /// + /// Gets or sets the remaining minutes of a book that can be played while still saving playstate. If this percentage is crossed playstate will be reset to the beginning and the item will be marked watched. + /// + /// The remaining time in minutes. + public int MaxAudiobookResume { get; set; } = 5; + /// /// Gets or sets the delay in seconds that we will wait after a file system change to try and discover what has been added/removed /// Some delay is necessary with some items because their creation is not atomic. It involves the creation of several -- cgit v1.2.3 From 326fa8ce384d4e0c433007e02f3f68b8d5538e55 Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Mon, 25 Jan 2021 03:40:34 +0800 Subject: add an enhanced nvdec decoder --- .../MediaEncoding/EncodingHelper.cs | 514 +++++++++++---------- .../Configuration/EncodingOptions.cs | 3 + 2 files changed, 262 insertions(+), 255 deletions(-) (limited to 'MediaBrowser.Model/Configuration') diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index efab87a38e..3b70ed9ddc 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -112,6 +112,11 @@ namespace MediaBrowser.Controller.MediaEncoding return _mediaEncoder.SupportsHwaccel("vaapi"); } + private bool IsCudaSupported(EncodingJobInfo state) + { + return _mediaEncoder.SupportsHwaccel("cuda"); + } + private bool IsTonemappingSupported(EncodingJobInfo state, EncodingOptions options) { var videoStream = state.VideoStream; @@ -458,7 +463,8 @@ namespace MediaBrowser.Controller.MediaEncoding var isVaapiEncoder = outputVideoCodec.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1; var isQsvDecoder = videoDecoder.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1; var isQsvEncoder = outputVideoCodec.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1; - var isNvdecHevcDecoder = videoDecoder.IndexOf("hevc_cuvid", StringComparison.OrdinalIgnoreCase) != -1; + var isNvdecDecoder = videoDecoder.IndexOf("cuda", StringComparison.OrdinalIgnoreCase) != -1; + var isCuvidHevcDecoder = videoDecoder.IndexOf("hevc_cuvid", StringComparison.OrdinalIgnoreCase) != -1; var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); var isMacOS = RuntimeInformation.IsOSPlatform(OSPlatform.OSX); @@ -534,8 +540,17 @@ namespace MediaBrowser.Controller.MediaEncoding } if (state.IsVideoRequest - && (string.Equals(encodingOptions.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase) && isNvdecHevcDecoder || isSwDecoder) - || (string.Equals(encodingOptions.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase) && isD3d11vaDecoder || isSwDecoder)) + && string.Equals(encodingOptions.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase)) + { + if (isNvdecDecoder) + { + arg.Append("-hwaccel_output_format cuda "); + } + } + + if (state.IsVideoRequest + && (string.Equals(encodingOptions.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase) && (isNvdecDecoder || isCuvidHevcDecoder || isSwDecoder)) + || (string.Equals(encodingOptions.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase) && (isD3d11vaDecoder || isSwDecoder))) { if (isTonemappingSupported) { @@ -922,18 +937,23 @@ namespace MediaBrowser.Controller.MediaEncoding { var videoStream = state.VideoStream; var isColorDepth10 = IsColorDepth10(state); + var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, encodingOptions) ?? string.Empty; + var isNvdecDecoder = videoDecoder.IndexOf("cuda", StringComparison.OrdinalIgnoreCase) != -1; - if (isColorDepth10 - && _mediaEncoder.SupportsHwaccel("opencl") - && encodingOptions.EnableTonemapping - && !string.IsNullOrEmpty(videoStream.VideoRange) - && videoStream.VideoRange.Contains("HDR", StringComparison.OrdinalIgnoreCase)) + if (!isNvdecDecoder) { - param += " -pix_fmt nv12"; - } - else - { - param += " -pix_fmt yuv420p"; + if (isColorDepth10 + && _mediaEncoder.SupportsHwaccel("opencl") + && encodingOptions.EnableTonemapping + && !string.IsNullOrEmpty(videoStream.VideoRange) + && videoStream.VideoRange.Contains("HDR", StringComparison.OrdinalIgnoreCase)) + { + param += " -pix_fmt nv12"; + } + else + { + param += " -pix_fmt yuv420p"; + } } } @@ -1912,6 +1932,8 @@ namespace MediaBrowser.Controller.MediaEncoding var isVaapiDecoder = videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1; var isVaapiH264Encoder = outputVideoCodec.IndexOf("h264_vaapi", StringComparison.OrdinalIgnoreCase) != -1; var isVaapiHevcEncoder = outputVideoCodec.IndexOf("hevc_vaapi", StringComparison.OrdinalIgnoreCase) != -1; + var isNvdecDecoder = videoDecoder.IndexOf("cuda", StringComparison.OrdinalIgnoreCase) != -1; + var isNvencEncoder = outputVideoCodec.IndexOf("nvenc", StringComparison.OrdinalIgnoreCase) != -1; var isTonemappingSupported = IsTonemappingSupported(state, options); var isTonemappingSupportedOnVaapi = string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isVaapiH264Encoder || isVaapiHevcEncoder); @@ -1940,11 +1962,14 @@ namespace MediaBrowser.Controller.MediaEncoding height.Value); } - // For QSV, feed it into hardware encoder now - if (isLinux && (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase) - || string.Equals(outputVideoCodec, "hevc_qsv", StringComparison.OrdinalIgnoreCase))) + if (!string.IsNullOrEmpty(videoSizeParam)) { - videoSizeParam += ",hwupload=extra_hw_frames=64"; + // For QSV, feed it into hardware encoder now + if (isLinux && (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase) + || string.Equals(outputVideoCodec, "hevc_qsv", StringComparison.OrdinalIgnoreCase))) + { + videoSizeParam += ",hwupload=extra_hw_frames=64"; + } } } @@ -2002,6 +2027,12 @@ namespace MediaBrowser.Controller.MediaEncoding : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay_qsv\""; } } + else if (isNvdecDecoder && isNvencEncoder) + { + retStr = !outputSizeParam.IsEmpty + ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay,format=yuv420p|nv12,hwupload_cuda\"" + : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay,format=yuv420p|nv12,hwupload_cuda\""; + } return string.Format( CultureInfo.InvariantCulture, @@ -2133,6 +2164,26 @@ namespace MediaBrowser.Controller.MediaEncoding (qsv_or_vaapi && isDeintEnabled) ? ":deinterlace=1" : string.Empty)); } } + else if ((videoDecoder ?? string.Empty).IndexOf("cuda", StringComparison.OrdinalIgnoreCase) != -1 + && width.HasValue + && height.HasValue) + { + var outputWidth = width.Value; + var outputHeight = height.Value; + + if (!videoWidth.HasValue + || outputWidth != videoWidth.Value + || !videoHeight.HasValue + || outputHeight != videoHeight.Value) + { + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "scale_cuda=w={0}:h={1}", + outputWidth, + outputHeight)); + } + } else if ((videoDecoder ?? string.Empty).IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1 && width.HasValue && height.HasValue) @@ -2367,17 +2418,20 @@ namespace MediaBrowser.Controller.MediaEncoding var isVaapiHevcEncoder = outputVideoCodec.IndexOf("hevc_vaapi", StringComparison.OrdinalIgnoreCase) != -1; var isQsvH264Encoder = outputVideoCodec.IndexOf("h264_qsv", StringComparison.OrdinalIgnoreCase) != -1; var isQsvHevcEncoder = outputVideoCodec.IndexOf("hevc_qsv", StringComparison.OrdinalIgnoreCase) != -1; - var isNvdecH264Decoder = videoDecoder.IndexOf("h264_cuvid", StringComparison.OrdinalIgnoreCase) != -1; - var isNvdecHevcDecoder = videoDecoder.IndexOf("hevc_cuvid", StringComparison.OrdinalIgnoreCase) != -1; + var isNvdecDecoder = videoDecoder.IndexOf("cuda", StringComparison.OrdinalIgnoreCase) != -1; + var isNvencEncoder = outputVideoCodec.IndexOf("nvenc", StringComparison.OrdinalIgnoreCase) != -1; + var isCuvidH264Decoder = videoDecoder.IndexOf("h264_cuvid", StringComparison.OrdinalIgnoreCase) != -1; + var isCuvidHevcDecoder = videoDecoder.IndexOf("hevc_cuvid", StringComparison.OrdinalIgnoreCase) != -1; var isLibX264Encoder = outputVideoCodec.IndexOf("libx264", StringComparison.OrdinalIgnoreCase) != -1; var isLibX265Encoder = outputVideoCodec.IndexOf("libx265", StringComparison.OrdinalIgnoreCase) != -1; var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); var isColorDepth10 = IsColorDepth10(state); var isTonemappingSupported = IsTonemappingSupported(state, options); - var isTonemappingSupportedOnNvenc = string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase) && isNvdecHevcDecoder || isSwDecoder; - var isTonemappingSupportedOnAmf = string.Equals(options.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase) && isD3d11vaDecoder || isSwDecoder; + var isTonemappingSupportedOnNvenc = string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase) && (isNvdecDecoder || isCuvidHevcDecoder || isSwDecoder); + var isTonemappingSupportedOnAmf = string.Equals(options.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase) && (isD3d11vaDecoder || isSwDecoder); var isTonemappingSupportedOnVaapi = string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isVaapiH264Encoder || isVaapiHevcEncoder); + var hasSubs = state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; var hasTextSubs = state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; @@ -2385,6 +2439,8 @@ namespace MediaBrowser.Controller.MediaEncoding var doubleRateDeinterlace = options.DeinterlaceDoubleRate && (videoStream?.RealFrameRate ?? 60) <= 30; var isScalingInAdvance = false; + var isCudaDeintInAdvance = false; + var isHwuploadCudaRequired = false; var isDeinterlaceH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true); var isDeinterlaceHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true); @@ -2428,15 +2484,17 @@ namespace MediaBrowser.Controller.MediaEncoding filters.Add("format=p010"); } - if (isNvdecHevcDecoder || isSwDecoder || isD3d11vaDecoder) + if ((isDeinterlaceH264 || isDeinterlaceHevc) && isNvdecDecoder) { - // Upload the HDR10 or HLG data to the OpenCL device, - // use tonemap_opencl filter for tone mapping, - // and then download the SDR data to memory. - filters.Add("hwupload"); + isCudaDeintInAdvance = true; + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "yadif={0}:-1:0", + doubleRateDeinterlace ? "1" : "0")); } - if (isVaapiDecoder) + if (isVaapiDecoder || isNvdecDecoder) { isScalingInAdvance = true; filters.AddRange( @@ -2452,11 +2510,28 @@ namespace MediaBrowser.Controller.MediaEncoding request.Height, request.MaxWidth, request.MaxHeight)); + } - // hwmap the HDR data to opencl device by cl-va p010 interop. + // hwmap the HDR data to opencl device by cl-va p010 interop. + if (isVaapiDecoder) + { filters.Add("hwmap"); } + // convert cuda device data to p010 host data. + if (isNvdecDecoder) + { + filters.Add("hwdownload,format=p010"); + } + + if (isNvdecDecoder || isCuvidHevcDecoder || isSwDecoder || isD3d11vaDecoder) + { + // Upload the HDR10 or HLG data to the OpenCL device, + // use tonemap_opencl filter for tone mapping, + // and then download the SDR data to memory. + filters.Add("hwupload"); + } + filters.Add( string.Format( CultureInfo.InvariantCulture, @@ -2468,21 +2543,15 @@ namespace MediaBrowser.Controller.MediaEncoding options.TonemappingParam, options.TonemappingRange)); - if (isNvdecHevcDecoder || isSwDecoder || isD3d11vaDecoder) + if (isNvdecDecoder || isCuvidHevcDecoder || isSwDecoder || isD3d11vaDecoder) { filters.Add("hwdownload"); + filters.Add("format=nv12"); } - if (isSwDecoder || isD3d11vaDecoder) + if (isNvdecDecoder && isNvencEncoder) { - if (isLibX264Encoder - || isLibX265Encoder - || hasGraphicalSubs - || (isNvdecHevcDecoder && isDeinterlaceHevc) - || (!isNvdecHevcDecoder && isDeinterlaceH264 || isDeinterlaceHevc)) - { - filters.Add("format=nv12"); - } + isHwuploadCudaRequired = true; } if (isVaapiDecoder) @@ -2507,7 +2576,7 @@ namespace MediaBrowser.Controller.MediaEncoding } // If we're hardware VAAPI decoding and software encoding, download frames from the decoder first. - else if (IsVaapiSupported(state) && isVaapiDecoder && (isLibX264Encoder || isLibX265Encoder)) + else if ((IsVaapiSupported(state) && isVaapiDecoder) && (isLibX264Encoder || isLibX265Encoder)) { var codec = videoStream.Codec.ToLowerInvariant(); @@ -2534,9 +2603,9 @@ namespace MediaBrowser.Controller.MediaEncoding } // Add hardware deinterlace filter before scaling filter. - if (isDeinterlaceH264) + if (isDeinterlaceH264 || isDeinterlaceHevc) { - if (isVaapiH264Encoder) + if (isVaapiEncoder) { filters.Add( string.Format( @@ -2544,6 +2613,14 @@ namespace MediaBrowser.Controller.MediaEncoding "deinterlace_vaapi=rate={0}", doubleRateDeinterlace ? "field" : "frame")); } + else if (isNvdecDecoder && !isCudaDeintInAdvance) + { + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "yadif_cuda={0}:-1:0", + doubleRateDeinterlace ? "1" : "0")); + } } // Add software deinterlace filter before scaling filter. @@ -2552,7 +2629,8 @@ namespace MediaBrowser.Controller.MediaEncoding && !isVaapiHevcEncoder && !isQsvH264Encoder && !isQsvHevcEncoder - && !isNvdecH264Decoder) + && !isNvdecDecoder + && !isCuvidH264Decoder) { if (string.Equals(options.DeinterlaceMethod, "bwdif", StringComparison.OrdinalIgnoreCase)) { @@ -2590,6 +2668,41 @@ namespace MediaBrowser.Controller.MediaEncoding request.MaxHeight)); } + // Another case is using Nvenc decoder. + if (isNvdecDecoder && !isTonemappingSupported) + { + var codec = videoStream.Codec.ToLowerInvariant(); + + // Assert 10-bit hardware decodable + if (isColorDepth10 && (string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase))) + { + // Download data from GPU to CPU as p010le format. + filters.Add("hwdownload"); + filters.Add("format=p010"); + + // cuda lacks of a pixel format converter. + if (isNvencEncoder) + { + isHwuploadCudaRequired = true; + filters.Add("format=yuv420p"); + } + } + + // Assert 8-bit hardware decodable + else if (!isColorDepth10 && (isLibX264Encoder || isLibX265Encoder || hasSubs)) + { + if (isNvencEncoder) + { + isHwuploadCudaRequired = true; + } + + filters.Add("hwdownload"); + filters.Add("format=nv12"); + } + } + // Add parameters to use VAAPI with burn-in text subtitles (GH issue #642) if (isVaapiH264Encoder || isVaapiHevcEncoder) { @@ -2618,10 +2731,20 @@ namespace MediaBrowser.Controller.MediaEncoding // Ensure proper filters are passed to ffmpeg in case of hardware acceleration via VA-API // Reference: https://trac.ffmpeg.org/wiki/Hardware/VAAPI - if (isVaapiH264Encoder) + if (isVaapiH264Encoder || isVaapiHevcEncoder) { filters.Add("hwmap"); } + + if (isNvdecDecoder && isNvencEncoder) + { + isHwuploadCudaRequired = true; + } + } + + if (isHwuploadCudaRequired && !hasGraphicalSubs) + { + filters.Add("hwupload_cuda"); } if (filters.Count > 0) @@ -3102,57 +3225,18 @@ namespace MediaBrowser.Controller.MediaEncoding { case "avc": case "h264": - if (_mediaEncoder.SupportsDecoder("h264_qsv") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase)) - { - // qsv decoder does not support 10-bit input - if ((videoStream.BitDepth ?? 8) > 8) - { - encodingOptions.HardwareDecodingCodecs = Array.Empty(); - return null; - } - - return "-c:v h264_qsv"; - } - - break; + return GetHwDecoderName(encodingOptions, "h264_qsv", "h264", isColorDepth10); case "hevc": case "h265": - if (_mediaEncoder.SupportsDecoder("hevc_qsv") && encodingOptions.HardwareDecodingCodecs.Contains("hevc", StringComparer.OrdinalIgnoreCase)) - { - return (isColorDepth10 && - !encodingOptions.EnableDecodingColorDepth10Hevc) ? null : "-c:v hevc_qsv"; - } - - break; + return GetHwDecoderName(encodingOptions, "hevc_qsv", "hevc", isColorDepth10); case "mpeg2video": - if (_mediaEncoder.SupportsDecoder("mpeg2_qsv") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase)) - { - return "-c:v mpeg2_qsv"; - } - - break; + return GetHwDecoderName(encodingOptions, "mpeg2_qsv", "mpeg2video", isColorDepth10); case "vc1": - if (_mediaEncoder.SupportsDecoder("vc1_qsv") && encodingOptions.HardwareDecodingCodecs.Contains("vc1", StringComparer.OrdinalIgnoreCase)) - { - return "-c:v vc1_qsv"; - } - - break; + return GetHwDecoderName(encodingOptions, "vc1_qsv", "vc1", isColorDepth10); case "vp8": - if (_mediaEncoder.SupportsDecoder("vp8_qsv") && encodingOptions.HardwareDecodingCodecs.Contains("vp8", StringComparer.OrdinalIgnoreCase)) - { - return "-c:v vp8_qsv"; - } - - break; + return GetHwDecoderName(encodingOptions, "vp8_qsv", "vp8", isColorDepth10); case "vp9": - if (_mediaEncoder.SupportsDecoder("vp9_qsv") && encodingOptions.HardwareDecodingCodecs.Contains("vp9", StringComparer.OrdinalIgnoreCase)) - { - return (isColorDepth10 && - !encodingOptions.EnableDecodingColorDepth10Vp9) ? null : "-c:v vp9_qsv"; - } - - break; + return GetHwDecoderName(encodingOptions, "vp9_qsv", "vp9", isColorDepth10); } } else if (string.Equals(encodingOptions.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase)) @@ -3161,57 +3245,34 @@ namespace MediaBrowser.Controller.MediaEncoding { case "avc": case "h264": - if (_mediaEncoder.SupportsDecoder("h264_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase)) - { - return "-c:v h264_cuvid"; - } - - break; + return encodingOptions.EnableEnhancedNvdecDecoder + ? GetHwaccelType(state, encodingOptions, "h264", isColorDepth10) + : GetHwDecoderName(encodingOptions, "h264_cuvid", "h264", isColorDepth10); case "hevc": case "h265": - if (_mediaEncoder.SupportsDecoder("hevc_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("hevc", StringComparer.OrdinalIgnoreCase)) - { - return (isColorDepth10 && - !encodingOptions.EnableDecodingColorDepth10Hevc) ? null : "-c:v hevc_cuvid"; - } - - break; + return encodingOptions.EnableEnhancedNvdecDecoder + ? GetHwaccelType(state, encodingOptions, "hevc", isColorDepth10) + : GetHwDecoderName(encodingOptions, "hevc_cuvid", "hevc", isColorDepth10); case "mpeg2video": - if (_mediaEncoder.SupportsDecoder("mpeg2_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase)) - { - return "-c:v mpeg2_cuvid"; - } - - break; + return encodingOptions.EnableEnhancedNvdecDecoder + ? GetHwaccelType(state, encodingOptions, "mpeg2video", isColorDepth10) + : GetHwDecoderName(encodingOptions, "mpeg2_cuvid", "mpeg2video", isColorDepth10); case "vc1": - if (_mediaEncoder.SupportsDecoder("vc1_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("vc1", StringComparer.OrdinalIgnoreCase)) - { - return "-c:v vc1_cuvid"; - } - - break; + return encodingOptions.EnableEnhancedNvdecDecoder + ? GetHwaccelType(state, encodingOptions, "vc1", isColorDepth10) + : GetHwDecoderName(encodingOptions, "vc1_cuvid", "vc1", isColorDepth10); case "mpeg4": - if (_mediaEncoder.SupportsDecoder("mpeg4_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg4", StringComparer.OrdinalIgnoreCase)) - { - return "-c:v mpeg4_cuvid"; - } - - break; + return encodingOptions.EnableEnhancedNvdecDecoder + ? GetHwaccelType(state, encodingOptions, "mpeg4", isColorDepth10) + : GetHwDecoderName(encodingOptions, "mpeg4_cuvid", "mpeg4", isColorDepth10); case "vp8": - if (_mediaEncoder.SupportsDecoder("vp8_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("vp8", StringComparer.OrdinalIgnoreCase)) - { - return "-c:v vp8_cuvid"; - } - - break; + return encodingOptions.EnableEnhancedNvdecDecoder + ? GetHwaccelType(state, encodingOptions, "vp8", isColorDepth10) + : GetHwDecoderName(encodingOptions, "vp8_cuvid", "vp8", isColorDepth10); case "vp9": - if (_mediaEncoder.SupportsDecoder("vp9_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("vp9", StringComparer.OrdinalIgnoreCase)) - { - return (isColorDepth10 && - !encodingOptions.EnableDecodingColorDepth10Vp9) ? null : "-c:v vp9_cuvid"; - } - - break; + return encodingOptions.EnableEnhancedNvdecDecoder + ? GetHwaccelType(state, encodingOptions, "vp9", isColorDepth10) + : GetHwDecoderName(encodingOptions, "vp9_cuvid", "vp9", isColorDepth10); } } else if (string.Equals(encodingOptions.HardwareAccelerationType, "mediacodec", StringComparison.OrdinalIgnoreCase)) @@ -3220,50 +3281,18 @@ namespace MediaBrowser.Controller.MediaEncoding { case "avc": case "h264": - if (_mediaEncoder.SupportsDecoder("h264_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase)) - { - return "-c:v h264_mediacodec"; - } - - break; + return GetHwDecoderName(encodingOptions, "h264_mediacodec", "h264", isColorDepth10); case "hevc": case "h265": - if (_mediaEncoder.SupportsDecoder("hevc_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("hevc", StringComparer.OrdinalIgnoreCase)) - { - return (isColorDepth10 && - !encodingOptions.EnableDecodingColorDepth10Hevc) ? null : "-c:v hevc_mediacodec"; - } - - break; + return GetHwDecoderName(encodingOptions, "hevc_mediacodec", "hevc", isColorDepth10); case "mpeg2video": - if (_mediaEncoder.SupportsDecoder("mpeg2_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase)) - { - return "-c:v mpeg2_mediacodec"; - } - - break; + return GetHwDecoderName(encodingOptions, "mpeg2_mediacodec", "mpeg2video", isColorDepth10); case "mpeg4": - if (_mediaEncoder.SupportsDecoder("mpeg4_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg4", StringComparer.OrdinalIgnoreCase)) - { - return "-c:v mpeg4_mediacodec"; - } - - break; + return GetHwDecoderName(encodingOptions, "mpeg4_mediacodec", "mpeg4", isColorDepth10); case "vp8": - if (_mediaEncoder.SupportsDecoder("vp8_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("vp8", StringComparer.OrdinalIgnoreCase)) - { - return "-c:v vp8_mediacodec"; - } - - break; + return GetHwDecoderName(encodingOptions, "vp8_mediacodec", "vp8", isColorDepth10); case "vp9": - if (_mediaEncoder.SupportsDecoder("vp9_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("vp9", StringComparer.OrdinalIgnoreCase)) - { - return (isColorDepth10 && - !encodingOptions.EnableDecodingColorDepth10Vp9) ? null : "-c:v vp9_mediacodec"; - } - - break; + return GetHwDecoderName(encodingOptions, "vp9_mediacodec", "vp9", isColorDepth10); } } else if (string.Equals(encodingOptions.HardwareAccelerationType, "omx", StringComparison.OrdinalIgnoreCase)) @@ -3272,33 +3301,13 @@ namespace MediaBrowser.Controller.MediaEncoding { case "avc": case "h264": - if (_mediaEncoder.SupportsDecoder("h264_mmal") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase)) - { - return "-c:v h264_mmal"; - } - - break; + return GetHwDecoderName(encodingOptions, "h264_mmal", "h264", isColorDepth10); case "mpeg2video": - if (_mediaEncoder.SupportsDecoder("mpeg2_mmal") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase)) - { - return "-c:v mpeg2_mmal"; - } - - break; + return GetHwDecoderName(encodingOptions, "mpeg2_mmal", "mpeg2video", isColorDepth10); case "mpeg4": - if (_mediaEncoder.SupportsDecoder("mpeg4_mmal") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg4", StringComparer.OrdinalIgnoreCase)) - { - return "-c:v mpeg4_mmal"; - } - - break; + return GetHwDecoderName(encodingOptions, "mpeg4_mmal", "mpeg4", isColorDepth10); case "vc1": - if (_mediaEncoder.SupportsDecoder("vc1_mmal") && encodingOptions.HardwareDecodingCodecs.Contains("vc1", StringComparer.OrdinalIgnoreCase)) - { - return "-c:v vc1_mmal"; - } - - break; + return GetHwDecoderName(encodingOptions, "vc1_mmal", "vc1", isColorDepth10); } } else if (string.Equals(encodingOptions.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase)) @@ -3307,20 +3316,18 @@ namespace MediaBrowser.Controller.MediaEncoding { case "avc": case "h264": - return GetHwaccelType(state, encodingOptions, "h264"); + return GetHwaccelType(state, encodingOptions, "h264", isColorDepth10); case "hevc": case "h265": - return (isColorDepth10 && - !encodingOptions.EnableDecodingColorDepth10Hevc) ? null : GetHwaccelType(state, encodingOptions, "hevc"); + return GetHwaccelType(state, encodingOptions, "hevc", isColorDepth10); case "mpeg2video": - return GetHwaccelType(state, encodingOptions, "mpeg2video"); + return GetHwaccelType(state, encodingOptions, "mpeg2video", isColorDepth10); case "vc1": - return GetHwaccelType(state, encodingOptions, "vc1"); + return GetHwaccelType(state, encodingOptions, "vc1", isColorDepth10); case "mpeg4": - return GetHwaccelType(state, encodingOptions, "mpeg4"); + return GetHwaccelType(state, encodingOptions, "mpeg4", isColorDepth10); case "vp9": - return (isColorDepth10 && - !encodingOptions.EnableDecodingColorDepth10Vp9) ? null : GetHwaccelType(state, encodingOptions, "vp9"); + return GetHwaccelType(state, encodingOptions, "vp9", isColorDepth10); } } else if (string.Equals(encodingOptions.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)) @@ -3329,20 +3336,18 @@ namespace MediaBrowser.Controller.MediaEncoding { case "avc": case "h264": - return GetHwaccelType(state, encodingOptions, "h264"); + return GetHwaccelType(state, encodingOptions, "h264", isColorDepth10); case "hevc": case "h265": - return (isColorDepth10 && - !encodingOptions.EnableDecodingColorDepth10Hevc) ? null : GetHwaccelType(state, encodingOptions, "hevc"); + return GetHwaccelType(state, encodingOptions, "hevc", isColorDepth10); case "mpeg2video": - return GetHwaccelType(state, encodingOptions, "mpeg2video"); + return GetHwaccelType(state, encodingOptions, "mpeg2video", isColorDepth10); case "vc1": - return GetHwaccelType(state, encodingOptions, "vc1"); + return GetHwaccelType(state, encodingOptions, "vc1", isColorDepth10); case "vp8": - return GetHwaccelType(state, encodingOptions, "vp8"); + return GetHwaccelType(state, encodingOptions, "vp8", isColorDepth10); case "vp9": - return (isColorDepth10 && - !encodingOptions.EnableDecodingColorDepth10Vp9) ? null : GetHwaccelType(state, encodingOptions, "vp9"); + return GetHwaccelType(state, encodingOptions, "vp9", isColorDepth10); } } else if (string.Equals(encodingOptions.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase)) @@ -3351,57 +3356,20 @@ namespace MediaBrowser.Controller.MediaEncoding { case "avc": case "h264": - if (_mediaEncoder.SupportsDecoder("h264_opencl") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase)) - { - return "-c:v h264_opencl"; - } - - break; + return GetHwDecoderName(encodingOptions, "h264_opencl", "h264", isColorDepth10); case "hevc": case "h265": - if (_mediaEncoder.SupportsDecoder("hevc_opencl") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase)) - { - return (isColorDepth10 && - !encodingOptions.EnableDecodingColorDepth10Hevc) ? null : "-c:v hevc_opencl"; - } - - break; + return GetHwDecoderName(encodingOptions, "hevc_opencl", "hevc", isColorDepth10); case "mpeg2video": - if (_mediaEncoder.SupportsDecoder("mpeg2_opencl") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase)) - { - return "-c:v mpeg2_opencl"; - } - - break; + return GetHwDecoderName(encodingOptions, "mpeg2_opencl", "mpeg2video", isColorDepth10); case "mpeg4": - if (_mediaEncoder.SupportsDecoder("mpeg4_opencl") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg4", StringComparer.OrdinalIgnoreCase)) - { - return "-c:v mpeg4_opencl"; - } - - break; + return GetHwDecoderName(encodingOptions, "mpeg4_opencl", "mpeg4", isColorDepth10); case "vc1": - if (_mediaEncoder.SupportsDecoder("vc1_opencl") && encodingOptions.HardwareDecodingCodecs.Contains("vc1", StringComparer.OrdinalIgnoreCase)) - { - return "-c:v vc1_opencl"; - } - - break; + return GetHwDecoderName(encodingOptions, "vc1_opencl", "vc1", isColorDepth10); case "vp8": - if (_mediaEncoder.SupportsDecoder("vp8_opencl") && encodingOptions.HardwareDecodingCodecs.Contains("vc1", StringComparer.OrdinalIgnoreCase)) - { - return "-c:v vp8_opencl"; - } - - break; + return GetHwDecoderName(encodingOptions, "vp8_opencl", "vp8", isColorDepth10); case "vp9": - if (_mediaEncoder.SupportsDecoder("vp9_opencl") && encodingOptions.HardwareDecodingCodecs.Contains("vc1", StringComparer.OrdinalIgnoreCase)) - { - return (isColorDepth10 && - !encodingOptions.EnableDecodingColorDepth10Vp9) ? null : "-c:v vp9_opencl"; - } - - break; + return GetHwDecoderName(encodingOptions, "vp9_opencl", "vp9", isColorDepth10); } } } @@ -3424,15 +3392,43 @@ namespace MediaBrowser.Controller.MediaEncoding return null; } + /// + /// Gets a hw decoder name + /// + public string GetHwDecoderName(EncodingOptions options, string decoder, string videoCodec, bool isColorDepth10) + { + var isCodecAvailable = _mediaEncoder.SupportsDecoder(decoder) && options.HardwareDecodingCodecs.Contains(videoCodec, StringComparer.OrdinalIgnoreCase); + if (isColorDepth10 && isCodecAvailable) + { + if ((options.HardwareDecodingCodecs.Contains("hevc", StringComparer.OrdinalIgnoreCase) && !options.EnableDecodingColorDepth10Hevc) + || (options.HardwareDecodingCodecs.Contains("vp9", StringComparer.OrdinalIgnoreCase) && !options.EnableDecodingColorDepth10Vp9)) + { + return null; + } + } + + return isCodecAvailable ? ("-c:v " + decoder) : null; + } + /// /// Gets a hwaccel type to use as a hardware decoder(dxva/vaapi) depending on the system /// - public string GetHwaccelType(EncodingJobInfo state, EncodingOptions options, string videoCodec) + public string GetHwaccelType(EncodingJobInfo state, EncodingOptions options, string videoCodec, bool isColorDepth10) { var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); var isWindows8orLater = Environment.OSVersion.Version.Major > 6 || (Environment.OSVersion.Version.Major == 6 && Environment.OSVersion.Version.Minor > 1); var isDxvaSupported = _mediaEncoder.SupportsHwaccel("dxva2") || _mediaEncoder.SupportsHwaccel("d3d11va"); + var isCodecAvailable = options.HardwareDecodingCodecs.Contains(videoCodec, StringComparer.OrdinalIgnoreCase); + + if (isColorDepth10 && isCodecAvailable) + { + if ((options.HardwareDecodingCodecs.Contains("hevc", StringComparer.OrdinalIgnoreCase) && !options.EnableDecodingColorDepth10Hevc) + || (options.HardwareDecodingCodecs.Contains("vp9", StringComparer.OrdinalIgnoreCase) && !options.EnableDecodingColorDepth10Vp9)) + { + return null; + } + } if (string.Equals(options.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase)) { @@ -3462,6 +3458,14 @@ namespace MediaBrowser.Controller.MediaEncoding } } + if (string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase)) + { + if (IsCudaSupported(state) && options.HardwareDecodingCodecs.Contains(videoCodec, StringComparer.OrdinalIgnoreCase)) + { + return "-hwaccel cuda"; + } + } + return null; } diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index 38b3335107..5cd8744eda 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -65,6 +65,8 @@ namespace MediaBrowser.Model.Configuration public bool EnableDecodingColorDepth10Vp9 { get; set; } + public bool EnableEnhancedNvdecDecoder { get; set; } + public bool EnableHardwareEncoding { get; set; } public bool AllowHevcEncoding { get; set; } @@ -100,6 +102,7 @@ namespace MediaBrowser.Model.Configuration DeinterlaceMethod = "yadif"; EnableDecodingColorDepth10Hevc = true; EnableDecodingColorDepth10Vp9 = true; + EnableEnhancedNvdecDecoder = true; EnableHardwareEncoding = true; AllowHevcEncoding = true; EnableSubtitleExtraction = true; -- cgit v1.2.3 From 09b9fa3ce12e8837605c00f7b8f963e649c7ecc7 Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Tue, 26 Jan 2021 03:45:56 +0800 Subject: add vpp tonemapping for vaapi --- .../MediaEncoding/EncodingHelper.cs | 92 ++++++++++++++++------ .../Configuration/EncodingOptions.cs | 3 + 2 files changed, 69 insertions(+), 26 deletions(-) (limited to 'MediaBrowser.Model/Configuration') diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 3b70ed9ddc..1ba02e276e 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -127,6 +127,25 @@ namespace MediaBrowser.Controller.MediaEncoding && videoStream.VideoRange.Contains("HDR", StringComparison.OrdinalIgnoreCase); } + private bool IsVppTonemappingSupported(EncodingJobInfo state, EncodingOptions options) + { + var videoStream = state.VideoStream; + var codec = videoStream.Codec; + if (string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)) + { + // Limited to HEVC for now since the filter doesn't accept master data from VP9. + return IsColorDepth10(state) + && string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase) + && _mediaEncoder.SupportsHwaccel("vaapi") + && options.EnableVppTonemapping + && !string.IsNullOrEmpty(videoStream.ColorTransfer) + && videoStream.ColorTransfer.Equals("smpte2084", StringComparison.OrdinalIgnoreCase); + } + + // Vpp tonemapping may come to QSV in the future. + return false; + } + /// /// Gets the name of the output video codec. /// @@ -469,6 +488,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); var isMacOS = RuntimeInformation.IsOSPlatform(OSPlatform.OSX); var isTonemappingSupported = IsTonemappingSupported(state, encodingOptions); + var isVppTonemappingSupported = IsVppTonemappingSupported(state, encodingOptions); if (!IsCopyCodec(outputVideoCodec)) { @@ -478,7 +498,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (isVaapiDecoder) { - if (isTonemappingSupported) + if (isTonemappingSupported && !isVppTonemappingSupported) { arg.Append("-init_hw_device vaapi=va:") .Append(encodingOptions.VaapiDevice) @@ -938,7 +958,7 @@ namespace MediaBrowser.Controller.MediaEncoding var videoStream = state.VideoStream; var isColorDepth10 = IsColorDepth10(state); var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, encodingOptions) ?? string.Empty; - var isNvdecDecoder = videoDecoder.IndexOf("cuda", StringComparison.OrdinalIgnoreCase) != -1; + var isNvdecDecoder = videoDecoder.Contains("cuda", StringComparison.OrdinalIgnoreCase); if (!isNvdecDecoder) { @@ -1932,14 +1952,15 @@ namespace MediaBrowser.Controller.MediaEncoding var isVaapiDecoder = videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1; var isVaapiH264Encoder = outputVideoCodec.IndexOf("h264_vaapi", StringComparison.OrdinalIgnoreCase) != -1; var isVaapiHevcEncoder = outputVideoCodec.IndexOf("hevc_vaapi", StringComparison.OrdinalIgnoreCase) != -1; - var isNvdecDecoder = videoDecoder.IndexOf("cuda", StringComparison.OrdinalIgnoreCase) != -1; - var isNvencEncoder = outputVideoCodec.IndexOf("nvenc", StringComparison.OrdinalIgnoreCase) != -1; + var isNvdecDecoder = videoDecoder.Contains("cuda", StringComparison.OrdinalIgnoreCase); + var isNvencEncoder = outputVideoCodec.Contains("nvenc", StringComparison.OrdinalIgnoreCase); var isTonemappingSupported = IsTonemappingSupported(state, options); + var isVppTonemappingSupported = IsVppTonemappingSupported(state, options); var isTonemappingSupportedOnVaapi = string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isVaapiH264Encoder || isVaapiHevcEncoder); // Tonemapping and burn-in graphical subtitles requires overlay_vaapi. // But it's still in ffmpeg mailing list. Disable it for now. - if (isTonemappingSupported && isTonemappingSupportedOnVaapi) + if (isTonemappingSupported && isTonemappingSupportedOnVaapi && !isVppTonemappingSupported) { return GetOutputSizeParam(state, options, outputVideoCodec); } @@ -2123,17 +2144,24 @@ namespace MediaBrowser.Controller.MediaEncoding || state.DeInterlace("h265", true) || state.DeInterlace("hevc", true); + var isVaapiDecoder = videoDecoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase); + var isVaapiH264Encoder = videoEncoder.Contains("h264_vaapi", StringComparison.OrdinalIgnoreCase); + var isVaapiHevcEncoder = videoEncoder.Contains("hevc_vaapi", StringComparison.OrdinalIgnoreCase); var isTonemappingSupported = IsTonemappingSupported(state, options); - var isTonemappingSupportedOnVaapi = string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase) && !qsv_or_vaapi; + var isVppTonemappingSupported = IsVppTonemappingSupported(state, options); + var isTonemappingSupportedOnVaapi = string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isVaapiH264Encoder || isVaapiHevcEncoder); - var outputPixFmt = string.Empty; - if (isTonemappingSupported && isTonemappingSupportedOnVaapi) - { - outputPixFmt = "format=p010:out_range=limited"; - } - else + var outputPixFmt = "format=nv12"; + if (isTonemappingSupportedOnVaapi) { - outputPixFmt = "format=nv12"; + if (isVppTonemappingSupported) + { + outputPixFmt = "format=p010"; + } + else if (isTonemappingSupported) + { + outputPixFmt = "format=p010:out_range=limited"; + } } if (!videoWidth.HasValue @@ -2164,7 +2192,7 @@ namespace MediaBrowser.Controller.MediaEncoding (qsv_or_vaapi && isDeintEnabled) ? ":deinterlace=1" : string.Empty)); } } - else if ((videoDecoder ?? string.Empty).IndexOf("cuda", StringComparison.OrdinalIgnoreCase) != -1 + else if ((videoDecoder ?? string.Empty).Contains("cuda", StringComparison.OrdinalIgnoreCase) && width.HasValue && height.HasValue) { @@ -2418,15 +2446,16 @@ namespace MediaBrowser.Controller.MediaEncoding var isVaapiHevcEncoder = outputVideoCodec.IndexOf("hevc_vaapi", StringComparison.OrdinalIgnoreCase) != -1; var isQsvH264Encoder = outputVideoCodec.IndexOf("h264_qsv", StringComparison.OrdinalIgnoreCase) != -1; var isQsvHevcEncoder = outputVideoCodec.IndexOf("hevc_qsv", StringComparison.OrdinalIgnoreCase) != -1; - var isNvdecDecoder = videoDecoder.IndexOf("cuda", StringComparison.OrdinalIgnoreCase) != -1; - var isNvencEncoder = outputVideoCodec.IndexOf("nvenc", StringComparison.OrdinalIgnoreCase) != -1; - var isCuvidH264Decoder = videoDecoder.IndexOf("h264_cuvid", StringComparison.OrdinalIgnoreCase) != -1; - var isCuvidHevcDecoder = videoDecoder.IndexOf("hevc_cuvid", StringComparison.OrdinalIgnoreCase) != -1; + var isNvdecDecoder = videoDecoder.Contains("cuda", StringComparison.OrdinalIgnoreCase); + var isNvencEncoder = outputVideoCodec.Contains("nvenc", StringComparison.OrdinalIgnoreCase); + var isCuvidH264Decoder = videoDecoder.Contains("h264_cuvid", StringComparison.OrdinalIgnoreCase); + var isCuvidHevcDecoder = videoDecoder.Contains("hevc_cuvid", StringComparison.OrdinalIgnoreCase); var isLibX264Encoder = outputVideoCodec.IndexOf("libx264", StringComparison.OrdinalIgnoreCase) != -1; var isLibX265Encoder = outputVideoCodec.IndexOf("libx265", StringComparison.OrdinalIgnoreCase) != -1; var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); var isColorDepth10 = IsColorDepth10(state); var isTonemappingSupported = IsTonemappingSupported(state, options); + var isVppTonemappingSupported = IsVppTonemappingSupported(state, options); var isTonemappingSupportedOnNvenc = string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase) && (isNvdecDecoder || isCuvidHevcDecoder || isSwDecoder); var isTonemappingSupportedOnAmf = string.Equals(options.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase) && (isD3d11vaDecoder || isSwDecoder); var isTonemappingSupportedOnVaapi = string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isVaapiH264Encoder || isVaapiHevcEncoder); @@ -2444,7 +2473,8 @@ namespace MediaBrowser.Controller.MediaEncoding var isDeinterlaceH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true); var isDeinterlaceHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true); - if (isTonemappingSupportedOnNvenc || isTonemappingSupportedOnAmf || isTonemappingSupportedOnVaapi) + // Add OpenCL tonemapping filter for NVENC/AMF/VAAPI. + if (isTonemappingSupportedOnNvenc || isTonemappingSupportedOnAmf || (isTonemappingSupportedOnVaapi && !isVppTonemappingSupported)) { // Currently only with the use of NVENC decoder can we get a decent performance. // Currently only the HEVC/H265 format is supported with NVDEC decoder. @@ -2490,7 +2520,7 @@ namespace MediaBrowser.Controller.MediaEncoding filters.Add( string.Format( CultureInfo.InvariantCulture, - "yadif={0}:-1:0", + "yadif_cuda={0}:-1:0", doubleRateDeinterlace ? "1" : "0")); } @@ -2563,7 +2593,10 @@ namespace MediaBrowser.Controller.MediaEncoding } // When the input may or may not be hardware VAAPI decodable. - if ((isVaapiH264Encoder || isVaapiHevcEncoder) && !isTonemappingSupported && !isTonemappingSupportedOnVaapi) + if ((isVaapiH264Encoder || isVaapiHevcEncoder) + && !isTonemappingSupported + && !isVppTonemappingSupported + && !isTonemappingSupportedOnVaapi) { filters.Add("format=nv12|vaapi"); filters.Add("hwupload"); @@ -2668,21 +2701,28 @@ namespace MediaBrowser.Controller.MediaEncoding request.MaxHeight)); } - // Another case is using Nvenc decoder. + // Add Vpp tonemapping filter for VAAPI. + // Full hardware based video post processing, faster than OpenCL but lacks fine tuning options. + if (isTonemappingSupportedOnVaapi && isVppTonemappingSupported) + { + filters.Add("tonemap_vaapi=format=nv12:transfer=bt709:matrix=bt709:primaries=bt709"); + } + + // Another case is when using Nvenc decoder. if (isNvdecDecoder && !isTonemappingSupported) { - var codec = videoStream.Codec.ToLowerInvariant(); + var codec = videoStream.Codec; // Assert 10-bit hardware decodable if (isColorDepth10 && (string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase) || string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) || string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase))) { - // Download data from GPU to CPU as p010le format. + // Download data from GPU to CPU as p010 format. filters.Add("hwdownload"); filters.Add("format=p010"); - // cuda lacks of a pixel format converter. + // Cuda lacks of a pixel format converter. if (isNvencEncoder) { isHwuploadCudaRequired = true; @@ -2710,7 +2750,7 @@ namespace MediaBrowser.Controller.MediaEncoding { // Convert hw context from ocl to va. // For tonemapping and text subs burn-in. - if (isTonemappingSupported && isTonemappingSupportedOnVaapi) + if (isTonemappingSupported && isTonemappingSupportedOnVaapi && !isVppTonemappingSupported) { filters.Add("scale_vaapi"); } diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index 5cd8744eda..da467e133f 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -39,6 +39,8 @@ namespace MediaBrowser.Model.Configuration public bool EnableTonemapping { get; set; } + public bool EnableVppTonemapping { get; set; } + public string TonemappingAlgorithm { get; set; } public string TonemappingRange { get; set; } @@ -90,6 +92,7 @@ namespace MediaBrowser.Model.Configuration // The left side of the dot is the platform number, and the right side is the device number on the platform. OpenclDevice = "0.0"; EnableTonemapping = false; + EnableVppTonemapping = false; TonemappingAlgorithm = "hable"; TonemappingRange = "auto"; TonemappingDesat = 0; -- cgit v1.2.3 From 90236efdf247da77cac0ed47daabe82b1bd6f7c9 Mon Sep 17 00:00:00 2001 From: "me@justinharrison.ca" Date: Wed, 10 Feb 2021 17:08:55 -0500 Subject: Default to English metadata during the setup wizard. --- MediaBrowser.Model/Configuration/ServerConfiguration.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'MediaBrowser.Model/Configuration') diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 0f0ad0f9aa..439a84024f 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -254,7 +254,7 @@ namespace MediaBrowser.Model.Configuration /// Gets or sets the preferred metadata language. /// /// The preferred metadata language. - public string PreferredMetadataLanguage { get; set; } = string.Empty; + public string PreferredMetadataLanguage { get; set; } = "en"; /// /// Gets or sets the metadata country code. -- cgit v1.2.3 From 9fcdbd4c4b79560cb22f6f27182b4c36606dae79 Mon Sep 17 00:00:00 2001 From: dkanada Date: Fri, 12 Feb 2021 21:58:37 +0900 Subject: remove deprecated settings from server config --- .../Library/Resolvers/Audio/MusicArtistResolver.cs | 5 ----- Jellyfin.Api/Controllers/PluginsController.cs | 4 +--- MediaBrowser.Model/Configuration/ServerConfiguration.cs | 7 ------- 3 files changed, 1 insertion(+), 15 deletions(-) (limited to 'MediaBrowser.Model/Configuration') diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs index e9e688fa67..60f82806fb 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs @@ -79,11 +79,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio return new MusicArtist(); } - if (_config.Configuration.EnableSimpleArtistDetection) - { - return null; - } - // Avoid mis-identifying top folders if (args.Parent.IsRoot) { diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs index b2e8bee91e..a5aa9bfcae 100644 --- a/Jellyfin.Api/Controllers/PluginsController.cs +++ b/Jellyfin.Api/Controllers/PluginsController.cs @@ -298,9 +298,7 @@ namespace Jellyfin.Api.Controllers } var imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath ?? string.Empty); - if (((ServerConfiguration)_config.CommonConfiguration).DisablePluginImages - || plugin.Manifest.ImagePath == null - || !System.IO.File.Exists(imagePath)) + if (plugin.Manifest.ImagePath == null || !System.IO.File.Exists(imagePath)) { return NotFound(); } diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 0f0ad0f9aa..ba55a2ace6 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -418,8 +418,6 @@ namespace MediaBrowser.Model.Configuration public PathSubstitution[] PathSubstitutions { get; set; } = Array.Empty(); - public bool EnableSimpleArtistDetection { get; set; } = false; - public string[] UninstalledPlugins { get; set; } = Array.Empty(); /// @@ -461,10 +459,5 @@ namespace MediaBrowser.Model.Configuration /// Gets or sets a value indicating whether older plugins should automatically be deleted from the plugin folder. /// public bool RemoveOldPlugins { get; set; } - - /// - /// Gets or sets a value indicating whether plugin image should be disabled. - /// - public bool DisablePluginImages { get; set; } } } -- cgit v1.2.3 From 141efafd3dbbef3dc64824a89fd3fdc77da0b25e Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Sat, 20 Feb 2021 23:13:04 +0100 Subject: Enable TreatWarningsAsErrors for MediaBrowser.Model --- Emby.Dlna/ContentDirectory/StubType.cs | 2 +- Emby.Dlna/DlnaManager.cs | 2 +- Emby.Dlna/PlayTo/TransportState.cs | 2 +- .../Data/SqliteItemRepository.cs | 6 +- Jellyfin.Api/Helpers/MediaInfoHelper.cs | 2 +- Jellyfin.Api/Helpers/StreamingHelpers.cs | 2 +- .../Probing/ProbeResultNormalizer.cs | 6 +- MediaBrowser.Model/Channels/ChannelFeatures.cs | 18 +- MediaBrowser.Model/Channels/ChannelQuery.cs | 6 +- .../Configuration/EncodingOptions.cs | 74 +- MediaBrowser.Model/Configuration/ImageOption.cs | 10 +- MediaBrowser.Model/Configuration/LibraryOptions.cs | 403 +------ MediaBrowser.Model/Configuration/MediaPathInfo.cs | 12 + .../Configuration/MetadataConfiguration.cs | 4 +- .../Configuration/MetadataOptions.cs | 20 +- .../Configuration/MetadataPluginSummary.cs | 12 +- MediaBrowser.Model/Configuration/TypeOptions.cs | 365 ++++++ .../Configuration/UserConfiguration.cs | 36 +- .../Configuration/XbmcMetadataOptions.cs | 16 +- MediaBrowser.Model/Dlna/AudioOptions.cs | 9 +- MediaBrowser.Model/Dlna/CodecProfile.cs | 12 +- MediaBrowser.Model/Dlna/ConditionProcessor.cs | 2 +- MediaBrowser.Model/Dlna/ContainerProfile.cs | 10 +- MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs | 36 +- MediaBrowser.Model/Dlna/IDeviceDiscovery.cs | 1 + .../Dlna/MediaFormatProfileResolver.cs | 4 +- MediaBrowser.Model/Dlna/ResolutionConfiguration.cs | 8 +- MediaBrowser.Model/Dlna/ResponseProfile.cs | 10 +- MediaBrowser.Model/Dlna/SearchCriteria.cs | 54 +- MediaBrowser.Model/Dlna/SortCriteria.cs | 4 +- MediaBrowser.Model/Dlna/StreamBuilder.cs | 14 +- MediaBrowser.Model/Dlna/StreamInfo.cs | 1190 ++++++++++---------- MediaBrowser.Model/Dto/BaseItemDto.cs | 10 +- MediaBrowser.Model/Dto/MediaSourceInfo.cs | 67 +- MediaBrowser.Model/Dto/MetadataEditorInfo.cs | 18 +- MediaBrowser.Model/Dto/NameGuidPair.cs | 14 + MediaBrowser.Model/Dto/NameIdPair.cs | 7 - MediaBrowser.Model/Dto/UserDto.cs | 18 +- MediaBrowser.Model/Entities/CollectionType.cs | 32 - MediaBrowser.Model/Entities/MediaStream.cs | 197 ++-- MediaBrowser.Model/Entities/PackageReviewInfo.cs | 40 - MediaBrowser.Model/Entities/SpecialFolder.cs | 36 + MediaBrowser.Model/Entities/VirtualFolderInfo.cs | 16 +- MediaBrowser.Model/Globalization/CultureDto.cs | 12 +- MediaBrowser.Model/IO/IFileSystem.cs | 7 +- MediaBrowser.Model/LiveTv/BaseTimerInfoDto.cs | 16 +- MediaBrowser.Model/LiveTv/ListingsProviderInfo.cs | 58 + MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs | 26 +- MediaBrowser.Model/LiveTv/LiveTvOptions.cs | 97 +- MediaBrowser.Model/LiveTv/LiveTvServiceInfo.cs | 10 +- MediaBrowser.Model/LiveTv/RecordingQuery.cs | 16 +- MediaBrowser.Model/LiveTv/SeriesTimerInfoDto.cs | 16 +- MediaBrowser.Model/LiveTv/TunerHostInfo.cs | 38 + MediaBrowser.Model/MediaBrowser.Model.csproj | 4 +- MediaBrowser.Model/MediaInfo/MediaInfo.cs | 22 +- .../MediaInfo/PlaybackInfoRequest.cs | 22 +- .../MediaInfo/PlaybackInfoResponse.cs | 16 +- MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs | 4 +- MediaBrowser.Model/Net/ISocket.cs | 6 + MediaBrowser.Model/Net/ISocketFactory.cs | 3 + MediaBrowser.Model/Net/MimeTypes.cs | 21 +- MediaBrowser.Model/Net/NetworkShare.cs | 33 - MediaBrowser.Model/Net/SocketReceiveResult.cs | 4 +- MediaBrowser.Model/Net/WebSocketMessage.cs | 2 +- .../Notifications/NotificationOptions.cs | 10 +- .../Notifications/NotificationRequest.cs | 14 +- MediaBrowser.Model/Providers/ExternalIdInfo.cs | 4 +- MediaBrowser.Model/Providers/RemoteImageInfo.cs | 2 +- MediaBrowser.Model/Providers/SubtitleOptions.cs | 16 +- MediaBrowser.Model/Querying/EpisodeQuery.cs | 10 +- MediaBrowser.Model/Querying/LatestItemsQuery.cs | 9 +- .../Querying/MovieRecommendationQuery.cs | 14 +- MediaBrowser.Model/Querying/NextUpQuery.cs | 20 +- MediaBrowser.Model/Querying/QueryFilters.cs | 25 +- MediaBrowser.Model/Querying/QueryFiltersLegacy.cs | 26 + MediaBrowser.Model/Querying/QueryResult.cs | 26 +- .../Querying/UpcomingEpisodesQuery.cs | 16 +- MediaBrowser.Model/Search/SearchQuery.cs | 32 +- MediaBrowser.Model/Session/BrowseRequest.cs | 1 + MediaBrowser.Model/Session/ClientCapabilities.cs | 14 +- MediaBrowser.Model/Session/GeneralCommand.cs | 13 +- MediaBrowser.Model/Session/PlaybackProgressInfo.cs | 14 - MediaBrowser.Model/Session/PlaystateCommand.cs | 6 +- MediaBrowser.Model/Session/QueueItem.cs | 14 + MediaBrowser.Model/Session/RepeatMode.cs | 11 + MediaBrowser.Model/Session/TranscodeReason.cs | 31 + MediaBrowser.Model/Session/TranscodingInfo.cs | 37 +- MediaBrowser.Model/Sync/SyncJob.cs | 10 +- MediaBrowser.Model/System/SystemInfo.cs | 18 +- MediaBrowser.Model/System/WakeOnLanInfo.cs | 2 +- MediaBrowser.Model/Tasks/IScheduledTaskWorker.cs | 5 +- MediaBrowser.Model/Tasks/ITaskManager.cs | 20 +- MediaBrowser.Model/Tasks/ITaskTrigger.cs | 4 + MediaBrowser.Model/Tasks/TaskInfo.cs | 16 +- MediaBrowser.Model/Tasks/TaskTriggerInfo.cs | 12 +- MediaBrowser.Model/Users/UserPolicy.cs | 100 +- jellyfin.ruleset | 2 + tests/Jellyfin.Api.Tests/ModelBinders/TestType.cs | 6 - 98 files changed, 1908 insertions(+), 1892 deletions(-) create mode 100644 MediaBrowser.Model/Configuration/MediaPathInfo.cs create mode 100644 MediaBrowser.Model/Configuration/TypeOptions.cs create mode 100644 MediaBrowser.Model/Dto/NameGuidPair.cs delete mode 100644 MediaBrowser.Model/Entities/PackageReviewInfo.cs create mode 100644 MediaBrowser.Model/Entities/SpecialFolder.cs create mode 100644 MediaBrowser.Model/LiveTv/ListingsProviderInfo.cs create mode 100644 MediaBrowser.Model/LiveTv/TunerHostInfo.cs delete mode 100644 MediaBrowser.Model/Net/NetworkShare.cs create mode 100644 MediaBrowser.Model/Querying/QueryFiltersLegacy.cs create mode 100644 MediaBrowser.Model/Session/QueueItem.cs create mode 100644 MediaBrowser.Model/Session/RepeatMode.cs create mode 100644 MediaBrowser.Model/Session/TranscodeReason.cs (limited to 'MediaBrowser.Model/Configuration') diff --git a/Emby.Dlna/ContentDirectory/StubType.cs b/Emby.Dlna/ContentDirectory/StubType.cs index 982ae5d68e..a5116055d1 100644 --- a/Emby.Dlna/ContentDirectory/StubType.cs +++ b/Emby.Dlna/ContentDirectory/StubType.cs @@ -1,5 +1,5 @@ #pragma warning disable CS1591 -#pragma warning disable SA1602 + namespace Emby.Dlna.ContentDirectory { diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs index 21ba1c7552..9ab3240388 100644 --- a/Emby.Dlna/DlnaManager.cs +++ b/Emby.Dlna/DlnaManager.cs @@ -553,7 +553,7 @@ namespace Emby.Dlna private void DumpProfiles() { - DeviceProfile[] list = new [] + DeviceProfile[] list = new[] { new SamsungSmartTvProfile(), new XboxOneProfile(), diff --git a/Emby.Dlna/PlayTo/TransportState.cs b/Emby.Dlna/PlayTo/TransportState.cs index 7068a5d24d..1ca71283a8 100644 --- a/Emby.Dlna/PlayTo/TransportState.cs +++ b/Emby.Dlna/PlayTo/TransportState.cs @@ -1,5 +1,5 @@ #pragma warning disable CS1591 -#pragma warning disable SA1602 + namespace Emby.Dlna.PlayTo { diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index dad8bec7b0..d78b93bd78 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -6207,9 +6207,9 @@ AND Type = @InternalPersonType)"); if (item.Type == MediaStreamType.Subtitle) { - item.localizedUndefined = _localization.GetLocalizedString("Undefined"); - item.localizedDefault = _localization.GetLocalizedString("Default"); - item.localizedForced = _localization.GetLocalizedString("Forced"); + item.LocalizedUndefined = _localization.GetLocalizedString("Undefined"); + item.LocalizedDefault = _localization.GetLocalizedString("Default"); + item.LocalizedForced = _localization.GetLocalizedString("Forced"); } return item; diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs index 0d8315dee1..ce6740fc99 100644 --- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs +++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs @@ -523,7 +523,7 @@ namespace Jellyfin.Api.Helpers /// Dlna profile type. public void NormalizeMediaSourceContainer(MediaSourceInfo mediaSource, DeviceProfile profile, DlnaProfileType type) { - mediaSource.Container = StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(mediaSource.Container, mediaSource.Path, profile, type); + mediaSource.Container = StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(mediaSource.Container, profile, type); } private void SetDeviceSpecificSubtitleInfo(StreamInfo info, MediaSourceInfo mediaSource, string accessToken) diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index 4957ee8b8d..8df1f5c272 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -183,7 +183,7 @@ namespace Jellyfin.Api.Helpers if (string.IsNullOrEmpty(containerInternal)) { containerInternal = streamingRequest.Static ? - StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(state.InputContainer, state.MediaPath, null, DlnaProfileType.Audio) + StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(state.InputContainer, null, DlnaProfileType.Audio) : GetOutputFileExtension(state); } diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index b5291b5609..b9cb49cf2f 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -681,9 +681,9 @@ namespace MediaBrowser.MediaEncoding.Probing { stream.Type = MediaStreamType.Subtitle; stream.Codec = NormalizeSubtitleCodec(stream.Codec); - stream.localizedUndefined = _localization.GetLocalizedString("Undefined"); - stream.localizedDefault = _localization.GetLocalizedString("Default"); - stream.localizedForced = _localization.GetLocalizedString("Forced"); + stream.LocalizedUndefined = _localization.GetLocalizedString("Undefined"); + stream.LocalizedDefault = _localization.GetLocalizedString("Default"); + stream.LocalizedForced = _localization.GetLocalizedString("Forced"); } else if (string.Equals(streamInfo.CodecType, "video", StringComparison.OrdinalIgnoreCase)) { diff --git a/MediaBrowser.Model/Channels/ChannelFeatures.cs b/MediaBrowser.Model/Channels/ChannelFeatures.cs index a55754eddc..d925b78b6d 100644 --- a/MediaBrowser.Model/Channels/ChannelFeatures.cs +++ b/MediaBrowser.Model/Channels/ChannelFeatures.cs @@ -7,6 +7,13 @@ namespace MediaBrowser.Model.Channels { public class ChannelFeatures { + public ChannelFeatures() + { + MediaTypes = Array.Empty(); + ContentTypes = Array.Empty(); + DefaultSortFields = Array.Empty(); + } + /// /// Gets or sets the name. /// @@ -38,7 +45,7 @@ namespace MediaBrowser.Model.Channels public ChannelMediaContentType[] ContentTypes { get; set; } /// - /// Represents the maximum number of records the channel allows retrieving at a time. + /// Gets or sets the maximum number of records the channel allows retrieving at a time. /// public int? MaxPageSize { get; set; } @@ -55,7 +62,7 @@ namespace MediaBrowser.Model.Channels public ChannelItemSortField[] DefaultSortFields { get; set; } /// - /// Indicates if a sort ascending/descending toggle is supported or not. + /// Gets or sets a value indicating whether a sort ascending/descending toggle is supported. /// public bool SupportsSortOrderToggle { get; set; } @@ -76,12 +83,5 @@ namespace MediaBrowser.Model.Channels /// /// true if [supports content downloading]; otherwise, false. public bool SupportsContentDownloading { get; set; } - - public ChannelFeatures() - { - MediaTypes = Array.Empty(); - ContentTypes = Array.Empty(); - DefaultSortFields = Array.Empty(); - } } } diff --git a/MediaBrowser.Model/Channels/ChannelQuery.cs b/MediaBrowser.Model/Channels/ChannelQuery.cs index fd90e7f062..59966127fb 100644 --- a/MediaBrowser.Model/Channels/ChannelQuery.cs +++ b/MediaBrowser.Model/Channels/ChannelQuery.cs @@ -10,7 +10,7 @@ namespace MediaBrowser.Model.Channels public class ChannelQuery { /// - /// Fields to return within the items, in addition to basic information. + /// Gets or sets the fields to return within the items, in addition to basic information. /// /// The fields. public ItemFields[] Fields { get; set; } @@ -28,13 +28,13 @@ namespace MediaBrowser.Model.Channels public Guid UserId { get; set; } /// - /// Skips over a given number of items within the results. Use for paging. + /// Gets or sets the start index. Use for paging. /// /// The start index. public int? StartIndex { get; set; } /// - /// The maximum number of items to return. + /// Gets or sets the maximum number of items to return. /// /// The limit. public int? Limit { get; set; } diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index da467e133f..a9b2803017 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -5,6 +5,41 @@ namespace MediaBrowser.Model.Configuration { public class EncodingOptions { + public EncodingOptions() + { + EnableFallbackFont = false; + DownMixAudioBoost = 2; + MaxMuxingQueueSize = 2048; + EnableThrottling = false; + ThrottleDelaySeconds = 180; + EncodingThreadCount = -1; + // This is a DRM device that is almost guaranteed to be there on every intel platform, + // plus it's the default one in ffmpeg if you don't specify anything + VaapiDevice = "/dev/dri/renderD128"; + // This is the OpenCL device that is used for tonemapping. + // The left side of the dot is the platform number, and the right side is the device number on the platform. + OpenclDevice = "0.0"; + EnableTonemapping = false; + EnableVppTonemapping = false; + TonemappingAlgorithm = "hable"; + TonemappingRange = "auto"; + TonemappingDesat = 0; + TonemappingThreshold = 0.8; + TonemappingPeak = 100; + TonemappingParam = 0; + H264Crf = 23; + H265Crf = 28; + DeinterlaceDoubleRate = false; + DeinterlaceMethod = "yadif"; + EnableDecodingColorDepth10Hevc = true; + EnableDecodingColorDepth10Vp9 = true; + EnableEnhancedNvdecDecoder = true; + EnableHardwareEncoding = true; + AllowHevcEncoding = true; + EnableSubtitleExtraction = true; + HardwareDecodingCodecs = new string[] { "h264", "vc1" }; + } + public int EncodingThreadCount { get; set; } public string TranscodingTempPath { get; set; } @@ -24,12 +59,12 @@ namespace MediaBrowser.Model.Configuration public string HardwareAccelerationType { get; set; } /// - /// FFmpeg path as set by the user via the UI. + /// Gets or sets the FFmpeg path as set by the user via the UI. /// public string EncoderAppPath { get; set; } /// - /// The current FFmpeg path being used by the system and displayed on the transcode page. + /// Gets or sets the current FFmpeg path being used by the system and displayed on the transcode page. /// public string EncoderAppPathDisplay { get; set; } @@ -76,40 +111,5 @@ namespace MediaBrowser.Model.Configuration public bool EnableSubtitleExtraction { get; set; } public string[] HardwareDecodingCodecs { get; set; } - - public EncodingOptions() - { - EnableFallbackFont = false; - DownMixAudioBoost = 2; - MaxMuxingQueueSize = 2048; - EnableThrottling = false; - ThrottleDelaySeconds = 180; - EncodingThreadCount = -1; - // This is a DRM device that is almost guaranteed to be there on every intel platform, - // plus it's the default one in ffmpeg if you don't specify anything - VaapiDevice = "/dev/dri/renderD128"; - // This is the OpenCL device that is used for tonemapping. - // The left side of the dot is the platform number, and the right side is the device number on the platform. - OpenclDevice = "0.0"; - EnableTonemapping = false; - EnableVppTonemapping = false; - TonemappingAlgorithm = "hable"; - TonemappingRange = "auto"; - TonemappingDesat = 0; - TonemappingThreshold = 0.8; - TonemappingPeak = 100; - TonemappingParam = 0; - H264Crf = 23; - H265Crf = 28; - DeinterlaceDoubleRate = false; - DeinterlaceMethod = "yadif"; - EnableDecodingColorDepth10Hevc = true; - EnableDecodingColorDepth10Vp9 = true; - EnableEnhancedNvdecDecoder = true; - EnableHardwareEncoding = true; - AllowHevcEncoding = true; - EnableSubtitleExtraction = true; - HardwareDecodingCodecs = new string[] { "h264", "vc1" }; - } } } diff --git a/MediaBrowser.Model/Configuration/ImageOption.cs b/MediaBrowser.Model/Configuration/ImageOption.cs index 2b1268c743..0af7b7e146 100644 --- a/MediaBrowser.Model/Configuration/ImageOption.cs +++ b/MediaBrowser.Model/Configuration/ImageOption.cs @@ -6,6 +6,11 @@ namespace MediaBrowser.Model.Configuration { public class ImageOption { + public ImageOption() + { + Limit = 1; + } + /// /// Gets or sets the type. /// @@ -23,10 +28,5 @@ namespace MediaBrowser.Model.Configuration /// /// The minimum width. public int MinWidth { get; set; } - - public ImageOption() - { - Limit = 1; - } } } diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs index 77ac11d69f..24698360ec 100644 --- a/MediaBrowser.Model/Configuration/LibraryOptions.cs +++ b/MediaBrowser.Model/Configuration/LibraryOptions.cs @@ -2,13 +2,30 @@ #pragma warning disable CS1591 using System; -using System.Collections.Generic; -using MediaBrowser.Model.Entities; namespace MediaBrowser.Model.Configuration { public class LibraryOptions { + public LibraryOptions() + { + TypeOptions = Array.Empty(); + DisabledSubtitleFetchers = Array.Empty(); + SubtitleFetcherOrder = Array.Empty(); + DisabledLocalMetadataReaders = Array.Empty(); + + SkipSubtitlesIfAudioTrackMatches = true; + RequirePerfectSubtitleMatch = true; + + EnablePhotos = true; + SaveSubtitlesWithMedia = true; + EnableRealtimeMonitor = true; + PathInfos = Array.Empty(); + EnableInternetProviders = true; + EnableAutomaticSeriesGrouping = true; + SeasonZeroDisplayName = "Specials"; + } + public bool EnablePhotos { get; set; } public bool EnableRealtimeMonitor { get; set; } @@ -79,387 +96,5 @@ namespace MediaBrowser.Model.Configuration return null; } - - public LibraryOptions() - { - TypeOptions = Array.Empty(); - DisabledSubtitleFetchers = Array.Empty(); - SubtitleFetcherOrder = Array.Empty(); - DisabledLocalMetadataReaders = Array.Empty(); - - SkipSubtitlesIfAudioTrackMatches = true; - RequirePerfectSubtitleMatch = true; - - EnablePhotos = true; - SaveSubtitlesWithMedia = true; - EnableRealtimeMonitor = true; - PathInfos = Array.Empty(); - EnableInternetProviders = true; - EnableAutomaticSeriesGrouping = true; - SeasonZeroDisplayName = "Specials"; - } - } - - public class MediaPathInfo - { - public string Path { get; set; } - - public string NetworkPath { get; set; } - } - - public class TypeOptions - { - public string Type { get; set; } - - public string[] MetadataFetchers { get; set; } - - public string[] MetadataFetcherOrder { get; set; } - - public string[] ImageFetchers { get; set; } - - public string[] ImageFetcherOrder { get; set; } - - public ImageOption[] ImageOptions { get; set; } - - public ImageOption GetImageOptions(ImageType type) - { - foreach (var i in ImageOptions) - { - if (i.Type == type) - { - return i; - } - } - - if (DefaultImageOptions.TryGetValue(Type, out ImageOption[] options)) - { - foreach (var i in options) - { - if (i.Type == type) - { - return i; - } - } - } - - return DefaultInstance; - } - - public int GetLimit(ImageType type) - { - return GetImageOptions(type).Limit; - } - - public int GetMinWidth(ImageType type) - { - return GetImageOptions(type).MinWidth; - } - - public bool IsEnabled(ImageType type) - { - return GetLimit(type) > 0; - } - - public TypeOptions() - { - MetadataFetchers = Array.Empty(); - MetadataFetcherOrder = Array.Empty(); - ImageFetchers = Array.Empty(); - ImageFetcherOrder = Array.Empty(); - ImageOptions = Array.Empty(); - } - - public static Dictionary DefaultImageOptions = new Dictionary - { - { - "Movie", new [] - { - new ImageOption - { - Limit = 1, - MinWidth = 1280, - Type = ImageType.Backdrop - }, - - // Don't download this by default as it's rarely used. - new ImageOption - { - Limit = 0, - Type = ImageType.Art - }, - - // Don't download this by default as it's rarely used. - new ImageOption - { - Limit = 0, - Type = ImageType.Disc - }, - - new ImageOption - { - Limit = 1, - Type = ImageType.Primary - }, - - new ImageOption - { - Limit = 0, - Type = ImageType.Banner - }, - - new ImageOption - { - Limit = 1, - Type = ImageType.Thumb - }, - - new ImageOption - { - Limit = 1, - Type = ImageType.Logo - } - } - }, - { - "MusicVideo", new [] - { - new ImageOption - { - Limit = 1, - MinWidth = 1280, - Type = ImageType.Backdrop - }, - - // Don't download this by default as it's rarely used. - new ImageOption - { - Limit = 0, - Type = ImageType.Art - }, - - // Don't download this by default as it's rarely used. - new ImageOption - { - Limit = 0, - Type = ImageType.Disc - }, - - new ImageOption - { - Limit = 1, - Type = ImageType.Primary - }, - - new ImageOption - { - Limit = 0, - Type = ImageType.Banner - }, - - new ImageOption - { - Limit = 1, - Type = ImageType.Thumb - }, - - new ImageOption - { - Limit = 1, - Type = ImageType.Logo - } - } - }, - { - "Series", new [] - { - new ImageOption - { - Limit = 1, - MinWidth = 1280, - Type = ImageType.Backdrop - }, - - // Don't download this by default as it's rarely used. - new ImageOption - { - Limit = 0, - Type = ImageType.Art - }, - - new ImageOption - { - Limit = 1, - Type = ImageType.Primary - }, - - new ImageOption - { - Limit = 1, - Type = ImageType.Banner - }, - - new ImageOption - { - Limit = 1, - Type = ImageType.Thumb - }, - - new ImageOption - { - Limit = 1, - Type = ImageType.Logo - } - } - }, - { - "MusicAlbum", new [] - { - new ImageOption - { - Limit = 0, - MinWidth = 1280, - Type = ImageType.Backdrop - }, - - // Don't download this by default as it's rarely used. - new ImageOption - { - Limit = 0, - Type = ImageType.Disc - } - } - }, - { - "MusicArtist", new [] - { - new ImageOption - { - Limit = 1, - MinWidth = 1280, - Type = ImageType.Backdrop - }, - - // Don't download this by default - // They do look great, but most artists won't have them, which means a banner view isn't really possible - new ImageOption - { - Limit = 0, - Type = ImageType.Banner - }, - - // Don't download this by default - // Generally not used - new ImageOption - { - Limit = 0, - Type = ImageType.Art - }, - - new ImageOption - { - Limit = 1, - Type = ImageType.Logo - } - } - }, - { - "BoxSet", new [] - { - new ImageOption - { - Limit = 1, - MinWidth = 1280, - Type = ImageType.Backdrop - }, - - new ImageOption - { - Limit = 1, - Type = ImageType.Primary - }, - - new ImageOption - { - Limit = 1, - Type = ImageType.Thumb - }, - - new ImageOption - { - Limit = 1, - Type = ImageType.Logo - }, - - // Don't download this by default as it's rarely used. - new ImageOption - { - Limit = 0, - Type = ImageType.Art - }, - - // Don't download this by default as it's rarely used. - new ImageOption - { - Limit = 0, - Type = ImageType.Disc - }, - - // Don't download this by default as it's rarely used. - new ImageOption - { - Limit = 0, - Type = ImageType.Banner - } - } - }, - { - "Season", new [] - { - new ImageOption - { - Limit = 0, - MinWidth = 1280, - Type = ImageType.Backdrop - }, - - new ImageOption - { - Limit = 1, - Type = ImageType.Primary - }, - - new ImageOption - { - Limit = 0, - Type = ImageType.Banner - }, - - new ImageOption - { - Limit = 0, - Type = ImageType.Thumb - } - } - }, - { - "Episode", new [] - { - new ImageOption - { - Limit = 0, - MinWidth = 1280, - Type = ImageType.Backdrop - }, - - new ImageOption - { - Limit = 1, - Type = ImageType.Primary - } - } - } - }; - - public static ImageOption DefaultInstance = new ImageOption(); } } diff --git a/MediaBrowser.Model/Configuration/MediaPathInfo.cs b/MediaBrowser.Model/Configuration/MediaPathInfo.cs new file mode 100644 index 0000000000..4f311c58f0 --- /dev/null +++ b/MediaBrowser.Model/Configuration/MediaPathInfo.cs @@ -0,0 +1,12 @@ +#nullable disable +#pragma warning disable CS1591 + +namespace MediaBrowser.Model.Configuration +{ + public class MediaPathInfo + { + public string Path { get; set; } + + public string NetworkPath { get; set; } + } +} diff --git a/MediaBrowser.Model/Configuration/MetadataConfiguration.cs b/MediaBrowser.Model/Configuration/MetadataConfiguration.cs index 706831bddc..be044243dd 100644 --- a/MediaBrowser.Model/Configuration/MetadataConfiguration.cs +++ b/MediaBrowser.Model/Configuration/MetadataConfiguration.cs @@ -4,11 +4,11 @@ namespace MediaBrowser.Model.Configuration { public class MetadataConfiguration { - public bool UseFileCreationTimeForDateAdded { get; set; } - public MetadataConfiguration() { UseFileCreationTimeForDateAdded = true; } + + public bool UseFileCreationTimeForDateAdded { get; set; } } } diff --git a/MediaBrowser.Model/Configuration/MetadataOptions.cs b/MediaBrowser.Model/Configuration/MetadataOptions.cs index e7dc3da3cb..76b72bd08e 100644 --- a/MediaBrowser.Model/Configuration/MetadataOptions.cs +++ b/MediaBrowser.Model/Configuration/MetadataOptions.cs @@ -10,6 +10,16 @@ namespace MediaBrowser.Model.Configuration /// public class MetadataOptions { + public MetadataOptions() + { + DisabledMetadataSavers = Array.Empty(); + LocalMetadataReaderOrder = Array.Empty(); + DisabledMetadataFetchers = Array.Empty(); + MetadataFetcherOrder = Array.Empty(); + DisabledImageFetchers = Array.Empty(); + ImageFetcherOrder = Array.Empty(); + } + public string ItemType { get; set; } public string[] DisabledMetadataSavers { get; set; } @@ -23,15 +33,5 @@ namespace MediaBrowser.Model.Configuration public string[] DisabledImageFetchers { get; set; } public string[] ImageFetcherOrder { get; set; } - - public MetadataOptions() - { - DisabledMetadataSavers = Array.Empty(); - LocalMetadataReaderOrder = Array.Empty(); - DisabledMetadataFetchers = Array.Empty(); - MetadataFetcherOrder = Array.Empty(); - DisabledImageFetchers = Array.Empty(); - ImageFetcherOrder = Array.Empty(); - } } } diff --git a/MediaBrowser.Model/Configuration/MetadataPluginSummary.cs b/MediaBrowser.Model/Configuration/MetadataPluginSummary.cs index 0c197ee021..aa07d66237 100644 --- a/MediaBrowser.Model/Configuration/MetadataPluginSummary.cs +++ b/MediaBrowser.Model/Configuration/MetadataPluginSummary.cs @@ -8,6 +8,12 @@ namespace MediaBrowser.Model.Configuration { public class MetadataPluginSummary { + public MetadataPluginSummary() + { + SupportedImageTypes = Array.Empty(); + Plugins = Array.Empty(); + } + /// /// Gets or sets the type of the item. /// @@ -25,11 +31,5 @@ namespace MediaBrowser.Model.Configuration /// /// The supported image types. public ImageType[] SupportedImageTypes { get; set; } - - public MetadataPluginSummary() - { - SupportedImageTypes = Array.Empty(); - Plugins = Array.Empty(); - } } } diff --git a/MediaBrowser.Model/Configuration/TypeOptions.cs b/MediaBrowser.Model/Configuration/TypeOptions.cs new file mode 100644 index 0000000000..d0179e5aab --- /dev/null +++ b/MediaBrowser.Model/Configuration/TypeOptions.cs @@ -0,0 +1,365 @@ +#nullable disable +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Model.Configuration +{ + public class TypeOptions + { + public static readonly ImageOption DefaultInstance = new ImageOption(); + + public static readonly Dictionary DefaultImageOptions = new Dictionary + { + { + "Movie", new[] + { + new ImageOption + { + Limit = 1, + MinWidth = 1280, + Type = ImageType.Backdrop + }, + + // Don't download this by default as it's rarely used. + new ImageOption + { + Limit = 0, + Type = ImageType.Art + }, + + // Don't download this by default as it's rarely used. + new ImageOption + { + Limit = 0, + Type = ImageType.Disc + }, + + new ImageOption + { + Limit = 1, + Type = ImageType.Primary + }, + + new ImageOption + { + Limit = 0, + Type = ImageType.Banner + }, + + new ImageOption + { + Limit = 1, + Type = ImageType.Thumb + }, + + new ImageOption + { + Limit = 1, + Type = ImageType.Logo + } + } + }, + { + "MusicVideo", new[] + { + new ImageOption + { + Limit = 1, + MinWidth = 1280, + Type = ImageType.Backdrop + }, + + // Don't download this by default as it's rarely used. + new ImageOption + { + Limit = 0, + Type = ImageType.Art + }, + + // Don't download this by default as it's rarely used. + new ImageOption + { + Limit = 0, + Type = ImageType.Disc + }, + + new ImageOption + { + Limit = 1, + Type = ImageType.Primary + }, + + new ImageOption + { + Limit = 0, + Type = ImageType.Banner + }, + + new ImageOption + { + Limit = 1, + Type = ImageType.Thumb + }, + + new ImageOption + { + Limit = 1, + Type = ImageType.Logo + } + } + }, + { + "Series", new[] + { + new ImageOption + { + Limit = 1, + MinWidth = 1280, + Type = ImageType.Backdrop + }, + + // Don't download this by default as it's rarely used. + new ImageOption + { + Limit = 0, + Type = ImageType.Art + }, + + new ImageOption + { + Limit = 1, + Type = ImageType.Primary + }, + + new ImageOption + { + Limit = 1, + Type = ImageType.Banner + }, + + new ImageOption + { + Limit = 1, + Type = ImageType.Thumb + }, + + new ImageOption + { + Limit = 1, + Type = ImageType.Logo + } + } + }, + { + "MusicAlbum", new[] + { + new ImageOption + { + Limit = 0, + MinWidth = 1280, + Type = ImageType.Backdrop + }, + + // Don't download this by default as it's rarely used. + new ImageOption + { + Limit = 0, + Type = ImageType.Disc + } + } + }, + { + "MusicArtist", new[] + { + new ImageOption + { + Limit = 1, + MinWidth = 1280, + Type = ImageType.Backdrop + }, + + // Don't download this by default + // They do look great, but most artists won't have them, which means a banner view isn't really possible + new ImageOption + { + Limit = 0, + Type = ImageType.Banner + }, + + // Don't download this by default + // Generally not used + new ImageOption + { + Limit = 0, + Type = ImageType.Art + }, + + new ImageOption + { + Limit = 1, + Type = ImageType.Logo + } + } + }, + { + "BoxSet", new[] + { + new ImageOption + { + Limit = 1, + MinWidth = 1280, + Type = ImageType.Backdrop + }, + + new ImageOption + { + Limit = 1, + Type = ImageType.Primary + }, + + new ImageOption + { + Limit = 1, + Type = ImageType.Thumb + }, + + new ImageOption + { + Limit = 1, + Type = ImageType.Logo + }, + + // Don't download this by default as it's rarely used. + new ImageOption + { + Limit = 0, + Type = ImageType.Art + }, + + // Don't download this by default as it's rarely used. + new ImageOption + { + Limit = 0, + Type = ImageType.Disc + }, + + // Don't download this by default as it's rarely used. + new ImageOption + { + Limit = 0, + Type = ImageType.Banner + } + } + }, + { + "Season", new[] + { + new ImageOption + { + Limit = 0, + MinWidth = 1280, + Type = ImageType.Backdrop + }, + + new ImageOption + { + Limit = 1, + Type = ImageType.Primary + }, + + new ImageOption + { + Limit = 0, + Type = ImageType.Banner + }, + + new ImageOption + { + Limit = 0, + Type = ImageType.Thumb + } + } + }, + { + "Episode", new[] + { + new ImageOption + { + Limit = 0, + MinWidth = 1280, + Type = ImageType.Backdrop + }, + + new ImageOption + { + Limit = 1, + Type = ImageType.Primary + } + } + } + }; + + public TypeOptions() + { + MetadataFetchers = Array.Empty(); + MetadataFetcherOrder = Array.Empty(); + ImageFetchers = Array.Empty(); + ImageFetcherOrder = Array.Empty(); + ImageOptions = Array.Empty(); + } + + public string Type { get; set; } + + public string[] MetadataFetchers { get; set; } + + public string[] MetadataFetcherOrder { get; set; } + + public string[] ImageFetchers { get; set; } + + public string[] ImageFetcherOrder { get; set; } + + public ImageOption[] ImageOptions { get; set; } + + public ImageOption GetImageOptions(ImageType type) + { + foreach (var i in ImageOptions) + { + if (i.Type == type) + { + return i; + } + } + + if (DefaultImageOptions.TryGetValue(Type, out ImageOption[] options)) + { + foreach (var i in options) + { + if (i.Type == type) + { + return i; + } + } + } + + return DefaultInstance; + } + + public int GetLimit(ImageType type) + { + return GetImageOptions(type).Limit; + } + + public int GetMinWidth(ImageType type) + { + return GetImageOptions(type).MinWidth; + } + + public bool IsEnabled(ImageType type) + { + return GetLimit(type) > 0; + } + } +} diff --git a/MediaBrowser.Model/Configuration/UserConfiguration.cs b/MediaBrowser.Model/Configuration/UserConfiguration.cs index cc0e0c4681..935e6cbe10 100644 --- a/MediaBrowser.Model/Configuration/UserConfiguration.cs +++ b/MediaBrowser.Model/Configuration/UserConfiguration.cs @@ -11,6 +11,24 @@ namespace MediaBrowser.Model.Configuration /// public class UserConfiguration { + /// + /// Initializes a new instance of the class. + /// + public UserConfiguration() + { + EnableNextEpisodeAutoPlay = true; + RememberAudioSelections = true; + RememberSubtitleSelections = true; + + HidePlayedInLatest = true; + PlayDefaultAudioTrack = true; + + LatestItemsExcludes = Array.Empty(); + OrderedViews = Array.Empty(); + MyMediaExcludes = Array.Empty(); + GroupedFolders = Array.Empty(); + } + /// /// Gets or sets the audio language preference. /// @@ -52,23 +70,5 @@ namespace MediaBrowser.Model.Configuration public bool RememberSubtitleSelections { get; set; } public bool EnableNextEpisodeAutoPlay { get; set; } - - /// - /// Initializes a new instance of the class. - /// - public UserConfiguration() - { - EnableNextEpisodeAutoPlay = true; - RememberAudioSelections = true; - RememberSubtitleSelections = true; - - HidePlayedInLatest = true; - PlayDefaultAudioTrack = true; - - LatestItemsExcludes = Array.Empty(); - OrderedViews = Array.Empty(); - MyMediaExcludes = Array.Empty(); - GroupedFolders = Array.Empty(); - } } } diff --git a/MediaBrowser.Model/Configuration/XbmcMetadataOptions.cs b/MediaBrowser.Model/Configuration/XbmcMetadataOptions.cs index 4d5f996f84..8ad070dcbf 100644 --- a/MediaBrowser.Model/Configuration/XbmcMetadataOptions.cs +++ b/MediaBrowser.Model/Configuration/XbmcMetadataOptions.cs @@ -5,6 +5,14 @@ namespace MediaBrowser.Model.Configuration { public class XbmcMetadataOptions { + public XbmcMetadataOptions() + { + ReleaseDateFormat = "yyyy-MM-dd"; + + SaveImagePathsInNfo = true; + EnablePathSubstitution = true; + } + public string UserId { get; set; } public string ReleaseDateFormat { get; set; } @@ -14,13 +22,5 @@ namespace MediaBrowser.Model.Configuration public bool EnablePathSubstitution { get; set; } public bool EnableExtraThumbsDuplication { get; set; } - - public XbmcMetadataOptions() - { - ReleaseDateFormat = "yyyy-MM-dd"; - - SaveImagePathsInNfo = true; - EnablePathSubstitution = true; - } } } diff --git a/MediaBrowser.Model/Dlna/AudioOptions.cs b/MediaBrowser.Model/Dlna/AudioOptions.cs index bbb8bf4263..4d4d8d78cb 100644 --- a/MediaBrowser.Model/Dlna/AudioOptions.cs +++ b/MediaBrowser.Model/Dlna/AudioOptions.cs @@ -34,20 +34,20 @@ namespace MediaBrowser.Model.Dlna public DeviceProfile Profile { get; set; } /// - /// Optional. Only needed if a specific AudioStreamIndex or SubtitleStreamIndex are requested. + /// Gets or sets a media source id. Optional. Only needed if a specific AudioStreamIndex or SubtitleStreamIndex are requested. /// public string MediaSourceId { get; set; } public string DeviceId { get; set; } /// - /// Allows an override of supported number of audio channels - /// Example: DeviceProfile supports five channel, but user only has stereo speakers + /// Gets or sets an override of supported number of audio channels + /// Example: DeviceProfile supports five channel, but user only has stereo speakers. /// public int? MaxAudioChannels { get; set; } /// - /// The application's configured quality setting. + /// Gets or sets the application's configured quality setting. /// public int? MaxBitrate { get; set; } @@ -66,6 +66,7 @@ namespace MediaBrowser.Model.Dlna /// /// Gets the maximum bitrate. /// + /// Whether or not this is audio. /// System.Nullable<System.Int32>. public int? GetMaxBitrate(bool isAudio) { diff --git a/MediaBrowser.Model/Dlna/CodecProfile.cs b/MediaBrowser.Model/Dlna/CodecProfile.cs index d4fd3e6730..8343cf028b 100644 --- a/MediaBrowser.Model/Dlna/CodecProfile.cs +++ b/MediaBrowser.Model/Dlna/CodecProfile.cs @@ -9,6 +9,12 @@ namespace MediaBrowser.Model.Dlna { public class CodecProfile { + public CodecProfile() + { + Conditions = Array.Empty(); + ApplyConditions = Array.Empty(); + } + [XmlAttribute("type")] public CodecType Type { get; set; } @@ -22,12 +28,6 @@ namespace MediaBrowser.Model.Dlna [XmlAttribute("container")] public string Container { get; set; } - public CodecProfile() - { - Conditions = Array.Empty(); - ApplyConditions = Array.Empty(); - } - public string[] GetCodecs() { return ContainerProfile.SplitValue(Codec); diff --git a/MediaBrowser.Model/Dlna/ConditionProcessor.cs b/MediaBrowser.Model/Dlna/ConditionProcessor.cs index faf1ee41be..55c4dd0742 100644 --- a/MediaBrowser.Model/Dlna/ConditionProcessor.cs +++ b/MediaBrowser.Model/Dlna/ConditionProcessor.cs @@ -1,8 +1,8 @@ #pragma warning disable CS1591 using System; -using System.Linq; using System.Globalization; +using System.Linq; using MediaBrowser.Model.MediaInfo; namespace MediaBrowser.Model.Dlna diff --git a/MediaBrowser.Model/Dlna/ContainerProfile.cs b/MediaBrowser.Model/Dlna/ContainerProfile.cs index 56c89d854f..d83c8f2f3c 100644 --- a/MediaBrowser.Model/Dlna/ContainerProfile.cs +++ b/MediaBrowser.Model/Dlna/ContainerProfile.cs @@ -9,6 +9,11 @@ namespace MediaBrowser.Model.Dlna { public class ContainerProfile { + public ContainerProfile() + { + Conditions = Array.Empty(); + } + [XmlAttribute("type")] public DlnaProfileType Type { get; set; } @@ -17,11 +22,6 @@ namespace MediaBrowser.Model.Dlna [XmlAttribute("container")] public string Container { get; set; } - public ContainerProfile() - { - Conditions = Array.Empty(); - } - public string[] GetContainers() { return SplitValue(Container); diff --git a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs index 50e3374f77..ec106f1054 100644 --- a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs +++ b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs @@ -81,13 +81,13 @@ namespace MediaBrowser.Model.Dlna DlnaFlags.DlnaV15; // if (isDirectStream) - //{ - // flagValue = flagValue | DlnaFlags.ByteBasedSeek; - //} - // else if (runtimeTicks.HasValue) - //{ - // flagValue = flagValue | DlnaFlags.TimeBasedSeek; - //} + // { + // flagValue = flagValue | DlnaFlags.ByteBasedSeek; + // } + // else if (runtimeTicks.HasValue) + // { + // flagValue = flagValue | DlnaFlags.TimeBasedSeek; + // } string dlnaflags = string.Format( CultureInfo.InvariantCulture, @@ -150,16 +150,18 @@ namespace MediaBrowser.Model.Dlna DlnaFlags.DlnaV15; // if (isDirectStream) - //{ - // flagValue = flagValue | DlnaFlags.ByteBasedSeek; - //} - // else if (runtimeTicks.HasValue) - //{ - // flagValue = flagValue | DlnaFlags.TimeBasedSeek; - //} - - string dlnaflags = string.Format(CultureInfo.InvariantCulture, ";DLNA.ORG_FLAGS={0}", - DlnaMaps.FlagsToString(flagValue)); + // { + // flagValue = flagValue | DlnaFlags.ByteBasedSeek; + // } + // else if (runtimeTicks.HasValue) + // { + // flagValue = flagValue | DlnaFlags.TimeBasedSeek; + // } + + string dlnaflags = string.Format( + CultureInfo.InvariantCulture, + ";DLNA.ORG_FLAGS={0}", + DlnaMaps.FlagsToString(flagValue)); ResponseProfile mediaProfile = _profile.GetVideoMediaProfile( container, diff --git a/MediaBrowser.Model/Dlna/IDeviceDiscovery.cs b/MediaBrowser.Model/Dlna/IDeviceDiscovery.cs index 05209e53d0..086088deae 100644 --- a/MediaBrowser.Model/Dlna/IDeviceDiscovery.cs +++ b/MediaBrowser.Model/Dlna/IDeviceDiscovery.cs @@ -8,6 +8,7 @@ namespace MediaBrowser.Model.Dlna public interface IDeviceDiscovery { event EventHandler> DeviceDiscovered; + event EventHandler> DeviceLeft; } } diff --git a/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs b/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs index 3c955989a1..f61b8d59e8 100644 --- a/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs +++ b/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs @@ -57,7 +57,6 @@ namespace MediaBrowser.Model.Dlna string.Equals(container, "mpegts", StringComparison.OrdinalIgnoreCase) || string.Equals(container, "m2ts", StringComparison.OrdinalIgnoreCase)) { - return ResolveVideoMPEG2TSFormat(videoCodec, audioCodec, width, height, timestampType); } @@ -323,7 +322,6 @@ namespace MediaBrowser.Model.Dlna if (string.Equals(videoCodec, "wmv", StringComparison.OrdinalIgnoreCase) && (string.IsNullOrEmpty(audioCodec) || string.Equals(audioCodec, "wma", StringComparison.OrdinalIgnoreCase) || string.Equals(videoCodec, "wmapro", StringComparison.OrdinalIgnoreCase))) { - if (width.HasValue && height.HasValue) { if ((width.Value <= 720) && (height.Value <= 576)) @@ -479,7 +477,9 @@ namespace MediaBrowser.Model.Dlna { if (string.Equals(container, "jpeg", StringComparison.OrdinalIgnoreCase) || string.Equals(container, "jpg", StringComparison.OrdinalIgnoreCase)) + { return ResolveImageJPGFormat(width, height); + } if (string.Equals(container, "png", StringComparison.OrdinalIgnoreCase)) { diff --git a/MediaBrowser.Model/Dlna/ResolutionConfiguration.cs b/MediaBrowser.Model/Dlna/ResolutionConfiguration.cs index 30c44fbe0e..f8f76c69d8 100644 --- a/MediaBrowser.Model/Dlna/ResolutionConfiguration.cs +++ b/MediaBrowser.Model/Dlna/ResolutionConfiguration.cs @@ -4,14 +4,14 @@ namespace MediaBrowser.Model.Dlna { public class ResolutionConfiguration { - public int MaxWidth { get; set; } - - public int MaxBitrate { get; set; } - public ResolutionConfiguration(int maxWidth, int maxBitrate) { MaxWidth = maxWidth; MaxBitrate = maxBitrate; } + + public int MaxWidth { get; set; } + + public int MaxBitrate { get; set; } } } diff --git a/MediaBrowser.Model/Dlna/ResponseProfile.cs b/MediaBrowser.Model/Dlna/ResponseProfile.cs index 48f53f06c2..bf9661f7f3 100644 --- a/MediaBrowser.Model/Dlna/ResponseProfile.cs +++ b/MediaBrowser.Model/Dlna/ResponseProfile.cs @@ -8,6 +8,11 @@ namespace MediaBrowser.Model.Dlna { public class ResponseProfile { + public ResponseProfile() + { + Conditions = Array.Empty(); + } + [XmlAttribute("container")] public string Container { get; set; } @@ -28,11 +33,6 @@ namespace MediaBrowser.Model.Dlna public ProfileCondition[] Conditions { get; set; } - public ResponseProfile() - { - Conditions = Array.Empty(); - } - public string[] GetContainers() { return ContainerProfile.SplitValue(Container); diff --git a/MediaBrowser.Model/Dlna/SearchCriteria.cs b/MediaBrowser.Model/Dlna/SearchCriteria.cs index 94f5bd3dbe..b1fc48c087 100644 --- a/MediaBrowser.Model/Dlna/SearchCriteria.cs +++ b/MediaBrowser.Model/Dlna/SearchCriteria.cs @@ -7,31 +7,6 @@ namespace MediaBrowser.Model.Dlna { public class SearchCriteria { - public SearchType SearchType { get; set; } - - /// - /// Splits the specified string. - /// - /// The string. - /// The term. - /// The limit. - /// System.String[]. - private static string[] RegexSplit(string str, string term, int limit) - { - return new Regex(term).Split(str, limit); - } - - /// - /// Splits the specified string. - /// - /// The string. - /// The term. - /// System.String[]. - private static string[] RegexSplit(string str, string term) - { - return Regex.Split(str, term, RegexOptions.IgnoreCase); - } - public SearchCriteria(string search) { if (search.Length == 0) @@ -48,8 +23,8 @@ namespace MediaBrowser.Model.Dlna if (subFactors.Length == 3) { - if (string.Equals("upnp:class", subFactors[0], StringComparison.OrdinalIgnoreCase) && - (string.Equals("=", subFactors[1], StringComparison.Ordinal) || string.Equals("derivedfrom", subFactors[1], StringComparison.OrdinalIgnoreCase))) + if (string.Equals("upnp:class", subFactors[0], StringComparison.OrdinalIgnoreCase) + && (string.Equals("=", subFactors[1], StringComparison.Ordinal) || string.Equals("derivedfrom", subFactors[1], StringComparison.OrdinalIgnoreCase))) { if (string.Equals("\"object.item.imageItem\"", subFactors[2], StringComparison.Ordinal) || string.Equals("\"object.item.imageItem.photo\"", subFactors[2], StringComparison.OrdinalIgnoreCase)) { @@ -71,5 +46,30 @@ namespace MediaBrowser.Model.Dlna } } } + + public SearchType SearchType { get; set; } + + /// + /// Splits the specified string. + /// + /// The string. + /// The term. + /// The limit. + /// System.String[]. + private static string[] RegexSplit(string str, string term, int limit) + { + return new Regex(term).Split(str, limit); + } + + /// + /// Splits the specified string. + /// + /// The string. + /// The term. + /// System.String[]. + private static string[] RegexSplit(string str, string term) + { + return Regex.Split(str, term, RegexOptions.IgnoreCase); + } } } diff --git a/MediaBrowser.Model/Dlna/SortCriteria.cs b/MediaBrowser.Model/Dlna/SortCriteria.cs index 53e4540cbb..7769d0bd3e 100644 --- a/MediaBrowser.Model/Dlna/SortCriteria.cs +++ b/MediaBrowser.Model/Dlna/SortCriteria.cs @@ -6,10 +6,10 @@ namespace MediaBrowser.Model.Dlna { public class SortCriteria { - public SortOrder SortOrder => SortOrder.Ascending; - public SortCriteria(string value) { } + + public SortOrder SortOrder => SortOrder.Ascending; } } diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index a3983afe5f..bf33691c77 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -227,7 +227,7 @@ namespace MediaBrowser.Model.Dlna } } - public static string NormalizeMediaSourceFormatIntoSingleContainer(string inputContainer, string _, DeviceProfile profile, DlnaProfileType type) + public static string NormalizeMediaSourceFormatIntoSingleContainer(string inputContainer, DeviceProfile profile, DlnaProfileType type) { if (string.IsNullOrEmpty(inputContainer)) { @@ -274,14 +274,14 @@ namespace MediaBrowser.Model.Dlna if (options.ForceDirectPlay) { playlistItem.PlayMethod = PlayMethod.DirectPlay; - playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, item.Path, options.Profile, DlnaProfileType.Audio); + playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio); return playlistItem; } if (options.ForceDirectStream) { playlistItem.PlayMethod = PlayMethod.DirectStream; - playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, item.Path, options.Profile, DlnaProfileType.Audio); + playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio); return playlistItem; } @@ -349,7 +349,7 @@ namespace MediaBrowser.Model.Dlna playlistItem.PlayMethod = PlayMethod.DirectStream; } - playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, item.Path, options.Profile, DlnaProfileType.Audio); + playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio); return playlistItem; } @@ -698,7 +698,7 @@ namespace MediaBrowser.Model.Dlna if (directPlay != null) { playlistItem.PlayMethod = directPlay.Value; - playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, item.Path, options.Profile, DlnaProfileType.Video); + playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video); if (subtitleStream != null) { @@ -1404,7 +1404,9 @@ namespace MediaBrowser.Model.Dlna { _logger.LogInformation( "Bitrate exceeds {PlayBackMethod} limit: media bitrate: {MediaBitrate}, max bitrate: {MaxBitrate}", - playMethod, itemBitrate, requestedMaxBitrate); + playMethod, + itemBitrate, + requestedMaxBitrate); return false; } diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index 4765052d59..f7010dcd0f 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -27,45 +27,6 @@ namespace MediaBrowser.Model.Dlna StreamOptions = new Dictionary(StringComparer.OrdinalIgnoreCase); } - public void SetOption(string qualifier, string name, string value) - { - if (string.IsNullOrEmpty(qualifier)) - { - SetOption(name, value); - } - else - { - SetOption(qualifier + "-" + name, value); - } - } - - public void SetOption(string name, string value) - { - StreamOptions[name] = value; - } - - public string GetOption(string qualifier, string name) - { - var value = GetOption(qualifier + "-" + name); - - if (string.IsNullOrEmpty(value)) - { - value = GetOption(name); - } - - return value; - } - - public string GetOption(string name) - { - if (StreamOptions.TryGetValue(name, out var value)) - { - return value; - } - - return null; - } - public Guid ItemId { get; set; } public PlayMethod PlayMethod { get; set; } @@ -152,887 +113,928 @@ namespace MediaBrowser.Model.Dlna PlayMethod == PlayMethod.DirectStream || PlayMethod == PlayMethod.DirectPlay; - public string ToUrl(string baseUrl, string accessToken) + /// + /// Gets the audio stream that will be used. + /// + public MediaStream TargetAudioStream { - if (PlayMethod == PlayMethod.DirectPlay) + get { - return MediaSource.Path; + if (MediaSource != null) + { + return MediaSource.GetDefaultAudioStream(AudioStreamIndex); + } + + return null; } + } - if (string.IsNullOrEmpty(baseUrl)) + /// + /// Gets the video stream that will be used. + /// + public MediaStream TargetVideoStream + { + get { - throw new ArgumentNullException(nameof(baseUrl)); + if (MediaSource != null) + { + return MediaSource.VideoStream; + } + + return null; } + } - var list = new List(); - foreach (NameValuePair pair in BuildParams(this, accessToken)) + /// + /// Gets the audio sample rate that will be in the output stream. + /// + public int? TargetAudioSampleRate + { + get { - if (string.IsNullOrEmpty(pair.Value)) + var stream = TargetAudioStream; + return AudioSampleRate.HasValue && !IsDirectStream + ? AudioSampleRate + : stream == null ? null : stream.SampleRate; + } + } + + /// + /// Gets the audio sample rate that will be in the output stream. + /// + public int? TargetAudioBitDepth + { + get + { + if (IsDirectStream) { - continue; + return TargetAudioStream == null ? (int?)null : TargetAudioStream.BitDepth; } - // Try to keep the url clean by omitting defaults - if (string.Equals(pair.Name, "StartTimeTicks", StringComparison.OrdinalIgnoreCase) && - string.Equals(pair.Value, "0", StringComparison.OrdinalIgnoreCase)) + var targetAudioCodecs = TargetAudioCodec; + var audioCodec = targetAudioCodecs.Length == 0 ? null : targetAudioCodecs[0]; + if (!string.IsNullOrEmpty(audioCodec)) { - continue; + return GetTargetAudioBitDepth(audioCodec); } - if (string.Equals(pair.Name, "SubtitleStreamIndex", StringComparison.OrdinalIgnoreCase) && - string.Equals(pair.Value, "-1", StringComparison.OrdinalIgnoreCase)) + return TargetAudioStream == null ? (int?)null : TargetAudioStream.BitDepth; + } + } + + /// + /// Gets the audio sample rate that will be in the output stream. + /// + public int? TargetVideoBitDepth + { + get + { + if (IsDirectStream) { - continue; + return TargetVideoStream == null ? (int?)null : TargetVideoStream.BitDepth; } - // Be careful, IsDirectStream==true by default (Static != false or not in query). - // See initialization of StreamingRequestDto in AudioController.GetAudioStream() method : Static = @static ?? true. - if (string.Equals(pair.Name, "Static", StringComparison.OrdinalIgnoreCase) && - string.Equals(pair.Value, "true", StringComparison.OrdinalIgnoreCase)) + var targetVideoCodecs = TargetVideoCodec; + var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0]; + if (!string.IsNullOrEmpty(videoCodec)) { - continue; + return GetTargetVideoBitDepth(videoCodec); } - var encodedValue = pair.Value.Replace(" ", "%20", StringComparison.Ordinal); - - list.Add(string.Format(CultureInfo.InvariantCulture, "{0}={1}", pair.Name, encodedValue)); + return TargetVideoStream == null ? (int?)null : TargetVideoStream.BitDepth; } - - string queryString = string.Join('&', list); - - return GetUrl(baseUrl, queryString); } - private string GetUrl(string baseUrl, string queryString) + /// + /// Gets the target reference frames. + /// + /// The target reference frames. + public int? TargetRefFrames { - if (string.IsNullOrEmpty(baseUrl)) + get { - throw new ArgumentNullException(nameof(baseUrl)); - } - - string extension = string.IsNullOrEmpty(Container) ? string.Empty : "." + Container; - - baseUrl = baseUrl.TrimEnd('/'); + if (IsDirectStream) + { + return TargetVideoStream == null ? (int?)null : TargetVideoStream.RefFrames; + } - if (MediaType == DlnaProfileType.Audio) - { - if (string.Equals(SubProtocol, "hls", StringComparison.OrdinalIgnoreCase)) + var targetVideoCodecs = TargetVideoCodec; + var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0]; + if (!string.IsNullOrEmpty(videoCodec)) { - return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString); + return GetTargetRefFrames(videoCodec); } - return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString); + return TargetVideoStream == null ? (int?)null : TargetVideoStream.RefFrames; } + } - if (string.Equals(SubProtocol, "hls", StringComparison.OrdinalIgnoreCase)) + /// + /// Gets the audio sample rate that will be in the output stream. + /// + public float? TargetFramerate + { + get { - return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString); + var stream = TargetVideoStream; + return MaxFramerate.HasValue && !IsDirectStream + ? MaxFramerate + : stream == null ? null : stream.AverageFrameRate ?? stream.RealFrameRate; } - - return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString); } - private static List BuildParams(StreamInfo item, string accessToken) + /// + /// Gets the audio sample rate that will be in the output stream. + /// + public double? TargetVideoLevel { - var list = new List(); - - string audioCodecs = item.AudioCodecs.Length == 0 ? - string.Empty : - string.Join(',', item.AudioCodecs); - - string videoCodecs = item.VideoCodecs.Length == 0 ? - string.Empty : - string.Join(',', item.VideoCodecs); - - list.Add(new NameValuePair("DeviceProfileId", item.DeviceProfileId ?? string.Empty)); - list.Add(new NameValuePair("DeviceId", item.DeviceId ?? string.Empty)); - list.Add(new NameValuePair("MediaSourceId", item.MediaSourceId ?? string.Empty)); - list.Add(new NameValuePair("Static", item.IsDirectStream.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); - list.Add(new NameValuePair("VideoCodec", videoCodecs)); - list.Add(new NameValuePair("AudioCodec", audioCodecs)); - list.Add(new NameValuePair("AudioStreamIndex", item.AudioStreamIndex.HasValue ? item.AudioStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - list.Add(new NameValuePair("SubtitleStreamIndex", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - list.Add(new NameValuePair("VideoBitrate", item.VideoBitrate.HasValue ? item.VideoBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - list.Add(new NameValuePair("AudioBitrate", item.AudioBitrate.HasValue ? item.AudioBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - list.Add(new NameValuePair("AudioSampleRate", item.AudioSampleRate.HasValue ? item.AudioSampleRate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - - list.Add(new NameValuePair("MaxFramerate", item.MaxFramerate.HasValue ? item.MaxFramerate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - list.Add(new NameValuePair("MaxWidth", item.MaxWidth.HasValue ? item.MaxWidth.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - list.Add(new NameValuePair("MaxHeight", item.MaxHeight.HasValue ? item.MaxHeight.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - - long startPositionTicks = item.StartPositionTicks; + get + { + if (IsDirectStream) + { + return TargetVideoStream == null ? (double?)null : TargetVideoStream.Level; + } - var isHls = string.Equals(item.SubProtocol, "hls", StringComparison.OrdinalIgnoreCase); + var targetVideoCodecs = TargetVideoCodec; + var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0]; + if (!string.IsNullOrEmpty(videoCodec)) + { + return GetTargetVideoLevel(videoCodec); + } - if (isHls) - { - list.Add(new NameValuePair("StartTimeTicks", string.Empty)); + return TargetVideoStream == null ? (double?)null : TargetVideoStream.Level; } - else + } + + /// + /// Gets the audio sample rate that will be in the output stream. + /// + public int? TargetPacketLength + { + get { - list.Add(new NameValuePair("StartTimeTicks", startPositionTicks.ToString(CultureInfo.InvariantCulture))); + var stream = TargetVideoStream; + return !IsDirectStream + ? null + : stream == null ? null : stream.PacketLength; } + } - list.Add(new NameValuePair("PlaySessionId", item.PlaySessionId ?? string.Empty)); - list.Add(new NameValuePair("api_key", accessToken ?? string.Empty)); - - string liveStreamId = item.MediaSource?.LiveStreamId; - list.Add(new NameValuePair("LiveStreamId", liveStreamId ?? string.Empty)); - - list.Add(new NameValuePair("SubtitleMethod", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleDeliveryMethod.ToString() : string.Empty)); - - if (!item.IsDirectStream) + /// + /// Gets the audio sample rate that will be in the output stream. + /// + public string TargetVideoProfile + { + get { - if (item.RequireNonAnamorphic) + if (IsDirectStream) { - list.Add(new NameValuePair("RequireNonAnamorphic", item.RequireNonAnamorphic.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); + return TargetVideoStream == null ? null : TargetVideoStream.Profile; } - list.Add(new NameValuePair("TranscodingMaxAudioChannels", item.TranscodingMaxAudioChannels.HasValue ? item.TranscodingMaxAudioChannels.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - - if (item.EnableSubtitlesInManifest) + var targetVideoCodecs = TargetVideoCodec; + var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0]; + if (!string.IsNullOrEmpty(videoCodec)) { - list.Add(new NameValuePair("EnableSubtitlesInManifest", item.EnableSubtitlesInManifest.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); - } - - if (item.EnableMpegtsM2TsMode) - { - list.Add(new NameValuePair("EnableMpegtsM2TsMode", item.EnableMpegtsM2TsMode.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); - } - - if (item.EstimateContentLength) - { - list.Add(new NameValuePair("EstimateContentLength", item.EstimateContentLength.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); - } - - if (item.TranscodeSeekInfo != TranscodeSeekInfo.Auto) - { - list.Add(new NameValuePair("TranscodeSeekInfo", item.TranscodeSeekInfo.ToString().ToLowerInvariant())); - } - - if (item.CopyTimestamps) - { - list.Add(new NameValuePair("CopyTimestamps", item.CopyTimestamps.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); - } - - list.Add(new NameValuePair("RequireAvc", item.RequireAvc.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); - } - - list.Add(new NameValuePair("Tag", item.MediaSource.ETag ?? string.Empty)); - - string subtitleCodecs = item.SubtitleCodecs.Length == 0 ? - string.Empty : - string.Join(',', item.SubtitleCodecs); - - list.Add(new NameValuePair("SubtitleCodec", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed ? subtitleCodecs : string.Empty)); - - if (isHls) - { - list.Add(new NameValuePair("SegmentContainer", item.Container ?? string.Empty)); - - if (item.SegmentLength.HasValue) - { - list.Add(new NameValuePair("SegmentLength", item.SegmentLength.Value.ToString(CultureInfo.InvariantCulture))); - } - - if (item.MinSegments.HasValue) - { - list.Add(new NameValuePair("MinSegments", item.MinSegments.Value.ToString(CultureInfo.InvariantCulture))); - } - - list.Add(new NameValuePair("BreakOnNonKeyFrames", item.BreakOnNonKeyFrames.ToString(CultureInfo.InvariantCulture))); - } - - foreach (var pair in item.StreamOptions) - { - if (string.IsNullOrEmpty(pair.Value)) - { - continue; + return GetOption(videoCodec, "profile"); } - // strip spaces to avoid having to encode h264 profile names - list.Add(new NameValuePair(pair.Key, pair.Value.Replace(" ", string.Empty, StringComparison.Ordinal))); + return TargetVideoStream == null ? null : TargetVideoStream.Profile; } + } - if (!item.IsDirectStream) + /// + /// Gets the target video codec tag. + /// + /// The target video codec tag. + public string TargetVideoCodecTag + { + get { - list.Add(new NameValuePair("TranscodeReasons", string.Join(',', item.TranscodeReasons.Distinct().Select(i => i.ToString())))); + var stream = TargetVideoStream; + return !IsDirectStream + ? null + : stream == null ? null : stream.CodecTag; } - - return list; } - public List GetExternalSubtitles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, string baseUrl, string accessToken) + /// + /// Gets the audio bitrate that will be in the output stream. + /// + public int? TargetAudioBitrate { - return GetExternalSubtitles(transcoderSupport, includeSelectedTrackOnly, false, baseUrl, accessToken); + get + { + var stream = TargetAudioStream; + return AudioBitrate.HasValue && !IsDirectStream + ? AudioBitrate + : stream == null ? null : stream.BitRate; + } } - public List GetExternalSubtitles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, bool enableAllProfiles, string baseUrl, string accessToken) + /// + /// Gets the audio channels that will be in the output stream. + /// + public int? TargetAudioChannels { - var list = GetSubtitleProfiles(transcoderSupport, includeSelectedTrackOnly, enableAllProfiles, baseUrl, accessToken); - var newList = new List(); - - // First add the selected track - foreach (SubtitleStreamInfo stream in list) + get { - if (stream.DeliveryMethod == SubtitleDeliveryMethod.External) + if (IsDirectStream) { - newList.Add(stream); + return TargetAudioStream == null ? (int?)null : TargetAudioStream.Channels; } - } - return newList; - } + var targetAudioCodecs = TargetAudioCodec; + var codec = targetAudioCodecs.Length == 0 ? null : targetAudioCodecs[0]; + if (!string.IsNullOrEmpty(codec)) + { + return GetTargetRefFrames(codec); + } - public List GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, string baseUrl, string accessToken) - { - return GetSubtitleProfiles(transcoderSupport, includeSelectedTrackOnly, false, baseUrl, accessToken); + return TargetAudioStream == null ? (int?)null : TargetAudioStream.Channels; + } } - public List GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, bool enableAllProfiles, string baseUrl, string accessToken) + /// + /// Gets the audio codec that will be in the output stream. + /// + public string[] TargetAudioCodec { - var list = new List(); + get + { + var stream = TargetAudioStream; - // HLS will preserve timestamps so we can just grab the full subtitle stream - long startPositionTicks = string.Equals(SubProtocol, "hls", StringComparison.OrdinalIgnoreCase) - ? 0 - : (PlayMethod == PlayMethod.Transcode && !CopyTimestamps ? StartPositionTicks : 0); + string inputCodec = stream?.Codec; - // First add the selected track - if (SubtitleStreamIndex.HasValue) - { - foreach (var stream in MediaSource.MediaStreams) + if (IsDirectStream) { - if (stream.Type == MediaStreamType.Subtitle && stream.Index == SubtitleStreamIndex.Value) - { - AddSubtitleProfiles(list, stream, transcoderSupport, enableAllProfiles, baseUrl, accessToken, startPositionTicks); - } + return string.IsNullOrEmpty(inputCodec) ? Array.Empty() : new[] { inputCodec }; } - } - if (!includeSelectedTrackOnly) - { - foreach (var stream in MediaSource.MediaStreams) + foreach (string codec in AudioCodecs) { - if (stream.Type == MediaStreamType.Subtitle && (!SubtitleStreamIndex.HasValue || stream.Index != SubtitleStreamIndex.Value)) + if (string.Equals(codec, inputCodec, StringComparison.OrdinalIgnoreCase)) { - AddSubtitleProfiles(list, stream, transcoderSupport, enableAllProfiles, baseUrl, accessToken, startPositionTicks); + return string.IsNullOrEmpty(codec) ? Array.Empty() : new[] { codec }; } } - } - - return list; - } - - private void AddSubtitleProfiles(List list, MediaStream stream, ITranscoderSupport transcoderSupport, bool enableAllProfiles, string baseUrl, string accessToken, long startPositionTicks) - { - if (enableAllProfiles) - { - foreach (var profile in DeviceProfile.SubtitleProfiles) - { - var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, new[] { profile }, transcoderSupport); - - list.Add(info); - } - } - else - { - var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, DeviceProfile.SubtitleProfiles, transcoderSupport); - list.Add(info); + return AudioCodecs; } } - private SubtitleStreamInfo GetSubtitleStreamInfo(MediaStream stream, string baseUrl, string accessToken, long startPositionTicks, SubtitleProfile[] subtitleProfiles, ITranscoderSupport transcoderSupport) + public string[] TargetVideoCodec { - var subtitleProfile = StreamBuilder.GetSubtitleProfile(MediaSource, stream, subtitleProfiles, PlayMethod, transcoderSupport, Container, SubProtocol); - var info = new SubtitleStreamInfo + get { - IsForced = stream.IsForced, - Language = stream.Language, - Name = stream.Language ?? "Unknown", - Format = subtitleProfile.Format, - Index = stream.Index, - DeliveryMethod = subtitleProfile.Method, - DisplayTitle = stream.DisplayTitle - }; + var stream = TargetVideoStream; - if (info.DeliveryMethod == SubtitleDeliveryMethod.External) - { - if (MediaSource.Protocol == MediaProtocol.File || !string.Equals(stream.Codec, subtitleProfile.Format, StringComparison.OrdinalIgnoreCase) || !stream.IsExternal) + string inputCodec = stream?.Codec; + + if (IsDirectStream) { - info.Url = string.Format(CultureInfo.InvariantCulture, "{0}/Videos/{1}/{2}/Subtitles/{3}/{4}/Stream.{5}", - baseUrl, - ItemId, - MediaSourceId, - stream.Index.ToString(CultureInfo.InvariantCulture), - startPositionTicks.ToString(CultureInfo.InvariantCulture), - subtitleProfile.Format); + return string.IsNullOrEmpty(inputCodec) ? Array.Empty() : new[] { inputCodec }; + } - if (!string.IsNullOrEmpty(accessToken)) + foreach (string codec in VideoCodecs) + { + if (string.Equals(codec, inputCodec, StringComparison.OrdinalIgnoreCase)) { - info.Url += "?api_key=" + accessToken; + return string.IsNullOrEmpty(codec) ? Array.Empty() : new[] { codec }; } - - info.IsExternalUrl = false; } - else - { - info.Url = stream.Path; - info.IsExternalUrl = true; - } - } - return info; + return VideoCodecs; + } } /// - /// Returns the audio stream that will be used. + /// Gets the audio channels that will be in the output stream. /// - public MediaStream TargetAudioStream + public long? TargetSize { get { - if (MediaSource != null) + if (IsDirectStream) { - return MediaSource.GetDefaultAudioStream(AudioStreamIndex); + return MediaSource.Size; + } + + if (RunTimeTicks.HasValue) + { + int? totalBitrate = TargetTotalBitrate; + + double totalSeconds = RunTimeTicks.Value; + // Convert to ms + totalSeconds /= 10000; + // Convert to seconds + totalSeconds /= 1000; + + return totalBitrate.HasValue ? + Convert.ToInt64(totalBitrate.Value * totalSeconds) : + (long?)null; } return null; } } - /// - /// Returns the video stream that will be used. - /// - public MediaStream TargetVideoStream + public int? TargetVideoBitrate { get { - if (MediaSource != null) - { - return MediaSource.VideoStream; - } + var stream = TargetVideoStream; - return null; + return VideoBitrate.HasValue && !IsDirectStream + ? VideoBitrate + : stream == null ? null : stream.BitRate; } } - /// - /// Predicts the audio sample rate that will be in the output stream. - /// - public int? TargetAudioSampleRate + public TransportStreamTimestamp TargetTimestamp { get { - var stream = TargetAudioStream; - return AudioSampleRate.HasValue && !IsDirectStream - ? AudioSampleRate - : stream == null ? null : stream.SampleRate; + var defaultValue = string.Equals(Container, "m2ts", StringComparison.OrdinalIgnoreCase) + ? TransportStreamTimestamp.Valid + : TransportStreamTimestamp.None; + + return !IsDirectStream + ? defaultValue + : MediaSource == null ? defaultValue : MediaSource.Timestamp ?? TransportStreamTimestamp.None; } } - /// - /// Predicts the audio sample rate that will be in the output stream. - /// - public int? TargetAudioBitDepth + public int? TargetTotalBitrate => (TargetAudioBitrate ?? 0) + (TargetVideoBitrate ?? 0); + + public bool? IsTargetAnamorphic { get { if (IsDirectStream) { - return TargetAudioStream == null ? (int?)null : TargetAudioStream.BitDepth; - } - - var targetAudioCodecs = TargetAudioCodec; - var audioCodec = targetAudioCodecs.Length == 0 ? null : targetAudioCodecs[0]; - if (!string.IsNullOrEmpty(audioCodec)) - { - return GetTargetAudioBitDepth(audioCodec); + return TargetVideoStream == null ? null : TargetVideoStream.IsAnamorphic; } - return TargetAudioStream == null ? (int?)null : TargetAudioStream.BitDepth; + return false; } } - /// - /// Predicts the audio sample rate that will be in the output stream. - /// - public int? TargetVideoBitDepth + public bool? IsTargetInterlaced { get { if (IsDirectStream) { - return TargetVideoStream == null ? (int?)null : TargetVideoStream.BitDepth; + return TargetVideoStream == null ? (bool?)null : TargetVideoStream.IsInterlaced; } var targetVideoCodecs = TargetVideoCodec; var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0]; if (!string.IsNullOrEmpty(videoCodec)) { - return GetTargetVideoBitDepth(videoCodec); + if (string.Equals(GetOption(videoCodec, "deinterlace"), "true", StringComparison.OrdinalIgnoreCase)) + { + return false; + } } - return TargetVideoStream == null ? (int?)null : TargetVideoStream.BitDepth; + return TargetVideoStream == null ? (bool?)null : TargetVideoStream.IsInterlaced; } } - /// - /// Gets the target reference frames. - /// - /// The target reference frames. - public int? TargetRefFrames + public bool? IsTargetAVC { get { if (IsDirectStream) { - return TargetVideoStream == null ? (int?)null : TargetVideoStream.RefFrames; - } - - var targetVideoCodecs = TargetVideoCodec; - var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0]; - if (!string.IsNullOrEmpty(videoCodec)) - { - return GetTargetRefFrames(videoCodec); + return TargetVideoStream == null ? null : TargetVideoStream.IsAVC; } - return TargetVideoStream == null ? (int?)null : TargetVideoStream.RefFrames; + return true; } } - /// - /// Predicts the audio sample rate that will be in the output stream. - /// - public float? TargetFramerate + public int? TargetWidth { get { - var stream = TargetVideoStream; - return MaxFramerate.HasValue && !IsDirectStream - ? MaxFramerate - : stream == null ? null : stream.AverageFrameRate ?? stream.RealFrameRate; + var videoStream = TargetVideoStream; + + if (videoStream != null && videoStream.Width.HasValue && videoStream.Height.HasValue) + { + ImageDimensions size = new ImageDimensions(videoStream.Width.Value, videoStream.Height.Value); + + size = DrawingUtils.Resize(size, 0, 0, MaxWidth ?? 0, MaxHeight ?? 0); + + return size.Width; + } + + return MaxWidth; } } - /// - /// Predicts the audio sample rate that will be in the output stream. - /// - public double? TargetVideoLevel + public int? TargetHeight { get { - if (IsDirectStream) - { - return TargetVideoStream == null ? (double?)null : TargetVideoStream.Level; - } + var videoStream = TargetVideoStream; - var targetVideoCodecs = TargetVideoCodec; - var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0]; - if (!string.IsNullOrEmpty(videoCodec)) + if (videoStream != null && videoStream.Width.HasValue && videoStream.Height.HasValue) { - return GetTargetVideoLevel(videoCodec); + ImageDimensions size = new ImageDimensions(videoStream.Width.Value, videoStream.Height.Value); + + size = DrawingUtils.Resize(size, 0, 0, MaxWidth ?? 0, MaxHeight ?? 0); + + return size.Height; } - return TargetVideoStream == null ? (double?)null : TargetVideoStream.Level; + return MaxHeight; } } - public int? GetTargetVideoBitDepth(string codec) + public int? TargetVideoStreamCount { - var value = GetOption(codec, "videobitdepth"); - if (string.IsNullOrEmpty(value)) + get { - return null; - } + if (IsDirectStream) + { + return GetMediaStreamCount(MediaStreamType.Video, int.MaxValue); + } - if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) - { - return result; + return GetMediaStreamCount(MediaStreamType.Video, 1); } - - return null; } - public int? GetTargetAudioBitDepth(string codec) + public int? TargetAudioStreamCount { - var value = GetOption(codec, "audiobitdepth"); - if (string.IsNullOrEmpty(value)) - { - return null; - } - - if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) + get { - return result; - } + if (IsDirectStream) + { + return GetMediaStreamCount(MediaStreamType.Audio, int.MaxValue); + } - return null; + return GetMediaStreamCount(MediaStreamType.Audio, 1); + } } - public double? GetTargetVideoLevel(string codec) + public void SetOption(string qualifier, string name, string value) { - var value = GetOption(codec, "level"); - if (string.IsNullOrEmpty(value)) + if (string.IsNullOrEmpty(qualifier)) { - return null; + SetOption(name, value); } - - if (double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) + else { - return result; + SetOption(qualifier + "-" + name, value); } + } - return null; + public void SetOption(string name, string value) + { + StreamOptions[name] = value; } - public int? GetTargetRefFrames(string codec) + public string GetOption(string qualifier, string name) { - var value = GetOption(codec, "maxrefframes"); + var value = GetOption(qualifier + "-" + name); + if (string.IsNullOrEmpty(value)) { - return null; + value = GetOption(name); } - if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) + return value; + } + + public string GetOption(string name) + { + if (StreamOptions.TryGetValue(name, out var value)) { - return result; + return value; } return null; } - /// - /// Predicts the audio sample rate that will be in the output stream. - /// - public int? TargetPacketLength + public string ToUrl(string baseUrl, string accessToken) { - get + if (PlayMethod == PlayMethod.DirectPlay) { - var stream = TargetVideoStream; - return !IsDirectStream - ? null - : stream == null ? null : stream.PacketLength; + return MediaSource.Path; } - } - /// - /// Predicts the audio sample rate that will be in the output stream. - /// - public string TargetVideoProfile - { - get + if (string.IsNullOrEmpty(baseUrl)) { - if (IsDirectStream) + throw new ArgumentNullException(nameof(baseUrl)); + } + + var list = new List(); + foreach (NameValuePair pair in BuildParams(this, accessToken)) + { + if (string.IsNullOrEmpty(pair.Value)) { - return TargetVideoStream == null ? null : TargetVideoStream.Profile; + continue; } - var targetVideoCodecs = TargetVideoCodec; - var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0]; - if (!string.IsNullOrEmpty(videoCodec)) + // Try to keep the url clean by omitting defaults + if (string.Equals(pair.Name, "StartTimeTicks", StringComparison.OrdinalIgnoreCase) && + string.Equals(pair.Value, "0", StringComparison.OrdinalIgnoreCase)) { - return GetOption(videoCodec, "profile"); + continue; } - return TargetVideoStream == null ? null : TargetVideoStream.Profile; - } - } + if (string.Equals(pair.Name, "SubtitleStreamIndex", StringComparison.OrdinalIgnoreCase) && + string.Equals(pair.Value, "-1", StringComparison.OrdinalIgnoreCase)) + { + continue; + } - /// - /// Gets the target video codec tag. - /// - /// The target video codec tag. - public string TargetVideoCodecTag - { - get - { - var stream = TargetVideoStream; - return !IsDirectStream - ? null - : stream == null ? null : stream.CodecTag; + // Be careful, IsDirectStream==true by default (Static != false or not in query). + // See initialization of StreamingRequestDto in AudioController.GetAudioStream() method : Static = @static ?? true. + if (string.Equals(pair.Name, "Static", StringComparison.OrdinalIgnoreCase) && + string.Equals(pair.Value, "true", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + var encodedValue = pair.Value.Replace(" ", "%20"); + + list.Add(string.Format(CultureInfo.InvariantCulture, "{0}={1}", pair.Name, encodedValue)); } + + string queryString = string.Join("&", list.ToArray()); + + return GetUrl(baseUrl, queryString); } - /// - /// Predicts the audio bitrate that will be in the output stream. - /// - public int? TargetAudioBitrate + private string GetUrl(string baseUrl, string queryString) { - get + if (string.IsNullOrEmpty(baseUrl)) { - var stream = TargetAudioStream; - return AudioBitrate.HasValue && !IsDirectStream - ? AudioBitrate - : stream == null ? null : stream.BitRate; + throw new ArgumentNullException(nameof(baseUrl)); } - } - /// - /// Predicts the audio channels that will be in the output stream. - /// - public int? TargetAudioChannels - { - get + string extension = string.IsNullOrEmpty(Container) ? string.Empty : "." + Container; + + baseUrl = baseUrl.TrimEnd('/'); + + if (MediaType == DlnaProfileType.Audio) { - if (IsDirectStream) + if (string.Equals(SubProtocol, "hls", StringComparison.OrdinalIgnoreCase)) { - return TargetAudioStream == null ? (int?)null : TargetAudioStream.Channels; + return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString); } - var targetAudioCodecs = TargetAudioCodec; - var codec = targetAudioCodecs.Length == 0 ? null : targetAudioCodecs[0]; - if (!string.IsNullOrEmpty(codec)) - { - return GetTargetRefFrames(codec); - } + return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString); + } - return TargetAudioStream == null ? (int?)null : TargetAudioStream.Channels; + if (string.Equals(SubProtocol, "hls", StringComparison.OrdinalIgnoreCase)) + { + return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString); } + + return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString); } - public int? GetTargetAudioChannels(string codec) + private static List BuildParams(StreamInfo item, string accessToken) { - var defaultValue = GlobalMaxAudioChannels ?? TranscodingMaxAudioChannels; + var list = new List(); - var value = GetOption(codec, "audiochannels"); - if (string.IsNullOrEmpty(value)) + string audioCodecs = item.AudioCodecs.Length == 0 ? + string.Empty : + string.Join(",", item.AudioCodecs); + + string videoCodecs = item.VideoCodecs.Length == 0 ? + string.Empty : + string.Join(",", item.VideoCodecs); + + list.Add(new NameValuePair("DeviceProfileId", item.DeviceProfileId ?? string.Empty)); + list.Add(new NameValuePair("DeviceId", item.DeviceId ?? string.Empty)); + list.Add(new NameValuePair("MediaSourceId", item.MediaSourceId ?? string.Empty)); + list.Add(new NameValuePair("Static", item.IsDirectStream.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); + list.Add(new NameValuePair("VideoCodec", videoCodecs)); + list.Add(new NameValuePair("AudioCodec", audioCodecs)); + list.Add(new NameValuePair("AudioStreamIndex", item.AudioStreamIndex.HasValue ? item.AudioStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + list.Add(new NameValuePair("SubtitleStreamIndex", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + list.Add(new NameValuePair("VideoBitrate", item.VideoBitrate.HasValue ? item.VideoBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + list.Add(new NameValuePair("AudioBitrate", item.AudioBitrate.HasValue ? item.AudioBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + list.Add(new NameValuePair("AudioSampleRate", item.AudioSampleRate.HasValue ? item.AudioSampleRate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + + list.Add(new NameValuePair("MaxFramerate", item.MaxFramerate.HasValue ? item.MaxFramerate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + list.Add(new NameValuePair("MaxWidth", item.MaxWidth.HasValue ? item.MaxWidth.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + list.Add(new NameValuePair("MaxHeight", item.MaxHeight.HasValue ? item.MaxHeight.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + + long startPositionTicks = item.StartPositionTicks; + + var isHls = string.Equals(item.SubProtocol, "hls", StringComparison.OrdinalIgnoreCase); + + if (isHls) { - return defaultValue; + list.Add(new NameValuePair("StartTimeTicks", string.Empty)); } - - if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) + else { - return Math.Min(result, defaultValue ?? result); + list.Add(new NameValuePair("StartTimeTicks", startPositionTicks.ToString(CultureInfo.InvariantCulture))); } - return defaultValue; - } + list.Add(new NameValuePair("PlaySessionId", item.PlaySessionId ?? string.Empty)); + list.Add(new NameValuePair("api_key", accessToken ?? string.Empty)); - /// - /// Predicts the audio codec that will be in the output stream. - /// - public string[] TargetAudioCodec - { - get - { - var stream = TargetAudioStream; + string liveStreamId = item.MediaSource?.LiveStreamId; + list.Add(new NameValuePair("LiveStreamId", liveStreamId ?? string.Empty)); - string inputCodec = stream?.Codec; + list.Add(new NameValuePair("SubtitleMethod", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleDeliveryMethod.ToString() : string.Empty)); - if (IsDirectStream) + if (!item.IsDirectStream) + { + if (item.RequireNonAnamorphic) { - return string.IsNullOrEmpty(inputCodec) ? Array.Empty() : new[] { inputCodec }; + list.Add(new NameValuePair("RequireNonAnamorphic", item.RequireNonAnamorphic.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); } - foreach (string codec in AudioCodecs) + list.Add(new NameValuePair("TranscodingMaxAudioChannels", item.TranscodingMaxAudioChannels.HasValue ? item.TranscodingMaxAudioChannels.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + + if (item.EnableSubtitlesInManifest) { - if (string.Equals(codec, inputCodec, StringComparison.OrdinalIgnoreCase)) - { - return string.IsNullOrEmpty(codec) ? Array.Empty() : new[] { codec }; - } + list.Add(new NameValuePair("EnableSubtitlesInManifest", item.EnableSubtitlesInManifest.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); } - return AudioCodecs; - } - } - - public string[] TargetVideoCodec - { - get - { - var stream = TargetVideoStream; + if (item.EnableMpegtsM2TsMode) + { + list.Add(new NameValuePair("EnableMpegtsM2TsMode", item.EnableMpegtsM2TsMode.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); + } - string inputCodec = stream?.Codec; + if (item.EstimateContentLength) + { + list.Add(new NameValuePair("EstimateContentLength", item.EstimateContentLength.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); + } - if (IsDirectStream) + if (item.TranscodeSeekInfo != TranscodeSeekInfo.Auto) { - return string.IsNullOrEmpty(inputCodec) ? Array.Empty() : new[] { inputCodec }; + list.Add(new NameValuePair("TranscodeSeekInfo", item.TranscodeSeekInfo.ToString().ToLowerInvariant())); } - foreach (string codec in VideoCodecs) + if (item.CopyTimestamps) { - if (string.Equals(codec, inputCodec, StringComparison.OrdinalIgnoreCase)) - { - return string.IsNullOrEmpty(codec) ? Array.Empty() : new[] { codec }; - } + list.Add(new NameValuePair("CopyTimestamps", item.CopyTimestamps.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); } - return VideoCodecs; + list.Add(new NameValuePair("RequireAvc", item.RequireAvc.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); } - } - /// - /// Predicts the audio channels that will be in the output stream. - /// - public long? TargetSize - { - get + list.Add(new NameValuePair("Tag", item.MediaSource.ETag ?? string.Empty)); + + string subtitleCodecs = item.SubtitleCodecs.Length == 0 ? + string.Empty : + string.Join(",", item.SubtitleCodecs); + + list.Add(new NameValuePair("SubtitleCodec", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed ? subtitleCodecs : string.Empty)); + + if (isHls) { - if (IsDirectStream) + list.Add(new NameValuePair("SegmentContainer", item.Container ?? string.Empty)); + + if (item.SegmentLength.HasValue) { - return MediaSource.Size; + list.Add(new NameValuePair("SegmentLength", item.SegmentLength.Value.ToString(CultureInfo.InvariantCulture))); } - if (RunTimeTicks.HasValue) + if (item.MinSegments.HasValue) { - int? totalBitrate = TargetTotalBitrate; - - double totalSeconds = RunTimeTicks.Value; - // Convert to ms - totalSeconds /= 10000; - // Convert to seconds - totalSeconds /= 1000; - - return totalBitrate.HasValue ? - Convert.ToInt64(totalBitrate.Value * totalSeconds) : - (long?)null; + list.Add(new NameValuePair("MinSegments", item.MinSegments.Value.ToString(CultureInfo.InvariantCulture))); } - return null; + list.Add(new NameValuePair("BreakOnNonKeyFrames", item.BreakOnNonKeyFrames.ToString(CultureInfo.InvariantCulture))); } - } - public int? TargetVideoBitrate - { - get + foreach (var pair in item.StreamOptions) { - var stream = TargetVideoStream; + if (string.IsNullOrEmpty(pair.Value)) + { + continue; + } - return VideoBitrate.HasValue && !IsDirectStream - ? VideoBitrate - : stream == null ? null : stream.BitRate; + // strip spaces to avoid having to encode h264 profile names + list.Add(new NameValuePair(pair.Key, pair.Value.Replace(" ", string.Empty))); } - } - public TransportStreamTimestamp TargetTimestamp - { - get + if (!item.IsDirectStream) { - var defaultValue = string.Equals(Container, "m2ts", StringComparison.OrdinalIgnoreCase) - ? TransportStreamTimestamp.Valid - : TransportStreamTimestamp.None; - - return !IsDirectStream - ? defaultValue - : MediaSource == null ? defaultValue : MediaSource.Timestamp ?? TransportStreamTimestamp.None; + list.Add(new NameValuePair("TranscodeReasons", string.Join(',', item.TranscodeReasons.Distinct()))); } + + return list; } - public int? TargetTotalBitrate => (TargetAudioBitrate ?? 0) + (TargetVideoBitrate ?? 0); + public List GetExternalSubtitles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, string baseUrl, string accessToken) + { + return GetExternalSubtitles(transcoderSupport, includeSelectedTrackOnly, false, baseUrl, accessToken); + } - public bool? IsTargetAnamorphic + public List GetExternalSubtitles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, bool enableAllProfiles, string baseUrl, string accessToken) { - get + var list = GetSubtitleProfiles(transcoderSupport, includeSelectedTrackOnly, enableAllProfiles, baseUrl, accessToken); + var newList = new List(); + + // First add the selected track + foreach (SubtitleStreamInfo stream in list) { - if (IsDirectStream) + if (stream.DeliveryMethod == SubtitleDeliveryMethod.External) { - return TargetVideoStream == null ? null : TargetVideoStream.IsAnamorphic; + newList.Add(stream); } - - return false; } + + return newList; } - public bool? IsTargetInterlaced + public List GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, string baseUrl, string accessToken) { - get + return GetSubtitleProfiles(transcoderSupport, includeSelectedTrackOnly, false, baseUrl, accessToken); + } + + public List GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, bool enableAllProfiles, string baseUrl, string accessToken) + { + var list = new List(); + + // HLS will preserve timestamps so we can just grab the full subtitle stream + long startPositionTicks = string.Equals(SubProtocol, "hls", StringComparison.OrdinalIgnoreCase) + ? 0 + : (PlayMethod == PlayMethod.Transcode && !CopyTimestamps ? StartPositionTicks : 0); + + // First add the selected track + if (SubtitleStreamIndex.HasValue) { - if (IsDirectStream) + foreach (var stream in MediaSource.MediaStreams) { - return TargetVideoStream == null ? (bool?)null : TargetVideoStream.IsInterlaced; + if (stream.Type == MediaStreamType.Subtitle && stream.Index == SubtitleStreamIndex.Value) + { + AddSubtitleProfiles(list, stream, transcoderSupport, enableAllProfiles, baseUrl, accessToken, startPositionTicks); + } } + } - var targetVideoCodecs = TargetVideoCodec; - var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0]; - if (!string.IsNullOrEmpty(videoCodec)) + if (!includeSelectedTrackOnly) + { + foreach (var stream in MediaSource.MediaStreams) { - if (string.Equals(GetOption(videoCodec, "deinterlace"), "true", StringComparison.OrdinalIgnoreCase)) + if (stream.Type == MediaStreamType.Subtitle && (!SubtitleStreamIndex.HasValue || stream.Index != SubtitleStreamIndex.Value)) { - return false; + AddSubtitleProfiles(list, stream, transcoderSupport, enableAllProfiles, baseUrl, accessToken, startPositionTicks); } } - - return TargetVideoStream == null ? (bool?)null : TargetVideoStream.IsInterlaced; } + + return list; } - public bool? IsTargetAVC + private void AddSubtitleProfiles(List list, MediaStream stream, ITranscoderSupport transcoderSupport, bool enableAllProfiles, string baseUrl, string accessToken, long startPositionTicks) { - get + if (enableAllProfiles) { - if (IsDirectStream) + foreach (var profile in DeviceProfile.SubtitleProfiles) { - return TargetVideoStream == null ? null : TargetVideoStream.IsAVC; + var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, new[] { profile }, transcoderSupport); + + list.Add(info); } + } + else + { + var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, DeviceProfile.SubtitleProfiles, transcoderSupport); - return true; + list.Add(info); } } - public int? TargetWidth + private SubtitleStreamInfo GetSubtitleStreamInfo(MediaStream stream, string baseUrl, string accessToken, long startPositionTicks, SubtitleProfile[] subtitleProfiles, ITranscoderSupport transcoderSupport) { - get + var subtitleProfile = StreamBuilder.GetSubtitleProfile(MediaSource, stream, subtitleProfiles, PlayMethod, transcoderSupport, Container, SubProtocol); + var info = new SubtitleStreamInfo { - var videoStream = TargetVideoStream; + IsForced = stream.IsForced, + Language = stream.Language, + Name = stream.Language ?? "Unknown", + Format = subtitleProfile.Format, + Index = stream.Index, + DeliveryMethod = subtitleProfile.Method, + DisplayTitle = stream.DisplayTitle + }; - if (videoStream != null && videoStream.Width.HasValue && videoStream.Height.HasValue) + if (info.DeliveryMethod == SubtitleDeliveryMethod.External) + { + if (MediaSource.Protocol == MediaProtocol.File || !string.Equals(stream.Codec, subtitleProfile.Format, StringComparison.OrdinalIgnoreCase) || !stream.IsExternal) { - ImageDimensions size = new ImageDimensions(videoStream.Width.Value, videoStream.Height.Value); + info.Url = string.Format( + CultureInfo.InvariantCulture, + "{0}/Videos/{1}/{2}/Subtitles/{3}/{4}/Stream.{5}", + baseUrl, + ItemId, + MediaSourceId, + stream.Index.ToString(CultureInfo.InvariantCulture), + startPositionTicks.ToString(CultureInfo.InvariantCulture), + subtitleProfile.Format); - size = DrawingUtils.Resize(size, 0, 0, MaxWidth ?? 0, MaxHeight ?? 0); + if (!string.IsNullOrEmpty(accessToken)) + { + info.Url += "?api_key=" + accessToken; + } - return size.Width; + info.IsExternalUrl = false; + } + else + { + info.Url = stream.Path; + info.IsExternalUrl = true; } + } - return MaxWidth; + return info; + } + + public int? GetTargetVideoBitDepth(string codec) + { + var value = GetOption(codec, "videobitdepth"); + if (string.IsNullOrEmpty(value)) + { + return null; + } + + if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) + { + return result; } + + return null; } - public int? TargetHeight + public int? GetTargetAudioBitDepth(string codec) { - get + var value = GetOption(codec, "audiobitdepth"); + if (string.IsNullOrEmpty(value)) { - var videoStream = TargetVideoStream; + return null; + } - if (videoStream != null && videoStream.Width.HasValue && videoStream.Height.HasValue) - { - ImageDimensions size = new ImageDimensions(videoStream.Width.Value, videoStream.Height.Value); + if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) + { + return result; + } - size = DrawingUtils.Resize(size, 0, 0, MaxWidth ?? 0, MaxHeight ?? 0); + return null; + } - return size.Height; - } + public double? GetTargetVideoLevel(string codec) + { + var value = GetOption(codec, "level"); + if (string.IsNullOrEmpty(value)) + { + return null; + } - return MaxHeight; + if (double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) + { + return result; } + + return null; } - public int? TargetVideoStreamCount + public int? GetTargetRefFrames(string codec) { - get + var value = GetOption(codec, "maxrefframes"); + if (string.IsNullOrEmpty(value)) { - if (IsDirectStream) - { - return GetMediaStreamCount(MediaStreamType.Video, int.MaxValue); - } + return null; + } - return GetMediaStreamCount(MediaStreamType.Video, 1); + if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) + { + return result; } + + return null; } - public int? TargetAudioStreamCount + public int? GetTargetAudioChannels(string codec) { - get + var defaultValue = GlobalMaxAudioChannels ?? TranscodingMaxAudioChannels; + + var value = GetOption(codec, "audiochannels"); + if (string.IsNullOrEmpty(value)) { - if (IsDirectStream) - { - return GetMediaStreamCount(MediaStreamType.Audio, int.MaxValue); - } + return defaultValue; + } - return GetMediaStreamCount(MediaStreamType.Audio, 1); + if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) + { + return Math.Min(result, defaultValue ?? result); } + + return defaultValue; } private int? GetMediaStreamCount(MediaStreamType type, int limit) diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs index 2f9f9d3cdb..a784025e3d 100644 --- a/MediaBrowser.Model/Dto/BaseItemDto.cs +++ b/MediaBrowser.Model/Dto/BaseItemDto.cs @@ -294,13 +294,13 @@ namespace MediaBrowser.Model.Dto public NameGuidPair[] GenreItems { get; set; } /// - /// If the item does not have a logo, this will hold the Id of the Parent that has one. + /// Gets or sets wether the item has a logo, this will hold the Id of the Parent that has one. /// /// The parent logo item id. public string ParentLogoItemId { get; set; } /// - /// If the item does not have any backdrops, this will hold the Id of the Parent that has one. + /// Gets or sets wether the item has any backdrops, this will hold the Id of the Parent that has one. /// /// The parent backdrop item id. public string ParentBackdropItemId { get; set; } @@ -318,7 +318,7 @@ namespace MediaBrowser.Model.Dto public int? LocalTrailerCount { get; set; } /// - /// User data for this item based on the user it's being requested for. + /// Gets or sets the user data for this item based on the user it's being requested for. /// /// The user data. public UserItemDataDto UserData { get; set; } @@ -506,7 +506,7 @@ namespace MediaBrowser.Model.Dto public string ParentLogoImageTag { get; set; } /// - /// If the item does not have a art, this will hold the Id of the Parent that has one. + /// Gets or sets wether the item has fan art, this will hold the Id of the Parent that has one. /// /// The parent art item id. public string ParentArtItemId { get; set; } @@ -695,7 +695,7 @@ namespace MediaBrowser.Model.Dto public string ChannelPrimaryImageTag { get; set; } /// - /// The start date of the recording, in UTC. + /// Gets or sets the start date of the recording, in UTC. /// public DateTime? StartDate { get; set; } diff --git a/MediaBrowser.Model/Dto/MediaSourceInfo.cs b/MediaBrowser.Model/Dto/MediaSourceInfo.cs index be682be23c..ec3b37efa3 100644 --- a/MediaBrowser.Model/Dto/MediaSourceInfo.cs +++ b/MediaBrowser.Model/Dto/MediaSourceInfo.cs @@ -12,6 +12,18 @@ namespace MediaBrowser.Model.Dto { public class MediaSourceInfo { + public MediaSourceInfo() + { + Formats = Array.Empty(); + MediaStreams = new List(); + MediaAttachments = Array.Empty(); + RequiredHttpHeaders = new Dictionary(); + SupportsTranscoding = true; + SupportsDirectStream = true; + SupportsDirectPlay = true; + SupportsProbing = true; + } + public MediaProtocol Protocol { get; set; } public string Id { get; set; } @@ -31,6 +43,7 @@ namespace MediaBrowser.Model.Dto public string Name { get; set; } /// + /// Gets or sets a value indicating whether the media is remote. /// Differentiate internet url vs local network. /// public bool IsRemote { get; set; } @@ -95,16 +108,28 @@ namespace MediaBrowser.Model.Dto public int? AnalyzeDurationMs { get; set; } - public MediaSourceInfo() + [JsonIgnore] + public TranscodeReason[] TranscodeReasons { get; set; } + + public int? DefaultAudioStreamIndex { get; set; } + + public int? DefaultSubtitleStreamIndex { get; set; } + + [JsonIgnore] + public MediaStream VideoStream { - Formats = Array.Empty(); - MediaStreams = new List(); - MediaAttachments = Array.Empty(); - RequiredHttpHeaders = new Dictionary(); - SupportsTranscoding = true; - SupportsDirectStream = true; - SupportsDirectPlay = true; - SupportsProbing = true; + get + { + foreach (var i in MediaStreams) + { + if (i.Type == MediaStreamType.Video) + { + return i; + } + } + + return null; + } } public void InferTotalBitrate(bool force = false) @@ -134,13 +159,6 @@ namespace MediaBrowser.Model.Dto } } - [JsonIgnore] - public TranscodeReason[] TranscodeReasons { get; set; } - - public int? DefaultAudioStreamIndex { get; set; } - - public int? DefaultSubtitleStreamIndex { get; set; } - public MediaStream GetDefaultAudioStream(int? defaultIndex) { if (defaultIndex.HasValue) @@ -175,23 +193,6 @@ namespace MediaBrowser.Model.Dto return null; } - [JsonIgnore] - public MediaStream VideoStream - { - get - { - foreach (var i in MediaStreams) - { - if (i.Type == MediaStreamType.Video) - { - return i; - } - } - - return null; - } - } - public MediaStream GetMediaStream(MediaStreamType type, int index) { foreach (var i in MediaStreams) diff --git a/MediaBrowser.Model/Dto/MetadataEditorInfo.cs b/MediaBrowser.Model/Dto/MetadataEditorInfo.cs index e4f38d6af3..e0e889f7d0 100644 --- a/MediaBrowser.Model/Dto/MetadataEditorInfo.cs +++ b/MediaBrowser.Model/Dto/MetadataEditorInfo.cs @@ -10,6 +10,15 @@ namespace MediaBrowser.Model.Dto { public class MetadataEditorInfo { + public MetadataEditorInfo() + { + ParentalRatingOptions = Array.Empty(); + Countries = Array.Empty(); + Cultures = Array.Empty(); + ExternalIdInfos = Array.Empty(); + ContentTypeOptions = Array.Empty(); + } + public ParentalRating[] ParentalRatingOptions { get; set; } public CountryInfo[] Countries { get; set; } @@ -21,14 +30,5 @@ namespace MediaBrowser.Model.Dto public string ContentType { get; set; } public NameValuePair[] ContentTypeOptions { get; set; } - - public MetadataEditorInfo() - { - ParentalRatingOptions = Array.Empty(); - Countries = Array.Empty(); - Cultures = Array.Empty(); - ExternalIdInfos = Array.Empty(); - ContentTypeOptions = Array.Empty(); - } } } diff --git a/MediaBrowser.Model/Dto/NameGuidPair.cs b/MediaBrowser.Model/Dto/NameGuidPair.cs new file mode 100644 index 0000000000..71166df979 --- /dev/null +++ b/MediaBrowser.Model/Dto/NameGuidPair.cs @@ -0,0 +1,14 @@ +#nullable disable +#pragma warning disable CS1591 + +using System; + +namespace MediaBrowser.Model.Dto +{ + public class NameGuidPair + { + public string Name { get; set; } + + public Guid Id { get; set; } + } +} diff --git a/MediaBrowser.Model/Dto/NameIdPair.cs b/MediaBrowser.Model/Dto/NameIdPair.cs index 45c2fb35db..7f18b45028 100644 --- a/MediaBrowser.Model/Dto/NameIdPair.cs +++ b/MediaBrowser.Model/Dto/NameIdPair.cs @@ -19,11 +19,4 @@ namespace MediaBrowser.Model.Dto /// The identifier. public string Id { get; set; } } - - public class NameGuidPair - { - public string Name { get; set; } - - public Guid Id { get; set; } - } } diff --git a/MediaBrowser.Model/Dto/UserDto.cs b/MediaBrowser.Model/Dto/UserDto.cs index 40222c9dca..256d7b10f1 100644 --- a/MediaBrowser.Model/Dto/UserDto.cs +++ b/MediaBrowser.Model/Dto/UserDto.cs @@ -10,6 +10,15 @@ namespace MediaBrowser.Model.Dto /// public class UserDto : IItemDto, IHasServerId { + /// + /// Initializes a new instance of the class. + /// + public UserDto() + { + Configuration = new UserConfiguration(); + Policy = new UserPolicy(); + } + /// /// Gets or sets the name. /// @@ -94,15 +103,6 @@ namespace MediaBrowser.Model.Dto /// The primary image aspect ratio. public double? PrimaryImageAspectRatio { get; set; } - /// - /// Initializes a new instance of the class. - /// - public UserDto() - { - Configuration = new UserConfiguration(); - Policy = new UserPolicy(); - } - /// public override string ToString() { diff --git a/MediaBrowser.Model/Entities/CollectionType.cs b/MediaBrowser.Model/Entities/CollectionType.cs index 3540387129..60b69d4b01 100644 --- a/MediaBrowser.Model/Entities/CollectionType.cs +++ b/MediaBrowser.Model/Entities/CollectionType.cs @@ -24,36 +24,4 @@ namespace MediaBrowser.Model.Entities public const string Playlists = "playlists"; public const string Folders = "folders"; } - - public static class SpecialFolder - { - public const string TvShowSeries = "TvShowSeries"; - public const string TvGenres = "TvGenres"; - public const string TvGenre = "TvGenre"; - public const string TvLatest = "TvLatest"; - public const string TvNextUp = "TvNextUp"; - public const string TvResume = "TvResume"; - public const string TvFavoriteSeries = "TvFavoriteSeries"; - public const string TvFavoriteEpisodes = "TvFavoriteEpisodes"; - - public const string MovieLatest = "MovieLatest"; - public const string MovieResume = "MovieResume"; - public const string MovieMovies = "MovieMovies"; - public const string MovieCollections = "MovieCollections"; - public const string MovieFavorites = "MovieFavorites"; - public const string MovieGenres = "MovieGenres"; - public const string MovieGenre = "MovieGenre"; - - public const string MusicArtists = "MusicArtists"; - public const string MusicAlbumArtists = "MusicAlbumArtists"; - public const string MusicAlbums = "MusicAlbums"; - public const string MusicGenres = "MusicGenres"; - public const string MusicLatest = "MusicLatest"; - public const string MusicPlaylists = "MusicPlaylists"; - public const string MusicSongs = "MusicSongs"; - public const string MusicFavorites = "MusicFavorites"; - public const string MusicFavoriteArtists = "MusicFavoriteArtists"; - public const string MusicFavoriteAlbums = "MusicFavoriteAlbums"; - public const string MusicFavoriteSongs = "MusicFavoriteSongs"; - } } diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index d85a8cde95..ade9d7e8dd 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -84,7 +84,7 @@ namespace MediaBrowser.Model.Entities public string Title { get; set; } /// - /// Gets or sets the video range. + /// Gets the video range. /// /// The video range. public string VideoRange @@ -108,11 +108,11 @@ namespace MediaBrowser.Model.Entities } } - public string localizedUndefined { get; set; } + public string LocalizedUndefined { get; set; } - public string localizedDefault { get; set; } + public string LocalizedDefault { get; set; } - public string localizedForced { get; set; } + public string LocalizedForced { get; set; } public string DisplayTitle { @@ -154,7 +154,7 @@ namespace MediaBrowser.Model.Entities if (IsDefault) { - attributes.Add(string.IsNullOrEmpty(localizedDefault) ? "Default" : localizedDefault); + attributes.Add(string.IsNullOrEmpty(LocalizedDefault) ? "Default" : LocalizedDefault); } if (!string.IsNullOrEmpty(Title)) @@ -229,17 +229,17 @@ namespace MediaBrowser.Model.Entities } else { - attributes.Add(string.IsNullOrEmpty(localizedUndefined) ? "Und" : localizedUndefined); + attributes.Add(string.IsNullOrEmpty(LocalizedUndefined) ? "Und" : LocalizedUndefined); } if (IsDefault) { - attributes.Add(string.IsNullOrEmpty(localizedDefault) ? "Default" : localizedDefault); + attributes.Add(string.IsNullOrEmpty(LocalizedDefault) ? "Default" : LocalizedDefault); } if (IsForced) { - attributes.Add(string.IsNullOrEmpty(localizedForced) ? "Forced" : localizedForced); + attributes.Add(string.IsNullOrEmpty(LocalizedForced) ? "Forced" : LocalizedForced); } if (!string.IsNullOrEmpty(Title)) @@ -266,67 +266,6 @@ namespace MediaBrowser.Model.Entities } } - private string GetResolutionText() - { - var i = this; - - if (i.Width.HasValue && i.Height.HasValue) - { - var width = i.Width.Value; - var height = i.Height.Value; - - if (width >= 3800 || height >= 2000) - { - return "4K"; - } - - if (width >= 2500) - { - if (i.IsInterlaced) - { - return "1440i"; - } - - return "1440p"; - } - - if (width >= 1900 || height >= 1000) - { - if (i.IsInterlaced) - { - return "1080i"; - } - - return "1080p"; - } - - if (width >= 1260 || height >= 700) - { - if (i.IsInterlaced) - { - return "720i"; - } - - return "720p"; - } - - if (width >= 700 || height >= 440) - { - - if (i.IsInterlaced) - { - return "480i"; - } - - return "480p"; - } - - return "SD"; - } - - return null; - } - public string NalLengthSize { get; set; } /// @@ -487,6 +426,96 @@ namespace MediaBrowser.Model.Entities } } + /// + /// Gets or sets a value indicating whether [supports external stream]. + /// + /// true if [supports external stream]; otherwise, false. + public bool SupportsExternalStream { get; set; } + + /// + /// Gets or sets the filename. + /// + /// The filename. + public string Path { get; set; } + + /// + /// Gets or sets the pixel format. + /// + /// The pixel format. + public string PixelFormat { get; set; } + + /// + /// Gets or sets the level. + /// + /// The level. + public double? Level { get; set; } + + /// + /// Gets or sets whether this instance is anamorphic. + /// + /// true if this instance is anamorphic; otherwise, false. + public bool? IsAnamorphic { get; set; } + + private string GetResolutionText() + { + var i = this; + + if (i.Width.HasValue && i.Height.HasValue) + { + var width = i.Width.Value; + var height = i.Height.Value; + + if (width >= 3800 || height >= 2000) + { + return "4K"; + } + + if (width >= 2500) + { + if (i.IsInterlaced) + { + return "1440i"; + } + + return "1440p"; + } + + if (width >= 1900 || height >= 1000) + { + if (i.IsInterlaced) + { + return "1080i"; + } + + return "1080p"; + } + + if (width >= 1260 || height >= 700) + { + if (i.IsInterlaced) + { + return "720i"; + } + + return "720p"; + } + + if (width >= 700 || height >= 440) + { + if (i.IsInterlaced) + { + return "480i"; + } + + return "480p"; + } + + return "SD"; + } + + return null; + } + public static bool IsTextFormat(string format) { string codec = format ?? string.Empty; @@ -533,35 +562,5 @@ namespace MediaBrowser.Model.Entities return true; } - - /// - /// Gets or sets a value indicating whether [supports external stream]. - /// - /// true if [supports external stream]; otherwise, false. - public bool SupportsExternalStream { get; set; } - - /// - /// Gets or sets the filename. - /// - /// The filename. - public string Path { get; set; } - - /// - /// Gets or sets the pixel format. - /// - /// The pixel format. - public string PixelFormat { get; set; } - - /// - /// Gets or sets the level. - /// - /// The level. - public double? Level { get; set; } - - /// - /// Gets a value indicating whether this instance is anamorphic. - /// - /// true if this instance is anamorphic; otherwise, false. - public bool? IsAnamorphic { get; set; } } } diff --git a/MediaBrowser.Model/Entities/PackageReviewInfo.cs b/MediaBrowser.Model/Entities/PackageReviewInfo.cs deleted file mode 100644 index 5b22b34ace..0000000000 --- a/MediaBrowser.Model/Entities/PackageReviewInfo.cs +++ /dev/null @@ -1,40 +0,0 @@ -#nullable disable -#pragma warning disable CS1591 - -using System; - -namespace MediaBrowser.Model.Entities -{ - public class PackageReviewInfo - { - /// - /// Gets or sets the package id (database key) for this review. - /// - public int id { get; set; } - - /// - /// Gets or sets the rating value. - /// - public int rating { get; set; } - - /// - /// Gets or sets whether or not this review recommends this item. - /// - public bool recommend { get; set; } - - /// - /// Gets or sets a short description of the review. - /// - public string title { get; set; } - - /// - /// Gets or sets the full review. - /// - public string review { get; set; } - - /// - /// Gets or sets the time of review. - /// - public DateTime timestamp { get; set; } - } -} diff --git a/MediaBrowser.Model/Entities/SpecialFolder.cs b/MediaBrowser.Model/Entities/SpecialFolder.cs new file mode 100644 index 0000000000..2250c5dffb --- /dev/null +++ b/MediaBrowser.Model/Entities/SpecialFolder.cs @@ -0,0 +1,36 @@ +#pragma warning disable CS1591 + +namespace MediaBrowser.Model.Entities +{ + public static class SpecialFolder + { + public const string TvShowSeries = "TvShowSeries"; + public const string TvGenres = "TvGenres"; + public const string TvGenre = "TvGenre"; + public const string TvLatest = "TvLatest"; + public const string TvNextUp = "TvNextUp"; + public const string TvResume = "TvResume"; + public const string TvFavoriteSeries = "TvFavoriteSeries"; + public const string TvFavoriteEpisodes = "TvFavoriteEpisodes"; + + public const string MovieLatest = "MovieLatest"; + public const string MovieResume = "MovieResume"; + public const string MovieMovies = "MovieMovies"; + public const string MovieCollections = "MovieCollections"; + public const string MovieFavorites = "MovieFavorites"; + public const string MovieGenres = "MovieGenres"; + public const string MovieGenre = "MovieGenre"; + + public const string MusicArtists = "MusicArtists"; + public const string MusicAlbumArtists = "MusicAlbumArtists"; + public const string MusicAlbums = "MusicAlbums"; + public const string MusicGenres = "MusicGenres"; + public const string MusicLatest = "MusicLatest"; + public const string MusicPlaylists = "MusicPlaylists"; + public const string MusicSongs = "MusicSongs"; + public const string MusicFavorites = "MusicFavorites"; + public const string MusicFavoriteArtists = "MusicFavoriteArtists"; + public const string MusicFavoriteAlbums = "MusicFavoriteAlbums"; + public const string MusicFavoriteSongs = "MusicFavoriteSongs"; + } +} diff --git a/MediaBrowser.Model/Entities/VirtualFolderInfo.cs b/MediaBrowser.Model/Entities/VirtualFolderInfo.cs index f2bc6f25e0..1b0e592401 100644 --- a/MediaBrowser.Model/Entities/VirtualFolderInfo.cs +++ b/MediaBrowser.Model/Entities/VirtualFolderInfo.cs @@ -11,6 +11,14 @@ namespace MediaBrowser.Model.Entities /// public class VirtualFolderInfo { + /// + /// Initializes a new instance of the class. + /// + public VirtualFolderInfo() + { + Locations = Array.Empty(); + } + /// /// Gets or sets the name. /// @@ -31,14 +39,6 @@ namespace MediaBrowser.Model.Entities public LibraryOptions LibraryOptions { get; set; } - /// - /// Initializes a new instance of the class. - /// - public VirtualFolderInfo() - { - Locations = Array.Empty(); - } - /// /// Gets or sets the item identifier. /// diff --git a/MediaBrowser.Model/Globalization/CultureDto.cs b/MediaBrowser.Model/Globalization/CultureDto.cs index 6af4a872ce..5246f87d92 100644 --- a/MediaBrowser.Model/Globalization/CultureDto.cs +++ b/MediaBrowser.Model/Globalization/CultureDto.cs @@ -10,6 +10,11 @@ namespace MediaBrowser.Model.Globalization /// public class CultureDto { + public CultureDto() + { + ThreeLetterISOLanguageNames = Array.Empty(); + } + /// /// Gets or sets the name. /// @@ -29,7 +34,7 @@ namespace MediaBrowser.Model.Globalization public string TwoLetterISOLanguageName { get; set; } /// - /// Gets or sets the name of the three letter ISO language. + /// Gets the name of the three letter ISO language. /// /// The name of the three letter ISO language. public string ThreeLetterISOLanguageName @@ -47,10 +52,5 @@ namespace MediaBrowser.Model.Globalization } public string[] ThreeLetterISOLanguageNames { get; set; } - - public CultureDto() - { - ThreeLetterISOLanguageNames = Array.Empty(); - } } } diff --git a/MediaBrowser.Model/IO/IFileSystem.cs b/MediaBrowser.Model/IO/IFileSystem.cs index dc65497876..ef08ecec66 100644 --- a/MediaBrowser.Model/IO/IFileSystem.cs +++ b/MediaBrowser.Model/IO/IFileSystem.cs @@ -155,13 +155,16 @@ namespace MediaBrowser.Model.IO /// Gets the directories. /// /// The path. - /// if set to true [recursive]. - /// IEnumerable<DirectoryInfo>. + /// If set to true also searches in subdirectiories. + /// All found directories. IEnumerable GetDirectories(string path, bool recursive = false); /// /// Gets the files. /// + /// The path in which to search. + /// If set to true also searches in subdirectiories. + /// All found files. IEnumerable GetFiles(string path, bool recursive = false); IEnumerable GetFiles(string path, IReadOnlyList extensions, bool enableCaseSensitiveExtensions, bool recursive); diff --git a/MediaBrowser.Model/LiveTv/BaseTimerInfoDto.cs b/MediaBrowser.Model/LiveTv/BaseTimerInfoDto.cs index 07e76d9600..c6de4c1ab8 100644 --- a/MediaBrowser.Model/LiveTv/BaseTimerInfoDto.cs +++ b/MediaBrowser.Model/LiveTv/BaseTimerInfoDto.cs @@ -9,7 +9,7 @@ namespace MediaBrowser.Model.LiveTv public class BaseTimerInfoDto : IHasServerId { /// - /// Id of the recording. + /// Gets or sets the Id of the recording. /// public string Id { get; set; } @@ -28,7 +28,7 @@ namespace MediaBrowser.Model.LiveTv public string ExternalId { get; set; } /// - /// ChannelId of the recording. + /// Gets or sets the channel id of the recording. /// public Guid ChannelId { get; set; } @@ -39,7 +39,7 @@ namespace MediaBrowser.Model.LiveTv public string ExternalChannelId { get; set; } /// - /// ChannelName of the recording. + /// Gets or sets the channel name of the recording. /// public string ChannelName { get; set; } @@ -58,22 +58,22 @@ namespace MediaBrowser.Model.LiveTv public string ExternalProgramId { get; set; } /// - /// Name of the recording. + /// Gets or sets the name of the recording. /// public string Name { get; set; } /// - /// Description of the recording. + /// Gets or sets the description of the recording. /// public string Overview { get; set; } /// - /// The start date of the recording, in UTC. + /// Gets or sets the start date of the recording, in UTC. /// public DateTime StartDate { get; set; } /// - /// The end date of the recording, in UTC. + /// Gets or sets the end date of the recording, in UTC. /// public DateTime EndDate { get; set; } @@ -108,7 +108,7 @@ namespace MediaBrowser.Model.LiveTv public bool IsPrePaddingRequired { get; set; } /// - /// If the item does not have any backdrops, this will hold the Id of the Parent that has one. + /// Gets or sets the Id of the Parent that has a backdrop if the item does not have one. /// /// The parent backdrop item id. public string ParentBackdropItemId { get; set; } diff --git a/MediaBrowser.Model/LiveTv/ListingsProviderInfo.cs b/MediaBrowser.Model/LiveTv/ListingsProviderInfo.cs new file mode 100644 index 0000000000..082daeb51b --- /dev/null +++ b/MediaBrowser.Model/LiveTv/ListingsProviderInfo.cs @@ -0,0 +1,58 @@ +#nullable disable +#pragma warning disable CS1591 + +using System; +using MediaBrowser.Model.Dto; + +namespace MediaBrowser.Model.LiveTv +{ + public class ListingsProviderInfo + { + public ListingsProviderInfo() + { + NewsCategories = new[] { "news", "journalism", "documentary", "current affairs" }; + SportsCategories = new[] { "sports", "basketball", "baseball", "football" }; + KidsCategories = new[] { "kids", "family", "children", "childrens", "disney" }; + MovieCategories = new[] { "movie" }; + EnabledTuners = Array.Empty(); + EnableAllTuners = true; + ChannelMappings = Array.Empty(); + } + + public string Id { get; set; } + + public string Type { get; set; } + + public string Username { get; set; } + + public string Password { get; set; } + + public string ListingsId { get; set; } + + public string ZipCode { get; set; } + + public string Country { get; set; } + + public string Path { get; set; } + + public string[] EnabledTuners { get; set; } + + public bool EnableAllTuners { get; set; } + + public string[] NewsCategories { get; set; } + + public string[] SportsCategories { get; set; } + + public string[] KidsCategories { get; set; } + + public string[] MovieCategories { get; set; } + + public NameValuePair[] ChannelMappings { get; set; } + + public string MoviePrefix { get; set; } + + public string PreferredLanguage { get; set; } + + public string UserAgent { get; set; } + } +} diff --git a/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs b/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs index bcba344cc5..ca8defd8ba 100644 --- a/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs +++ b/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs @@ -11,6 +11,12 @@ namespace MediaBrowser.Model.LiveTv /// public class LiveTvChannelQuery { + public LiveTvChannelQuery() + { + EnableUserData = true; + SortBy = Array.Empty(); + } + /// /// Gets or sets the type of the channel. /// @@ -48,13 +54,13 @@ namespace MediaBrowser.Model.LiveTv public Guid UserId { get; set; } /// - /// Skips over a given number of items within the results. Use for paging. + /// gets or sets the start index. Used for paging. /// /// The start index. public int? StartIndex { get; set; } /// - /// The maximum number of items to return. + /// Gets or sets the maximum number of items to return. /// /// The limit. public int? Limit { get; set; } @@ -68,15 +74,15 @@ namespace MediaBrowser.Model.LiveTv public bool EnableUserData { get; set; } /// - /// Used to specific whether to return news or not. + /// Gets or sets a value whether to return news or not. /// - /// If set to null, all programs will be returned + /// If set to null, all programs will be returned. public bool? IsNews { get; set; } /// - /// Used to specific whether to return movies or not. + /// Gets or sets a value whether to return movies or not. /// - /// If set to null, all programs will be returned + /// If set to null, all programs will be returned. public bool? IsMovie { get; set; } /// @@ -96,15 +102,9 @@ namespace MediaBrowser.Model.LiveTv public string[] SortBy { get; set; } /// - /// The sort order to return results with. + /// Gets or sets the sort order to return results with. /// /// The sort order. public SortOrder? SortOrder { get; set; } - - public LiveTvChannelQuery() - { - EnableUserData = true; - SortBy = Array.Empty(); - } } } diff --git a/MediaBrowser.Model/LiveTv/LiveTvOptions.cs b/MediaBrowser.Model/LiveTv/LiveTvOptions.cs index 789de3198a..4cece941cf 100644 --- a/MediaBrowser.Model/LiveTv/LiveTvOptions.cs +++ b/MediaBrowser.Model/LiveTv/LiveTvOptions.cs @@ -2,12 +2,19 @@ #pragma warning disable CS1591 using System; -using MediaBrowser.Model.Dto; namespace MediaBrowser.Model.LiveTv { public class LiveTvOptions { + public LiveTvOptions() + { + TunerHosts = Array.Empty(); + ListingProviders = Array.Empty(); + MediaLocationsCreated = Array.Empty(); + RecordingPostProcessorArguments = "\"{path}\""; + } + public int? GuideDays { get; set; } public string RecordingPath { get; set; } @@ -33,93 +40,5 @@ namespace MediaBrowser.Model.LiveTv public string RecordingPostProcessor { get; set; } public string RecordingPostProcessorArguments { get; set; } - - public LiveTvOptions() - { - TunerHosts = Array.Empty(); - ListingProviders = Array.Empty(); - MediaLocationsCreated = Array.Empty(); - RecordingPostProcessorArguments = "\"{path}\""; - } - } - - public class TunerHostInfo - { - public string Id { get; set; } - - public string Url { get; set; } - - public string Type { get; set; } - - public string DeviceId { get; set; } - - public string FriendlyName { get; set; } - - public bool ImportFavoritesOnly { get; set; } - - public bool AllowHWTranscoding { get; set; } - - public bool EnableStreamLooping { get; set; } - - public string Source { get; set; } - - public int TunerCount { get; set; } - - public string UserAgent { get; set; } - - public TunerHostInfo() - { - AllowHWTranscoding = true; - } - } - - public class ListingsProviderInfo - { - public string Id { get; set; } - - public string Type { get; set; } - - public string Username { get; set; } - - public string Password { get; set; } - - public string ListingsId { get; set; } - - public string ZipCode { get; set; } - - public string Country { get; set; } - - public string Path { get; set; } - - public string[] EnabledTuners { get; set; } - - public bool EnableAllTuners { get; set; } - - public string[] NewsCategories { get; set; } - - public string[] SportsCategories { get; set; } - - public string[] KidsCategories { get; set; } - - public string[] MovieCategories { get; set; } - - public NameValuePair[] ChannelMappings { get; set; } - - public string MoviePrefix { get; set; } - - public string PreferredLanguage { get; set; } - - public string UserAgent { get; set; } - - public ListingsProviderInfo() - { - NewsCategories = new[] { "news", "journalism", "documentary", "current affairs" }; - SportsCategories = new[] { "sports", "basketball", "baseball", "football" }; - KidsCategories = new[] { "kids", "family", "children", "childrens", "disney" }; - MovieCategories = new[] { "movie" }; - EnabledTuners = Array.Empty(); - EnableAllTuners = true; - ChannelMappings = Array.Empty(); - } } } diff --git a/MediaBrowser.Model/LiveTv/LiveTvServiceInfo.cs b/MediaBrowser.Model/LiveTv/LiveTvServiceInfo.cs index 856f638c5c..ef5c5d2f31 100644 --- a/MediaBrowser.Model/LiveTv/LiveTvServiceInfo.cs +++ b/MediaBrowser.Model/LiveTv/LiveTvServiceInfo.cs @@ -10,6 +10,11 @@ namespace MediaBrowser.Model.LiveTv /// public class LiveTvServiceInfo { + public LiveTvServiceInfo() + { + Tuners = Array.Empty(); + } + /// /// Gets or sets the name. /// @@ -53,10 +58,5 @@ namespace MediaBrowser.Model.LiveTv public bool IsVisible { get; set; } public string[] Tuners { get; set; } - - public LiveTvServiceInfo() - { - Tuners = Array.Empty(); - } } } diff --git a/MediaBrowser.Model/LiveTv/RecordingQuery.cs b/MediaBrowser.Model/LiveTv/RecordingQuery.cs index 69e7db4708..99bb1603c3 100644 --- a/MediaBrowser.Model/LiveTv/RecordingQuery.cs +++ b/MediaBrowser.Model/LiveTv/RecordingQuery.cs @@ -12,6 +12,11 @@ namespace MediaBrowser.Model.LiveTv /// public class RecordingQuery { + public RecordingQuery() + { + EnableTotalRecordCount = true; + } + /// /// Gets or sets the channel identifier. /// @@ -31,13 +36,13 @@ namespace MediaBrowser.Model.LiveTv public string Id { get; set; } /// - /// Skips over a given number of items within the results. Use for paging. + /// Gets or sets the start index. Use for paging. /// /// The start index. public int? StartIndex { get; set; } /// - /// The maximum number of items to return. + /// Gets or sets the maximum number of items to return. /// /// The limit. public int? Limit { get; set; } @@ -61,7 +66,7 @@ namespace MediaBrowser.Model.LiveTv public string SeriesTimerId { get; set; } /// - /// Fields to return within the items, in addition to basic information. + /// Gets or sets the fields to return within the items, in addition to basic information. /// /// The fields. public ItemFields[] Fields { get; set; } @@ -85,10 +90,5 @@ namespace MediaBrowser.Model.LiveTv public ImageType[] EnableImageTypes { get; set; } public bool EnableTotalRecordCount { get; set; } - - public RecordingQuery() - { - EnableTotalRecordCount = true; - } } } diff --git a/MediaBrowser.Model/LiveTv/SeriesTimerInfoDto.cs b/MediaBrowser.Model/LiveTv/SeriesTimerInfoDto.cs index 90422d19c3..b26f5f45fe 100644 --- a/MediaBrowser.Model/LiveTv/SeriesTimerInfoDto.cs +++ b/MediaBrowser.Model/LiveTv/SeriesTimerInfoDto.cs @@ -7,6 +7,14 @@ using MediaBrowser.Model.Entities; namespace MediaBrowser.Model.LiveTv { + public enum KeepUntil + { + UntilDeleted, + UntilSpaceNeeded, + UntilWatched, + UntilDate + } + /// /// Class SeriesTimerInfoDto. /// @@ -83,12 +91,4 @@ namespace MediaBrowser.Model.LiveTv /// The parent primary image tag. public string ParentPrimaryImageTag { get; set; } } - - public enum KeepUntil - { - UntilDeleted, - UntilSpaceNeeded, - UntilWatched, - UntilDate - } } diff --git a/MediaBrowser.Model/LiveTv/TunerHostInfo.cs b/MediaBrowser.Model/LiveTv/TunerHostInfo.cs new file mode 100644 index 0000000000..7d4bbb2d07 --- /dev/null +++ b/MediaBrowser.Model/LiveTv/TunerHostInfo.cs @@ -0,0 +1,38 @@ +#nullable disable +#pragma warning disable CS1591 + +using System; +using MediaBrowser.Model.Dto; + +namespace MediaBrowser.Model.LiveTv +{ + public class TunerHostInfo + { + public TunerHostInfo() + { + AllowHWTranscoding = true; + } + + public string Id { get; set; } + + public string Url { get; set; } + + public string Type { get; set; } + + public string DeviceId { get; set; } + + public string FriendlyName { get; set; } + + public bool ImportFavoritesOnly { get; set; } + + public bool AllowHWTranscoding { get; set; } + + public bool EnableStreamLooping { get; set; } + + public string Source { get; set; } + + public int TunerCount { get; set; } + + public string UserAgent { get; set; } + } +} diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index c534286510..b6d9169139 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -17,7 +17,7 @@ net5.0 false true - true + true enable latest true @@ -44,7 +44,7 @@ - + diff --git a/MediaBrowser.Model/MediaInfo/MediaInfo.cs b/MediaBrowser.Model/MediaInfo/MediaInfo.cs index 472055c22c..a268a4fa66 100644 --- a/MediaBrowser.Model/MediaInfo/MediaInfo.cs +++ b/MediaBrowser.Model/MediaInfo/MediaInfo.cs @@ -10,6 +10,17 @@ namespace MediaBrowser.Model.MediaInfo { public class MediaInfo : MediaSourceInfo, IHasProviderIds { + public MediaInfo() + { + Chapters = Array.Empty(); + Artists = Array.Empty(); + AlbumArtists = Array.Empty(); + Studios = Array.Empty(); + Genres = Array.Empty(); + People = Array.Empty(); + ProviderIds = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + public ChapterInfo[] Chapters { get; set; } /// @@ -69,16 +80,5 @@ namespace MediaBrowser.Model.MediaInfo /// /// The overview. public string Overview { get; set; } - - public MediaInfo() - { - Chapters = Array.Empty(); - Artists = Array.Empty(); - AlbumArtists = Array.Empty(); - Studios = Array.Empty(); - Genres = Array.Empty(); - People = Array.Empty(); - ProviderIds = new Dictionary(StringComparer.OrdinalIgnoreCase); - } } } diff --git a/MediaBrowser.Model/MediaInfo/PlaybackInfoRequest.cs b/MediaBrowser.Model/MediaInfo/PlaybackInfoRequest.cs index 3216856777..ecd9b8834e 100644 --- a/MediaBrowser.Model/MediaInfo/PlaybackInfoRequest.cs +++ b/MediaBrowser.Model/MediaInfo/PlaybackInfoRequest.cs @@ -8,6 +8,17 @@ namespace MediaBrowser.Model.MediaInfo { public class PlaybackInfoRequest { + public PlaybackInfoRequest() + { + EnableDirectPlay = true; + EnableDirectStream = true; + EnableTranscoding = true; + AllowVideoStreamCopy = true; + AllowAudioStreamCopy = true; + IsPlayback = true; + DirectPlayProtocols = new MediaProtocol[] { MediaProtocol.Http }; + } + public Guid Id { get; set; } public Guid UserId { get; set; } @@ -43,16 +54,5 @@ namespace MediaBrowser.Model.MediaInfo public bool AutoOpenLiveStream { get; set; } public MediaProtocol[] DirectPlayProtocols { get; set; } - - public PlaybackInfoRequest() - { - EnableDirectPlay = true; - EnableDirectStream = true; - EnableTranscoding = true; - AllowVideoStreamCopy = true; - AllowAudioStreamCopy = true; - IsPlayback = true; - DirectPlayProtocols = new MediaProtocol[] { MediaProtocol.Http }; - } } } diff --git a/MediaBrowser.Model/MediaInfo/PlaybackInfoResponse.cs b/MediaBrowser.Model/MediaInfo/PlaybackInfoResponse.cs index 2733501822..32971b108f 100644 --- a/MediaBrowser.Model/MediaInfo/PlaybackInfoResponse.cs +++ b/MediaBrowser.Model/MediaInfo/PlaybackInfoResponse.cs @@ -10,6 +10,14 @@ namespace MediaBrowser.Model.MediaInfo /// public class PlaybackInfoResponse { + /// + /// Initializes a new instance of the class. + /// + public PlaybackInfoResponse() + { + MediaSources = Array.Empty(); + } + /// /// Gets or sets the media sources. /// @@ -27,13 +35,5 @@ namespace MediaBrowser.Model.MediaInfo /// /// The error code. public PlaybackErrorCode? ErrorCode { get; set; } - - /// - /// Initializes a new instance of the class. - /// - public PlaybackInfoResponse() - { - MediaSources = Array.Empty(); - } } } diff --git a/MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs b/MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs index 37f5c55da6..d5c3a6aec5 100644 --- a/MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs +++ b/MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs @@ -7,11 +7,11 @@ namespace MediaBrowser.Model.MediaInfo { public class SubtitleTrackInfo { - public IReadOnlyList TrackEvents { get; set; } - public SubtitleTrackInfo() { TrackEvents = Array.Empty(); } + + public IReadOnlyList TrackEvents { get; set; } } } diff --git a/MediaBrowser.Model/Net/ISocket.cs b/MediaBrowser.Model/Net/ISocket.cs index 5b6ed92df1..3de41d565a 100644 --- a/MediaBrowser.Model/Net/ISocket.cs +++ b/MediaBrowser.Model/Net/ISocket.cs @@ -23,6 +23,12 @@ namespace MediaBrowser.Model.Net /// /// Sends a UDP message to a particular end point (uni or multicast). /// + /// An array of type that contains the data to send. + /// The zero-based position in buffer at which to begin sending data. + /// The number of bytes to send. + /// An that represents the remote device. + /// The cancellation token to cancel operation. + /// The task object representing the asynchronous operation. Task SendToAsync(byte[] buffer, int offset, int bytes, IPEndPoint endPoint, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.Model/Net/ISocketFactory.cs b/MediaBrowser.Model/Net/ISocketFactory.cs index 363abefc19..1527ef595c 100644 --- a/MediaBrowser.Model/Net/ISocketFactory.cs +++ b/MediaBrowser.Model/Net/ISocketFactory.cs @@ -14,6 +14,9 @@ namespace MediaBrowser.Model.Net /// /// Creates a new unicast socket using the specified local port number. /// + /// The local IP address to bind to. + /// The local port to bind to. + /// A new unicast socket using the specified local port number. ISocket CreateSsdpUdpSocket(IPAddress localIp, int localPort); /// diff --git a/MediaBrowser.Model/Net/MimeTypes.cs b/MediaBrowser.Model/Net/MimeTypes.cs index 902db1e9e5..96f5ab51ae 100644 --- a/MediaBrowser.Model/Net/MimeTypes.cs +++ b/MediaBrowser.Model/Net/MimeTypes.cs @@ -91,9 +91,9 @@ namespace MediaBrowser.Model.Net { ".webp", "image/webp" }, // Type font - { ".ttf" , "font/ttf" }, - { ".woff" , "font/woff" }, - { ".woff2" , "font/woff2" }, + { ".ttf", "font/ttf" }, + { ".woff", "font/woff" }, + { ".woff2", "font/woff2" }, // Type text { ".ass", "text/x-ssa" }, @@ -168,14 +168,17 @@ namespace MediaBrowser.Model.Net /// /// Gets the type of the MIME. /// - public static string? GetMimeType(string path, bool enableStreamDefault) + /// The filename to find the MIME type of. + /// Whether of not to return a default value if no fitting MIME type is found. + /// The worrect MIME type for the given filename, or `null` if it wasn't found and is false. + public static string? GetMimeType(string filename, bool enableStreamDefault) { - if (path.Length == 0) + if (filename.Length == 0) { - throw new ArgumentException("String can't be empty.", nameof(path)); + throw new ArgumentException("String can't be empty.", nameof(filename)); } - var ext = Path.GetExtension(path); + var ext = Path.GetExtension(filename); if (_mimeTypeLookup.TryGetValue(ext, out string? result)) { @@ -210,9 +213,9 @@ namespace MediaBrowser.Model.Net return enableStreamDefault ? "application/octet-stream" : null; } - public static string? ToExtension(string? mimeType) + public static string? ToExtension(string mimeType) { - if (string.IsNullOrEmpty(mimeType)) + if (mimeType.Length == 0) { throw new ArgumentException("String can't be empty.", nameof(mimeType)); } diff --git a/MediaBrowser.Model/Net/NetworkShare.cs b/MediaBrowser.Model/Net/NetworkShare.cs deleted file mode 100644 index 6344cbe21e..0000000000 --- a/MediaBrowser.Model/Net/NetworkShare.cs +++ /dev/null @@ -1,33 +0,0 @@ -#nullable disable -#pragma warning disable CS1591 - -namespace MediaBrowser.Model.Net -{ - public class NetworkShare - { - /// - /// The name of the computer that this share belongs to. - /// - public string Server { get; set; } - - /// - /// Share name. - /// - public string Name { get; set; } - - /// - /// Local path. - /// - public string Path { get; set; } - - /// - /// Share type. - /// - public NetworkShareType ShareType { get; set; } - - /// - /// Comment. - /// - public string Remark { get; set; } - } -} diff --git a/MediaBrowser.Model/Net/SocketReceiveResult.cs b/MediaBrowser.Model/Net/SocketReceiveResult.cs index 54139fe9c5..1524786ea7 100644 --- a/MediaBrowser.Model/Net/SocketReceiveResult.cs +++ b/MediaBrowser.Model/Net/SocketReceiveResult.cs @@ -20,12 +20,12 @@ namespace MediaBrowser.Model.Net public int ReceivedBytes { get; set; } /// - /// The the data was received from. + /// Gets or sets the the data was received from. /// public IPEndPoint RemoteEndPoint { get; set; } /// - /// The local . + /// Gets or sets the local . /// public IPAddress LocalIPAddress { get; set; } } diff --git a/MediaBrowser.Model/Net/WebSocketMessage.cs b/MediaBrowser.Model/Net/WebSocketMessage.cs index bffbbe612d..b00158cb33 100644 --- a/MediaBrowser.Model/Net/WebSocketMessage.cs +++ b/MediaBrowser.Model/Net/WebSocketMessage.cs @@ -9,7 +9,7 @@ namespace MediaBrowser.Model.Net /// /// Class WebSocketMessage. /// - /// + /// The type of the data. public class WebSocketMessage { /// diff --git a/MediaBrowser.Model/Notifications/NotificationOptions.cs b/MediaBrowser.Model/Notifications/NotificationOptions.cs index 239a3777e1..94bb5d6e35 100644 --- a/MediaBrowser.Model/Notifications/NotificationOptions.cs +++ b/MediaBrowser.Model/Notifications/NotificationOptions.cs @@ -2,18 +2,16 @@ #pragma warning disable CS1591 using System; -using Jellyfin.Data.Enums; -using MediaBrowser.Model.Extensions; using System.Linq; using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; +using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Users; namespace MediaBrowser.Model.Notifications { public class NotificationOptions { - public NotificationOption[] Options { get; set; } - public NotificationOptions() { Options = new[] @@ -71,6 +69,8 @@ namespace MediaBrowser.Model.Notifications }; } + public NotificationOption[] Options { get; set; } + public NotificationOption GetOptions(string type) { foreach (NotificationOption i in Options) @@ -104,7 +104,7 @@ namespace MediaBrowser.Model.Notifications NotificationOption opt = GetOptions(type); return opt != null && opt.Enabled && - !opt.DisabledMonitorUsers.Contains(userId.ToString(""), StringComparer.OrdinalIgnoreCase); + !opt.DisabledMonitorUsers.Contains(userId.ToString(string.Empty), StringComparer.OrdinalIgnoreCase); } public bool IsEnabledToSendToUser(string type, string userId, User user) diff --git a/MediaBrowser.Model/Notifications/NotificationRequest.cs b/MediaBrowser.Model/Notifications/NotificationRequest.cs index febc2bc099..622c50cd88 100644 --- a/MediaBrowser.Model/Notifications/NotificationRequest.cs +++ b/MediaBrowser.Model/Notifications/NotificationRequest.cs @@ -7,6 +7,12 @@ namespace MediaBrowser.Model.Notifications { public class NotificationRequest { + public NotificationRequest() + { + UserIds = Array.Empty(); + Date = DateTime.UtcNow; + } + public string Name { get; set; } public string Description { get; set; } @@ -20,16 +26,10 @@ namespace MediaBrowser.Model.Notifications public DateTime Date { get; set; } /// - /// The corresponding type name used in configuration. Not for display. + /// Gets or sets the corresponding type name used in configuration. Not for display. /// public string NotificationType { get; set; } public SendToUserType? SendToUserMode { get; set; } - - public NotificationRequest() - { - UserIds = Array.Empty(); - Date = DateTime.UtcNow; - } } } diff --git a/MediaBrowser.Model/Providers/ExternalIdInfo.cs b/MediaBrowser.Model/Providers/ExternalIdInfo.cs index afe95e6eee..0ea3e96cae 100644 --- a/MediaBrowser.Model/Providers/ExternalIdInfo.cs +++ b/MediaBrowser.Model/Providers/ExternalIdInfo.cs @@ -6,11 +6,11 @@ namespace MediaBrowser.Model.Providers public class ExternalIdInfo { /// - /// Represents the external id information for serialization to the client. + /// Initializes a new instance of the class. /// /// Name of the external id provider (IE: IMDB, MusicBrainz, etc). /// Key for this id. This key should be unique across all providers. - /// Specific media type for this id + /// Specific media type for this id. /// URL format string. public ExternalIdInfo(string name, string key, ExternalIdMediaType? type, string urlFormatString) { diff --git a/MediaBrowser.Model/Providers/RemoteImageInfo.cs b/MediaBrowser.Model/Providers/RemoteImageInfo.cs index fb25999e0a..48207d2d41 100644 --- a/MediaBrowser.Model/Providers/RemoteImageInfo.cs +++ b/MediaBrowser.Model/Providers/RemoteImageInfo.cs @@ -22,7 +22,7 @@ namespace MediaBrowser.Model.Providers public string Url { get; set; } /// - /// Gets a url used for previewing a smaller version. + /// Gets or sets a url used for previewing a smaller version. /// public string ThumbnailUrl { get; set; } diff --git a/MediaBrowser.Model/Providers/SubtitleOptions.cs b/MediaBrowser.Model/Providers/SubtitleOptions.cs index 5702c460b0..6ea1e14862 100644 --- a/MediaBrowser.Model/Providers/SubtitleOptions.cs +++ b/MediaBrowser.Model/Providers/SubtitleOptions.cs @@ -7,6 +7,14 @@ namespace MediaBrowser.Model.Providers { public class SubtitleOptions { + public SubtitleOptions() + { + DownloadLanguages = Array.Empty(); + + SkipIfAudioTrackMatches = true; + RequirePerfectMatch = true; + } + public bool SkipIfEmbeddedSubtitlesPresent { get; set; } public bool SkipIfAudioTrackMatches { get; set; } @@ -24,13 +32,5 @@ namespace MediaBrowser.Model.Providers public bool IsOpenSubtitleVipAccount { get; set; } public bool RequirePerfectMatch { get; set; } - - public SubtitleOptions() - { - DownloadLanguages = Array.Empty(); - - SkipIfAudioTrackMatches = true; - RequirePerfectMatch = true; - } } } diff --git a/MediaBrowser.Model/Querying/EpisodeQuery.cs b/MediaBrowser.Model/Querying/EpisodeQuery.cs index 13b1a0dcbf..56a7f33201 100644 --- a/MediaBrowser.Model/Querying/EpisodeQuery.cs +++ b/MediaBrowser.Model/Querying/EpisodeQuery.cs @@ -7,6 +7,11 @@ namespace MediaBrowser.Model.Querying { public class EpisodeQuery { + public EpisodeQuery() + { + Fields = Array.Empty(); + } + /// /// Gets or sets the user identifier. /// @@ -66,10 +71,5 @@ namespace MediaBrowser.Model.Querying /// /// The start item identifier. public string StartItemId { get; set; } - - public EpisodeQuery() - { - Fields = Array.Empty(); - } } } diff --git a/MediaBrowser.Model/Querying/LatestItemsQuery.cs b/MediaBrowser.Model/Querying/LatestItemsQuery.cs index 7954ef4b43..f555ffb36d 100644 --- a/MediaBrowser.Model/Querying/LatestItemsQuery.cs +++ b/MediaBrowser.Model/Querying/LatestItemsQuery.cs @@ -14,31 +14,32 @@ namespace MediaBrowser.Model.Querying } /// - /// The user to localize search results for. + /// Gets or sets the user to localize search results for. /// /// The user id. public Guid UserId { get; set; } /// + /// Gets or sets the parent id. /// Specify this to localize the search to a specific item or folder. Omit to use the root. /// /// The parent id. public Guid ParentId { get; set; } /// - /// Skips over a given number of items within the results. Use for paging. + /// Gets or sets the start index. Used for paging. /// /// The start index. public int? StartIndex { get; set; } /// - /// The maximum number of items to return. + /// Gets or sets the maximum number of items to return. /// /// The limit. public int? Limit { get; set; } /// - /// Fields to return within the items, in addition to basic information. + /// Gets or sets the fields to return within the items, in addition to basic information. /// /// The fields. public ItemFields[] Fields { get; set; } diff --git a/MediaBrowser.Model/Querying/MovieRecommendationQuery.cs b/MediaBrowser.Model/Querying/MovieRecommendationQuery.cs index 1c8875890a..b800f5de5f 100644 --- a/MediaBrowser.Model/Querying/MovieRecommendationQuery.cs +++ b/MediaBrowser.Model/Querying/MovieRecommendationQuery.cs @@ -7,6 +7,13 @@ namespace MediaBrowser.Model.Querying { public class MovieRecommendationQuery { + public MovieRecommendationQuery() + { + ItemLimit = 10; + CategoryLimit = 6; + Fields = Array.Empty(); + } + /// /// Gets or sets the user identifier. /// @@ -36,12 +43,5 @@ namespace MediaBrowser.Model.Querying /// /// The fields. public ItemFields[] Fields { get; set; } - - public MovieRecommendationQuery() - { - ItemLimit = 10; - CategoryLimit = 6; - Fields = Array.Empty(); - } } } diff --git a/MediaBrowser.Model/Querying/NextUpQuery.cs b/MediaBrowser.Model/Querying/NextUpQuery.cs index 001d0623cc..0555afc00d 100644 --- a/MediaBrowser.Model/Querying/NextUpQuery.cs +++ b/MediaBrowser.Model/Querying/NextUpQuery.cs @@ -8,6 +8,13 @@ namespace MediaBrowser.Model.Querying { public class NextUpQuery { + public NextUpQuery() + { + EnableImageTypes = Array.Empty(); + EnableTotalRecordCount = true; + DisableFirstEpisode = false; + } + /// /// Gets or sets the user id. /// @@ -27,19 +34,19 @@ namespace MediaBrowser.Model.Querying public string SeriesId { get; set; } /// - /// Skips over a given number of items within the results. Use for paging. + /// Gets or sets the start index. Use for paging. /// /// The start index. public int? StartIndex { get; set; } /// - /// The maximum number of items to return. + /// Gets or sets the maximum number of items to return. /// /// The limit. public int? Limit { get; set; } /// - /// Fields to return within the items, in addition to basic information. + /// gets or sets the fields to return within the items, in addition to basic information. /// /// The fields. public ItemFields[] Fields { get; set; } @@ -68,12 +75,5 @@ namespace MediaBrowser.Model.Querying /// Gets or sets a value indicating whether do disable sending first episode as next up. /// public bool DisableFirstEpisode { get; set; } - - public NextUpQuery() - { - EnableImageTypes = Array.Empty(); - EnableTotalRecordCount = true; - DisableFirstEpisode = false; - } } } diff --git a/MediaBrowser.Model/Querying/QueryFilters.cs b/MediaBrowser.Model/Querying/QueryFilters.cs index 6e4d251818..73b27a7b06 100644 --- a/MediaBrowser.Model/Querying/QueryFilters.cs +++ b/MediaBrowser.Model/Querying/QueryFilters.cs @@ -6,35 +6,16 @@ using MediaBrowser.Model.Dto; namespace MediaBrowser.Model.Querying { - public class QueryFiltersLegacy + public class QueryFilters { - public string[] Genres { get; set; } - - public string[] Tags { get; set; } - - public string[] OfficialRatings { get; set; } - - public int[] Years { get; set; } - - public QueryFiltersLegacy() + public QueryFilters() { - Genres = Array.Empty(); Tags = Array.Empty(); - OfficialRatings = Array.Empty(); - Years = Array.Empty(); + Genres = Array.Empty(); } - } - public class QueryFilters - { public NameGuidPair[] Genres { get; set; } public string[] Tags { get; set; } - - public QueryFilters() - { - Tags = Array.Empty(); - Genres = Array.Empty(); - } } } diff --git a/MediaBrowser.Model/Querying/QueryFiltersLegacy.cs b/MediaBrowser.Model/Querying/QueryFiltersLegacy.cs new file mode 100644 index 0000000000..fcb450ed30 --- /dev/null +++ b/MediaBrowser.Model/Querying/QueryFiltersLegacy.cs @@ -0,0 +1,26 @@ +#nullable disable +#pragma warning disable CS1591 + +using System; + +namespace MediaBrowser.Model.Querying +{ + public class QueryFiltersLegacy + { + public QueryFiltersLegacy() + { + Genres = Array.Empty(); + Tags = Array.Empty(); + OfficialRatings = Array.Empty(); + Years = Array.Empty(); + } + + public string[] Genres { get; set; } + + public string[] Tags { get; set; } + + public string[] OfficialRatings { get; set; } + + public int[] Years { get; set; } + } +} diff --git a/MediaBrowser.Model/Querying/QueryResult.cs b/MediaBrowser.Model/Querying/QueryResult.cs index 490f48b84b..8ce794800b 100644 --- a/MediaBrowser.Model/Querying/QueryResult.cs +++ b/MediaBrowser.Model/Querying/QueryResult.cs @@ -8,6 +8,17 @@ namespace MediaBrowser.Model.Querying { public class QueryResult { + public QueryResult() + { + Items = Array.Empty(); + } + + public QueryResult(IReadOnlyList items) + { + Items = items; + TotalRecordCount = items.Count; + } + /// /// Gets or sets the items. /// @@ -15,26 +26,15 @@ namespace MediaBrowser.Model.Querying public IReadOnlyList Items { get; set; } /// - /// The total number of records available. + /// Gets or sets the total number of records available. /// /// The total record count. public int TotalRecordCount { get; set; } /// - /// The index of the first record in Items. + /// Gets or sets the index of the first record in Items. /// /// First record index. public int StartIndex { get; set; } - - public QueryResult() - { - Items = Array.Empty(); - } - - public QueryResult(IReadOnlyList items) - { - Items = items; - TotalRecordCount = items.Count; - } } } diff --git a/MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs b/MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs index eb62394605..2cf0f0d5f8 100644 --- a/MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs +++ b/MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs @@ -8,6 +8,11 @@ namespace MediaBrowser.Model.Querying { public class UpcomingEpisodesQuery { + public UpcomingEpisodesQuery() + { + EnableImageTypes = Array.Empty(); + } + /// /// Gets or sets the user id. /// @@ -21,19 +26,19 @@ namespace MediaBrowser.Model.Querying public string ParentId { get; set; } /// - /// Skips over a given number of items within the results. Use for paging. + /// Gets or sets the start index. Use for paging. /// /// The start index. public int? StartIndex { get; set; } /// - /// The maximum number of items to return. + /// Gets or sets the maximum number of items to return. /// /// The limit. public int? Limit { get; set; } /// - /// Fields to return within the items, in addition to basic information. + /// Gets or sets the fields to return within the items, in addition to basic information. /// /// The fields. public ItemFields[] Fields { get; set; } @@ -55,10 +60,5 @@ namespace MediaBrowser.Model.Querying /// /// The enable image types. public ImageType[] EnableImageTypes { get; set; } - - public UpcomingEpisodesQuery() - { - EnableImageTypes = Array.Empty(); - } } } diff --git a/MediaBrowser.Model/Search/SearchQuery.cs b/MediaBrowser.Model/Search/SearchQuery.cs index ce60062cd3..aedfa4d363 100644 --- a/MediaBrowser.Model/Search/SearchQuery.cs +++ b/MediaBrowser.Model/Search/SearchQuery.cs @@ -7,8 +7,21 @@ namespace MediaBrowser.Model.Search { public class SearchQuery { + public SearchQuery() + { + IncludeArtists = true; + IncludeGenres = true; + IncludeMedia = true; + IncludePeople = true; + IncludeStudios = true; + + MediaTypes = Array.Empty(); + IncludeItemTypes = Array.Empty(); + ExcludeItemTypes = Array.Empty(); + } + /// - /// The user to localize search results for. + /// Gets or sets the user to localize search results for. /// /// The user id. public Guid UserId { get; set; } @@ -20,13 +33,13 @@ namespace MediaBrowser.Model.Search public string SearchTerm { get; set; } /// - /// Skips over a given number of items within the results. Use for paging. + /// Gets or sets the start index. Used for paging. /// /// The start index. public int? StartIndex { get; set; } /// - /// The maximum number of items to return. + /// Gets or sets the maximum number of items to return. /// /// The limit. public int? Limit { get; set; } @@ -58,18 +71,5 @@ namespace MediaBrowser.Model.Search public bool? IsKids { get; set; } public bool? IsSports { get; set; } - - public SearchQuery() - { - IncludeArtists = true; - IncludeGenres = true; - IncludeMedia = true; - IncludePeople = true; - IncludeStudios = true; - - MediaTypes = Array.Empty(); - IncludeItemTypes = Array.Empty(); - ExcludeItemTypes = Array.Empty(); - } } } diff --git a/MediaBrowser.Model/Session/BrowseRequest.cs b/MediaBrowser.Model/Session/BrowseRequest.cs index 1c997d5846..65afe5cf34 100644 --- a/MediaBrowser.Model/Session/BrowseRequest.cs +++ b/MediaBrowser.Model/Session/BrowseRequest.cs @@ -7,6 +7,7 @@ namespace MediaBrowser.Model.Session public class BrowseRequest { /// + /// Gets or sets the item type. /// Artist, Genre, Studio, Person, or any kind of BaseItem. /// /// The type of the item. diff --git a/MediaBrowser.Model/Session/ClientCapabilities.cs b/MediaBrowser.Model/Session/ClientCapabilities.cs index 5852f4e37a..d692906c64 100644 --- a/MediaBrowser.Model/Session/ClientCapabilities.cs +++ b/MediaBrowser.Model/Session/ClientCapabilities.cs @@ -9,6 +9,13 @@ namespace MediaBrowser.Model.Session { public class ClientCapabilities { + public ClientCapabilities() + { + PlayableMediaTypes = Array.Empty(); + SupportedCommands = Array.Empty(); + SupportsPersistentIdentifier = true; + } + public IReadOnlyList PlayableMediaTypes { get; set; } public IReadOnlyList SupportedCommands { get; set; } @@ -28,12 +35,5 @@ namespace MediaBrowser.Model.Session public string AppStoreUrl { get; set; } public string IconUrl { get; set; } - - public ClientCapabilities() - { - PlayableMediaTypes = Array.Empty(); - SupportedCommands = Array.Empty(); - SupportsPersistentIdentifier = true; - } } } diff --git a/MediaBrowser.Model/Session/GeneralCommand.cs b/MediaBrowser.Model/Session/GeneralCommand.cs index 77bb6bcf77..29528c1106 100644 --- a/MediaBrowser.Model/Session/GeneralCommand.cs +++ b/MediaBrowser.Model/Session/GeneralCommand.cs @@ -1,4 +1,3 @@ -#nullable disable #pragma warning disable CS1591 using System; @@ -8,15 +7,15 @@ namespace MediaBrowser.Model.Session { public class GeneralCommand { - public GeneralCommandType Name { get; set; } - - public Guid ControllingUserId { get; set; } - - public Dictionary Arguments { get; set; } - public GeneralCommand() { Arguments = new Dictionary(); } + + public GeneralCommandType Name { get; set; } + + public Guid ControllingUserId { get; set; } + + public Dictionary Arguments { get; } } } diff --git a/MediaBrowser.Model/Session/PlaybackProgressInfo.cs b/MediaBrowser.Model/Session/PlaybackProgressInfo.cs index 73dbe6a2da..a6e7efcb0c 100644 --- a/MediaBrowser.Model/Session/PlaybackProgressInfo.cs +++ b/MediaBrowser.Model/Session/PlaybackProgressInfo.cs @@ -111,18 +111,4 @@ namespace MediaBrowser.Model.Session public string PlaylistItemId { get; set; } } - - public enum RepeatMode - { - RepeatNone = 0, - RepeatAll = 1, - RepeatOne = 2 - } - - public class QueueItem - { - public Guid Id { get; set; } - - public string PlaylistItemId { get; set; } - } } diff --git a/MediaBrowser.Model/Session/PlaystateCommand.cs b/MediaBrowser.Model/Session/PlaystateCommand.cs index 3aa091f791..df47f3b73d 100644 --- a/MediaBrowser.Model/Session/PlaystateCommand.cs +++ b/MediaBrowser.Model/Session/PlaystateCommand.cs @@ -1,5 +1,3 @@ -#pragma warning disable CS1591 - namespace MediaBrowser.Model.Session { /// @@ -46,6 +44,10 @@ namespace MediaBrowser.Model.Session /// The fast forward. /// FastForward, + + /// + /// The play pause. + /// PlayPause } } diff --git a/MediaBrowser.Model/Session/QueueItem.cs b/MediaBrowser.Model/Session/QueueItem.cs new file mode 100644 index 0000000000..32b19101b2 --- /dev/null +++ b/MediaBrowser.Model/Session/QueueItem.cs @@ -0,0 +1,14 @@ +#nullable disable +#pragma warning disable CS1591 + +using System; + +namespace MediaBrowser.Model.Session +{ + public class QueueItem + { + public Guid Id { get; set; } + + public string PlaylistItemId { get; set; } + } +} diff --git a/MediaBrowser.Model/Session/RepeatMode.cs b/MediaBrowser.Model/Session/RepeatMode.cs new file mode 100644 index 0000000000..c6e173d6b8 --- /dev/null +++ b/MediaBrowser.Model/Session/RepeatMode.cs @@ -0,0 +1,11 @@ +#pragma warning disable CS1591 + +namespace MediaBrowser.Model.Session +{ + public enum RepeatMode + { + RepeatNone = 0, + RepeatAll = 1, + RepeatOne = 2 + } +} diff --git a/MediaBrowser.Model/Session/TranscodeReason.cs b/MediaBrowser.Model/Session/TranscodeReason.cs new file mode 100644 index 0000000000..e93b5d2882 --- /dev/null +++ b/MediaBrowser.Model/Session/TranscodeReason.cs @@ -0,0 +1,31 @@ +#pragma warning disable CS1591 + +namespace MediaBrowser.Model.Session +{ + public enum TranscodeReason + { + ContainerNotSupported = 0, + VideoCodecNotSupported = 1, + AudioCodecNotSupported = 2, + ContainerBitrateExceedsLimit = 3, + AudioBitrateNotSupported = 4, + AudioChannelsNotSupported = 5, + VideoResolutionNotSupported = 6, + UnknownVideoStreamInfo = 7, + UnknownAudioStreamInfo = 8, + AudioProfileNotSupported = 9, + AudioSampleRateNotSupported = 10, + AnamorphicVideoNotSupported = 11, + InterlacedVideoNotSupported = 12, + SecondaryAudioNotSupported = 13, + RefFramesNotSupported = 14, + VideoBitDepthNotSupported = 15, + VideoBitrateNotSupported = 16, + VideoFramerateNotSupported = 17, + VideoLevelNotSupported = 18, + VideoProfileNotSupported = 19, + AudioBitDepthNotSupported = 20, + SubtitleCodecNotSupported = 21, + DirectPlayError = 22 + } +} diff --git a/MediaBrowser.Model/Session/TranscodingInfo.cs b/MediaBrowser.Model/Session/TranscodingInfo.cs index e832c2f6fa..064a087d5d 100644 --- a/MediaBrowser.Model/Session/TranscodingInfo.cs +++ b/MediaBrowser.Model/Session/TranscodingInfo.cs @@ -7,6 +7,11 @@ namespace MediaBrowser.Model.Session { public class TranscodingInfo { + public TranscodingInfo() + { + TranscodeReasons = Array.Empty(); + } + public string AudioCodec { get; set; } public string VideoCodec { get; set; } @@ -30,37 +35,5 @@ namespace MediaBrowser.Model.Session public int? AudioChannels { get; set; } public TranscodeReason[] TranscodeReasons { get; set; } - - public TranscodingInfo() - { - TranscodeReasons = Array.Empty(); - } - } - - public enum TranscodeReason - { - ContainerNotSupported = 0, - VideoCodecNotSupported = 1, - AudioCodecNotSupported = 2, - ContainerBitrateExceedsLimit = 3, - AudioBitrateNotSupported = 4, - AudioChannelsNotSupported = 5, - VideoResolutionNotSupported = 6, - UnknownVideoStreamInfo = 7, - UnknownAudioStreamInfo = 8, - AudioProfileNotSupported = 9, - AudioSampleRateNotSupported = 10, - AnamorphicVideoNotSupported = 11, - InterlacedVideoNotSupported = 12, - SecondaryAudioNotSupported = 13, - RefFramesNotSupported = 14, - VideoBitDepthNotSupported = 15, - VideoBitrateNotSupported = 16, - VideoFramerateNotSupported = 17, - VideoLevelNotSupported = 18, - VideoProfileNotSupported = 19, - AudioBitDepthNotSupported = 20, - SubtitleCodecNotSupported = 21, - DirectPlayError = 22 } } diff --git a/MediaBrowser.Model/Sync/SyncJob.cs b/MediaBrowser.Model/Sync/SyncJob.cs index b9290b6e83..3e396e5d13 100644 --- a/MediaBrowser.Model/Sync/SyncJob.cs +++ b/MediaBrowser.Model/Sync/SyncJob.cs @@ -7,6 +7,11 @@ namespace MediaBrowser.Model.Sync { public class SyncJob { + public SyncJob() + { + RequestedItemIds = Array.Empty(); + } + /// /// Gets or sets the identifier. /// @@ -126,10 +131,5 @@ namespace MediaBrowser.Model.Sync public string PrimaryImageItemId { get; set; } public string PrimaryImageTag { get; set; } - - public SyncJob() - { - RequestedItemIds = Array.Empty(); - } } } diff --git a/MediaBrowser.Model/System/SystemInfo.cs b/MediaBrowser.Model/System/SystemInfo.cs index 4b83fb7e61..d75ae91c02 100644 --- a/MediaBrowser.Model/System/SystemInfo.cs +++ b/MediaBrowser.Model/System/SystemInfo.cs @@ -30,6 +30,14 @@ namespace MediaBrowser.Model.System /// public class SystemInfo : PublicSystemInfo { + /// + /// Initializes a new instance of the class. + /// + public SystemInfo() + { + CompletedInstallations = Array.Empty(); + } + /// /// Gets or sets the display name of the operating system. /// @@ -37,7 +45,7 @@ namespace MediaBrowser.Model.System public string OperatingSystemDisplayName { get; set; } /// - /// Get or sets the package name. + /// Gets or sets the package name. /// /// The value of the '-package' command line argument. public string PackageName { get; set; } @@ -127,13 +135,5 @@ namespace MediaBrowser.Model.System public FFmpegLocation EncoderLocation { get; set; } public Architecture SystemArchitecture { get; set; } - - /// - /// Initializes a new instance of the class. - /// - public SystemInfo() - { - CompletedInstallations = Array.Empty(); - } } } diff --git a/MediaBrowser.Model/System/WakeOnLanInfo.cs b/MediaBrowser.Model/System/WakeOnLanInfo.cs index b2cbe737d1..aba19a6baf 100644 --- a/MediaBrowser.Model/System/WakeOnLanInfo.cs +++ b/MediaBrowser.Model/System/WakeOnLanInfo.cs @@ -36,7 +36,7 @@ namespace MediaBrowser.Model.System /// Gets the MAC address of the device. /// /// The MAC address. - public string? MacAddress { get; set; } + public string? MacAddress { get; } /// /// Gets or sets the wake-on-LAN port. diff --git a/MediaBrowser.Model/Tasks/IScheduledTaskWorker.cs b/MediaBrowser.Model/Tasks/IScheduledTaskWorker.cs index 2f05e08c51..ca769e26b3 100644 --- a/MediaBrowser.Model/Tasks/IScheduledTaskWorker.cs +++ b/MediaBrowser.Model/Tasks/IScheduledTaskWorker.cs @@ -15,7 +15,7 @@ namespace MediaBrowser.Model.Tasks event EventHandler> TaskProgress; /// - /// Gets or sets the scheduled task. + /// Gets the scheduled task. /// /// The scheduled task. IScheduledTask ScheduledTask { get; } @@ -57,10 +57,9 @@ namespace MediaBrowser.Model.Tasks double? CurrentProgress { get; } /// - /// Gets the triggers that define when the task will run. + /// Gets or sets the triggers that define when the task will run. /// /// The triggers. - /// value TaskTriggerInfo[] Triggers { get; set; } /// diff --git a/MediaBrowser.Model/Tasks/ITaskManager.cs b/MediaBrowser.Model/Tasks/ITaskManager.cs index 02b29074e3..a86bf2a1c8 100644 --- a/MediaBrowser.Model/Tasks/ITaskManager.cs +++ b/MediaBrowser.Model/Tasks/ITaskManager.cs @@ -9,6 +9,10 @@ namespace MediaBrowser.Model.Tasks { public interface ITaskManager : IDisposable { + event EventHandler> TaskExecuting; + + event EventHandler TaskCompleted; + /// /// Gets the list of Scheduled Tasks. /// @@ -18,7 +22,7 @@ namespace MediaBrowser.Model.Tasks /// /// Cancels if running and queue. /// - /// + /// An implementatin of . /// Task options. void CancelIfRunningAndQueue(TaskOptions options) where T : IScheduledTask; @@ -26,21 +30,21 @@ namespace MediaBrowser.Model.Tasks /// /// Cancels if running and queue. /// - /// + /// An implementatin of . void CancelIfRunningAndQueue() where T : IScheduledTask; /// /// Cancels if running. /// - /// + /// An implementatin of . void CancelIfRunning() where T : IScheduledTask; /// /// Queues the scheduled task. /// - /// + /// An implementatin of . /// Task options. void QueueScheduledTask(TaskOptions options) where T : IScheduledTask; @@ -48,7 +52,7 @@ namespace MediaBrowser.Model.Tasks /// /// Queues the scheduled task. /// - /// + /// An implementatin of . void QueueScheduledTask() where T : IScheduledTask; @@ -58,6 +62,8 @@ namespace MediaBrowser.Model.Tasks /// /// Queues the scheduled task. /// + /// The to queue. + /// The to use. void QueueScheduledTask(IScheduledTask task, TaskOptions options); /// @@ -67,12 +73,10 @@ namespace MediaBrowser.Model.Tasks void AddTasks(IEnumerable tasks); void Cancel(IScheduledTaskWorker task); + Task Execute(IScheduledTaskWorker task, TaskOptions options); void Execute() where T : IScheduledTask; - - event EventHandler> TaskExecuting; - event EventHandler TaskCompleted; } } diff --git a/MediaBrowser.Model/Tasks/ITaskTrigger.cs b/MediaBrowser.Model/Tasks/ITaskTrigger.cs index 5c30d6c220..cbd60cca18 100644 --- a/MediaBrowser.Model/Tasks/ITaskTrigger.cs +++ b/MediaBrowser.Model/Tasks/ITaskTrigger.cs @@ -21,6 +21,10 @@ namespace MediaBrowser.Model.Tasks /// /// Stars waiting for the trigger action. /// + /// Result of the last run triggerd task. + /// The . + /// The name of the task. + /// Wheter or not this is is fired during startup. void Start(TaskResult lastResult, ILogger logger, string taskName, bool isApplicationStartup); /// diff --git a/MediaBrowser.Model/Tasks/TaskInfo.cs b/MediaBrowser.Model/Tasks/TaskInfo.cs index 77100dfe76..16de0b1210 100644 --- a/MediaBrowser.Model/Tasks/TaskInfo.cs +++ b/MediaBrowser.Model/Tasks/TaskInfo.cs @@ -8,6 +8,14 @@ namespace MediaBrowser.Model.Tasks /// public class TaskInfo { + /// + /// Initializes a new instance of the class. + /// + public TaskInfo() + { + Triggers = Array.Empty(); + } + /// /// Gets or sets the name. /// @@ -67,13 +75,5 @@ namespace MediaBrowser.Model.Tasks /// /// The key. public string Key { get; set; } - - /// - /// Initializes a new instance of the class. - /// - public TaskInfo() - { - Triggers = Array.Empty(); - } } } diff --git a/MediaBrowser.Model/Tasks/TaskTriggerInfo.cs b/MediaBrowser.Model/Tasks/TaskTriggerInfo.cs index 5aeaffc2b3..f8a8c727ef 100644 --- a/MediaBrowser.Model/Tasks/TaskTriggerInfo.cs +++ b/MediaBrowser.Model/Tasks/TaskTriggerInfo.cs @@ -10,6 +10,12 @@ namespace MediaBrowser.Model.Tasks /// public class TaskTriggerInfo { + public const string TriggerDaily = "DailyTrigger"; + public const string TriggerWeekly = "WeeklyTrigger"; + public const string TriggerInterval = "IntervalTrigger"; + public const string TriggerSystemEvent = "SystemEventTrigger"; + public const string TriggerStartup = "StartupTrigger"; + /// /// Gets or sets the type. /// @@ -39,11 +45,5 @@ namespace MediaBrowser.Model.Tasks /// /// The maximum runtime ticks. public long? MaxRuntimeTicks { get; set; } - - public const string TriggerDaily = "DailyTrigger"; - public const string TriggerWeekly = "WeeklyTrigger"; - public const string TriggerInterval = "IntervalTrigger"; - public const string TriggerSystemEvent = "SystemEventTrigger"; - public const string TriggerStartup = "StartupTrigger"; } } diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs index 37da04adf7..111070d813 100644 --- a/MediaBrowser.Model/Users/UserPolicy.cs +++ b/MediaBrowser.Model/Users/UserPolicy.cs @@ -10,6 +10,56 @@ namespace MediaBrowser.Model.Users { public class UserPolicy { + public UserPolicy() + { + IsHidden = true; + + EnableContentDeletion = false; + EnableContentDeletionFromFolders = Array.Empty(); + + EnableSyncTranscoding = true; + EnableMediaConversion = true; + + EnableMediaPlayback = true; + EnableAudioPlaybackTranscoding = true; + EnableVideoPlaybackTranscoding = true; + EnablePlaybackRemuxing = true; + ForceRemoteSourceTranscoding = false; + EnableLiveTvManagement = true; + EnableLiveTvAccess = true; + + // Without this on by default, admins won't be able to do this + // Improve in the future + EnableLiveTvManagement = true; + + EnableSharedDeviceControl = true; + + BlockedTags = Array.Empty(); + BlockUnratedItems = Array.Empty(); + + EnableUserPreferenceAccess = true; + + AccessSchedules = Array.Empty(); + + LoginAttemptsBeforeLockout = -1; + + MaxActiveSessions = 0; + + EnableAllChannels = true; + EnabledChannels = Array.Empty(); + + EnableAllFolders = true; + EnabledFolders = Array.Empty(); + + EnabledDevices = Array.Empty(); + EnableAllDevices = true; + + EnableContentDownloading = true; + EnablePublicSharing = true; + EnableRemoteAccess = true; + SyncPlayAccess = SyncPlayUserAccessType.CreateAndJoinGroups; + } + /// /// Gets or sets a value indicating whether this instance is administrator. /// @@ -112,55 +162,5 @@ namespace MediaBrowser.Model.Users /// /// Access level to SyncPlay features. public SyncPlayUserAccessType SyncPlayAccess { get; set; } - - public UserPolicy() - { - IsHidden = true; - - EnableContentDeletion = false; - EnableContentDeletionFromFolders = Array.Empty(); - - EnableSyncTranscoding = true; - EnableMediaConversion = true; - - EnableMediaPlayback = true; - EnableAudioPlaybackTranscoding = true; - EnableVideoPlaybackTranscoding = true; - EnablePlaybackRemuxing = true; - ForceRemoteSourceTranscoding = false; - EnableLiveTvManagement = true; - EnableLiveTvAccess = true; - - // Without this on by default, admins won't be able to do this - // Improve in the future - EnableLiveTvManagement = true; - - EnableSharedDeviceControl = true; - - BlockedTags = Array.Empty(); - BlockUnratedItems = Array.Empty(); - - EnableUserPreferenceAccess = true; - - AccessSchedules = Array.Empty(); - - LoginAttemptsBeforeLockout = -1; - - MaxActiveSessions = 0; - - EnableAllChannels = true; - EnabledChannels = Array.Empty(); - - EnableAllFolders = true; - EnabledFolders = Array.Empty(); - - EnabledDevices = Array.Empty(); - EnableAllDevices = true; - - EnableContentDownloading = true; - EnablePublicSharing = true; - EnableRemoteAccess = true; - SyncPlayAccess = SyncPlayUserAccessType.CreateAndJoinGroups; - } } } diff --git a/jellyfin.ruleset b/jellyfin.ruleset index fa09bfb66a..81337390cc 100644 --- a/jellyfin.ruleset +++ b/jellyfin.ruleset @@ -32,6 +32,8 @@ + + diff --git a/tests/Jellyfin.Api.Tests/ModelBinders/TestType.cs b/tests/Jellyfin.Api.Tests/ModelBinders/TestType.cs index 544a74637a..92c534eaea 100644 --- a/tests/Jellyfin.Api.Tests/ModelBinders/TestType.cs +++ b/tests/Jellyfin.Api.Tests/ModelBinders/TestType.cs @@ -1,17 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Text; - namespace Jellyfin.Api.Tests.ModelBinders { public enum TestType { -#pragma warning disable SA1602 // Enumeration items should be documented How, Much, Is, The, Fish -#pragma warning restore SA1602 // Enumeration items should be documented } } -- cgit v1.2.3 From ebb6467db46f650d39fe9b2de266afcd561aa351 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Sat, 27 Feb 2021 11:42:37 -0500 Subject: Remove unused entity --- MediaBrowser.Model/Configuration/AccessSchedule.cs | 27 ---------------------- 1 file changed, 27 deletions(-) delete mode 100644 MediaBrowser.Model/Configuration/AccessSchedule.cs (limited to 'MediaBrowser.Model/Configuration') diff --git a/MediaBrowser.Model/Configuration/AccessSchedule.cs b/MediaBrowser.Model/Configuration/AccessSchedule.cs deleted file mode 100644 index 7bd355449f..0000000000 --- a/MediaBrowser.Model/Configuration/AccessSchedule.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Jellyfin.Data.Enums; - -#pragma warning disable CS1591 - -namespace MediaBrowser.Model.Configuration -{ - public class AccessSchedule - { - /// - /// Gets or sets the day of week. - /// - /// The day of week. - public DynamicDayOfWeek DayOfWeek { get; set; } - - /// - /// Gets or sets the start hour. - /// - /// The start hour. - public double StartHour { get; set; } - - /// - /// Gets or sets the end hour. - /// - /// The end hour. - public double EndHour { get; set; } - } -} -- cgit v1.2.3 From cc0f191228d7444a598370ac93ba7312700d58b1 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Thu, 8 Apr 2021 09:57:17 -0400 Subject: Disable hevc encoding by default --- MediaBrowser.Model/Configuration/EncodingOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'MediaBrowser.Model/Configuration') diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index a9b2803017..365bbeef66 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -35,7 +35,7 @@ namespace MediaBrowser.Model.Configuration EnableDecodingColorDepth10Vp9 = true; EnableEnhancedNvdecDecoder = true; EnableHardwareEncoding = true; - AllowHevcEncoding = true; + AllowHevcEncoding = false; EnableSubtitleExtraction = true; HardwareDecodingCodecs = new string[] { "h264", "vc1" }; } -- cgit v1.2.3 From 383c2d73745345e4b6edc0f1b605851cd4062a83 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Fri, 11 Jun 2021 23:36:10 +0200 Subject: Remove useless nullable directives --- Emby.Server.Implementations/Updates/InstallationManager.cs | 4 ---- MediaBrowser.Controller/Drawing/IImageEncoder.cs | 3 --- MediaBrowser.Controller/Drawing/IImageProcessor.cs | 3 --- MediaBrowser.Controller/Drawing/ImageHelper.cs | 3 --- MediaBrowser.Controller/Entities/BaseItemExtensions.cs | 3 --- MediaBrowser.Controller/LiveTv/TimerEventInfo.cs | 3 --- MediaBrowser.Controller/Net/IWebSocketConnection.cs | 4 ---- MediaBrowser.Model/Configuration/PathSubstitution.cs | 2 -- MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs | 1 - MediaBrowser.Model/Plugins/PluginInfo.cs | 2 -- MediaBrowser.Model/Plugins/PluginPageInfo.cs | 2 -- MediaBrowser.Model/Updates/VersionInfo.cs | 2 -- 12 files changed, 32 deletions(-) (limited to 'MediaBrowser.Model/Configuration') diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index ba62857eb9..b0921cbd82 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -1,7 +1,3 @@ -#nullable disable - -#nullable enable - using System; using System.Collections.Concurrent; using System.Collections.Generic; diff --git a/MediaBrowser.Controller/Drawing/IImageEncoder.cs b/MediaBrowser.Controller/Drawing/IImageEncoder.cs index 800f7a8bb9..4e640d4215 100644 --- a/MediaBrowser.Controller/Drawing/IImageEncoder.cs +++ b/MediaBrowser.Controller/Drawing/IImageEncoder.cs @@ -1,7 +1,4 @@ -#nullable disable - #pragma warning disable CS1591 -#nullable enable using System; using System.Collections.Generic; diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs index 9bfead8b3b..c7f61a90bb 100644 --- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs +++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs @@ -1,7 +1,4 @@ -#nullable disable - #pragma warning disable CS1591 -#nullable enable using System; using System.Collections.Generic; diff --git a/MediaBrowser.Controller/Drawing/ImageHelper.cs b/MediaBrowser.Controller/Drawing/ImageHelper.cs index 204175ed5a..9ef92bc981 100644 --- a/MediaBrowser.Controller/Drawing/ImageHelper.cs +++ b/MediaBrowser.Controller/Drawing/ImageHelper.cs @@ -1,7 +1,4 @@ -#nullable disable - #pragma warning disable CS1591 -#nullable enable using MediaBrowser.Model.Drawing; diff --git a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs index c39b18891f..89ad392a4b 100644 --- a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs +++ b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs @@ -1,6 +1,3 @@ -#nullable disable - -#nullable enable #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs b/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs index 728387c56d..92eb0be9c0 100644 --- a/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs +++ b/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs @@ -1,6 +1,3 @@ -#nullable disable - -#nullable enable #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Net/IWebSocketConnection.cs b/MediaBrowser.Controller/Net/IWebSocketConnection.cs index f1ba1ec720..c8c5caf809 100644 --- a/MediaBrowser.Controller/Net/IWebSocketConnection.cs +++ b/MediaBrowser.Controller/Net/IWebSocketConnection.cs @@ -1,9 +1,5 @@ -#nullable disable - #pragma warning disable CS1591 -#nullable enable - using System; using System.Net; using System.Net.WebSockets; diff --git a/MediaBrowser.Model/Configuration/PathSubstitution.cs b/MediaBrowser.Model/Configuration/PathSubstitution.cs index bffaa85945..2c9b5f005c 100644 --- a/MediaBrowser.Model/Configuration/PathSubstitution.cs +++ b/MediaBrowser.Model/Configuration/PathSubstitution.cs @@ -1,5 +1,3 @@ -#nullable enable - namespace MediaBrowser.Model.Configuration { /// diff --git a/MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs b/MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs index b3db57b6d9..d5c3a6aec5 100644 --- a/MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs +++ b/MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs @@ -1,4 +1,3 @@ -#nullable enable #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Model/Plugins/PluginInfo.cs b/MediaBrowser.Model/Plugins/PluginInfo.cs index 25216610d4..8eb90bdb05 100644 --- a/MediaBrowser.Model/Plugins/PluginInfo.cs +++ b/MediaBrowser.Model/Plugins/PluginInfo.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; namespace MediaBrowser.Model.Plugins diff --git a/MediaBrowser.Model/Plugins/PluginPageInfo.cs b/MediaBrowser.Model/Plugins/PluginPageInfo.cs index 85c0aa204b..f4d83c28b1 100644 --- a/MediaBrowser.Model/Plugins/PluginPageInfo.cs +++ b/MediaBrowser.Model/Plugins/PluginPageInfo.cs @@ -1,5 +1,3 @@ -#nullable enable - namespace MediaBrowser.Model.Plugins { /// diff --git a/MediaBrowser.Model/Updates/VersionInfo.cs b/MediaBrowser.Model/Updates/VersionInfo.cs index 209092265e..03a540dde2 100644 --- a/MediaBrowser.Model/Updates/VersionInfo.cs +++ b/MediaBrowser.Model/Updates/VersionInfo.cs @@ -1,5 +1,3 @@ -#nullable enable - using System.Text.Json.Serialization; using SysVersion = System.Version; -- cgit v1.2.3