diff options
174 files changed, 2546 insertions, 2502 deletions
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index a97a4c741..edc8b0864 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -7,6 +7,7 @@ - [anthonylavado](https://github.com/anthonylavado) - [Artiume](https://github.com/Artiume) - [AThomsen](https://github.com/AThomsen) + - [barongreenback](https://github.com/BaronGreenback) - [barronpm](https://github.com/barronpm) - [bilde2910](https://github.com/bilde2910) - [bfayers](https://github.com/bfayers) diff --git a/Dockerfile b/Dockerfile index 963027b49..41dd3d081 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,8 +27,15 @@ ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility" COPY --from=builder /jellyfin /jellyfin COPY --from=web-builder /dist /jellyfin/jellyfin-web + +# https://github.com/intel/compute-runtime/releases +ARG GMMLIB_VERSION=20.3.2 +ARG IGC_VERSION=1.0.5435 +ARG NEO_VERSION=20.46.18421 +ARG LEVEL_ZERO_VERSION=1.0.18421 + # Install dependencies: -# mesa-va-drivers: needed for AMD VAAPI +# mesa-va-drivers: needed for AMD VAAPI. Mesa >= 20.1 is required for HEVC transcoding. RUN apt-get update \ && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg wget apt-transport-https \ && wget -O - https://repo.jellyfin.org/jellyfin_team.gpg.key | apt-key add - \ @@ -39,6 +46,20 @@ RUN apt-get update \ jellyfin-ffmpeg \ openssl \ locales \ +# Intel VAAPI Tone mapping dependencies: +# Prefer NEO to Beignet since the latter one doesn't support Comet Lake or newer for now. +# Do not use the intel-opencl-icd package from repo since they will not build with RELEASE_WITH_REGKEYS enabled. + && mkdir intel-compute-runtime \ + && cd intel-compute-runtime \ + && wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-gmmlib_${GMMLIB_VERSION}_amd64.deb \ + && wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-${IGC_VERSION}/intel-igc-core_${IGC_VERSION}_amd64.deb \ + && wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-${IGC_VERSION}/intel-igc-opencl_${IGC_VERSION}_amd64.deb \ + && wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-opencl_${NEO_VERSION}_amd64.deb \ + && wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-ocloc_${NEO_VERSION}_amd64.deb \ + && wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-level-zero-gpu_${LEVEL_ZERO_VERSION}_amd64.deb \ + && dpkg -i *.deb \ + && cd .. \ + && rm -rf intel-compute-runtime \ && apt-get remove gnupg wget apt-transport-https -y \ && apt-get clean autoclean -y \ && apt-get autoremove -y \ diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs index b93651746..27f1fdaba 100644 --- a/Emby.Dlna/ContentDirectory/ControlHandler.cs +++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs @@ -1681,7 +1681,6 @@ namespace Emby.Dlna.ContentDirectory private ServerItem GetItemFromObjectId(string id) { return DidlBuilder.IsIdRoot(id) - ? new ServerItem(_libraryManager.GetUserRootFolder()) : ParseItemId(id); } diff --git a/Emby.Dlna/Eventing/DlnaEventManager.cs b/Emby.Dlna/Eventing/DlnaEventManager.cs index b6e45c50e..ff81e83b5 100644 --- a/Emby.Dlna/Eventing/DlnaEventManager.cs +++ b/Emby.Dlna/Eventing/DlnaEventManager.cs @@ -72,7 +72,8 @@ namespace Emby.Dlna.Eventing Id = id, CallbackUrl = callbackUrl, SubscriptionTime = DateTime.UtcNow, - TimeoutSeconds = timeout + TimeoutSeconds = timeout, + NotificationType = notificationType }); return GetEventSubscriptionResponse(id, requestedTimeoutString, timeout); diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index f8a00efac..fb4454a34 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; @@ -134,20 +136,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 +157,7 @@ namespace Emby.Dlna.Main if (options.EnableServer) { - await StartDevicePublisher(options).ConfigureAwait(false); + StartDevicePublisher(options); } else { @@ -225,7 +227,7 @@ namespace Emby.Dlna.Main } } - public async Task StartDevicePublisher(Configuration.DlnaOptions options) + public void StartDevicePublisher(Configuration.DlnaOptions options) { if (!options.BlastAliveMessages) { @@ -245,7 +247,7 @@ namespace Emby.Dlna.Main SupportPnpRootDevice = false }; - await RegisterServerEndpoints().ConfigureAwait(false); + RegisterServerEndpoints(); _publisher.StartBroadcastingAliveMessages(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds)); } @@ -255,14 +257,22 @@ namespace Emby.Dlna.Main } } - private async Task RegisterServerEndpoints() + private void RegisterServerEndpoints() { - var addresses = await _appHost.GetLocalIpAddresses().ConfigureAwait(false); - var udn = CreateUuid(_appHost.SystemId); var descriptorUri = "/dlna/" + udn + "/description.xml"; - foreach (var address in addresses) + var bindAddresses = NetworkManager.CreateCollection( + _networkManager.GetInternalBindAddresses() + .Where(i => i.AddressFamily == AddressFamily.InterNetwork || (i.AddressFamily == AddressFamily.InterNetworkV6 && i.Address.ScopeId != 0))); + + if (bindAddresses.Count == 0) + { + // No interfaces returned, so use loopback. + bindAddresses = _networkManager.GetLoopbacks(); + } + + foreach (IPNetAddress address in bindAddresses) { if (address.AddressFamily == AddressFamily.InterNetworkV6) { @@ -271,7 +281,7 @@ namespace Emby.Dlna.Main } // Limit to LAN addresses only - if (!_networkManager.IsAddressInSubnets(address, true, true)) + if (!_networkManager.IsInLocalNetwork(address)) { continue; } @@ -280,14 +290,14 @@ namespace Emby.Dlna.Main _logger.LogInformation("Registering publisher for {0} on {1}", fullService, address); - var uri = new Uri(_appHost.GetLocalApiUrl(address) + descriptorUri); + var uri = new Uri(_appHost.GetSmartApiUrl(address.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 = address.Address, + PrefixLength = address.PrefixLength, FriendlyName = "Jellyfin", Manufacturer = "Jellyfin", ModelName = "Jellyfin Server", diff --git a/Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs b/Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs index 1dc9c79c1..56788ae22 100644 --- a/Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs +++ b/Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using Emby.Dlna.Common; -using MediaBrowser.Model.Dlna; namespace Emby.Dlna.MediaReceiverRegistrar { diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs index f8ff03076..938ce5fbf 100644 --- a/Emby.Dlna/PlayTo/Device.cs +++ b/Emby.Dlna/PlayTo/Device.cs @@ -12,8 +12,6 @@ using System.Xml; using System.Xml.Linq; using Emby.Dlna.Common; using Emby.Dlna.Ssdp; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; using Microsoft.Extensions.Logging; namespace Emby.Dlna.PlayTo @@ -345,7 +343,7 @@ namespace Emby.Dlna.PlayTo RestartTimer(true); } - private string CreateDidlMeta(string value) + private static string CreateDidlMeta(string value) { if (string.IsNullOrEmpty(value)) { @@ -962,7 +960,7 @@ namespace Emby.Dlna.PlayTo url = "/dmr/" + url; } - if (!url.StartsWith("/", StringComparison.Ordinal)) + if (!url.StartsWith('/')) { url = "/" + url; } diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs index 3907b2a39..b7cd91a5c 100644 --- a/Emby.Dlna/PlayTo/PlayToController.cs +++ b/Emby.Dlna/PlayTo/PlayToController.cs @@ -9,7 +9,6 @@ using System.Threading.Tasks; using Emby.Dlna.Didl; using Jellyfin.Data.Entities; using Jellyfin.Data.Events; -using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; @@ -41,7 +40,6 @@ namespace Emby.Dlna.PlayTo private readonly IUserDataManager _userDataManager; private readonly ILocalizationManager _localization; private readonly IMediaSourceManager _mediaSourceManager; - private readonly IConfigurationManager _config; private readonly IMediaEncoder _mediaEncoder; private readonly IDeviceDiscovery _deviceDiscovery; @@ -68,7 +66,6 @@ namespace Emby.Dlna.PlayTo IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, - IConfigurationManager config, IMediaEncoder mediaEncoder) { _session = session; @@ -84,7 +81,6 @@ namespace Emby.Dlna.PlayTo _userDataManager = userDataManager; _localization = localization; _mediaSourceManager = mediaSourceManager; - _config = config; _mediaEncoder = mediaEncoder; } @@ -337,25 +333,17 @@ namespace Emby.Dlna.PlayTo } var startIndex = command.StartIndex ?? 0; + int len = items.Count - startIndex; if (startIndex > 0) { - items = items.GetRange(startIndex, items.Count - startIndex); + items = items.GetRange(startIndex, len); } - var playlist = new List<PlaylistItem>(); - var isFirst = true; - - foreach (var item in items) + var playlist = new PlaylistItem[len]; + playlist[0] = CreatePlaylistItem(items[0], user, command.StartPositionTicks.Value, command.MediaSourceId, command.AudioStreamIndex, command.SubtitleStreamIndex); + for (int i = 1; i < len; i++) { - if (isFirst && command.StartPositionTicks.HasValue) - { - playlist.Add(CreatePlaylistItem(item, user, command.StartPositionTicks.Value, command.MediaSourceId, command.AudioStreamIndex, command.SubtitleStreamIndex)); - isFirst = false; - } - else - { - playlist.Add(CreatePlaylistItem(item, user, 0, null, null, null)); - } + playlist[i] = CreatePlaylistItem(items[i], user, 0, null, null, null); } _logger.LogDebug("{0} - Playlist created", _session.DeviceName); @@ -468,8 +456,8 @@ namespace Emby.Dlna.PlayTo _dlnaManager.GetDefaultProfile(); var mediaSources = item is IHasMediaSources - ? _mediaSourceManager.GetStaticMediaSources(item, true, user) - : new List<MediaSourceInfo>(); + ? _mediaSourceManager.GetStaticMediaSources(item, true, user).ToArray() + : Array.Empty<MediaSourceInfo>(); var playlistItem = GetPlaylistItem(item, mediaSources, profile, _session.DeviceId, mediaSourceId, audioStreamIndex, subtitleStreamIndex); playlistItem.StreamInfo.StartPositionTicks = startPostionTicks; @@ -548,7 +536,7 @@ namespace Emby.Dlna.PlayTo return null; } - private PlaylistItem GetPlaylistItem(BaseItem item, List<MediaSourceInfo> mediaSources, DeviceProfile profile, string deviceId, string mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex) + private PlaylistItem GetPlaylistItem(BaseItem item, MediaSourceInfo[] mediaSources, DeviceProfile profile, string deviceId, string mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex) { if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) { @@ -557,7 +545,7 @@ namespace Emby.Dlna.PlayTo StreamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildVideoItem(new VideoOptions { ItemId = item.Id, - MediaSources = mediaSources.ToArray(), + MediaSources = mediaSources, Profile = profile, DeviceId = deviceId, MaxBitrate = profile.MaxStreamingBitrate, @@ -577,7 +565,7 @@ namespace Emby.Dlna.PlayTo StreamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildAudioItem(new AudioOptions { ItemId = item.Id, - MediaSources = mediaSources.ToArray(), + MediaSources = mediaSources, Profile = profile, DeviceId = deviceId, MaxBitrate = profile.MaxStreamingBitrate, @@ -590,7 +578,7 @@ namespace Emby.Dlna.PlayTo if (string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase)) { - return new PlaylistItemFactory().Create((Photo)item, profile); + return PlaylistItemFactory.Create((Photo)item, profile); } throw new ArgumentException("Unrecognized item type."); @@ -774,13 +762,14 @@ namespace Emby.Dlna.PlayTo private async Task SeekAfterTransportChange(long positionTicks, CancellationToken cancellationToken) { - const int maxWait = 15000000; - const int interval = 500; + const int MaxWait = 15000000; + const int Interval = 500; + var currentWait = 0; - while (_device.TransportState != TransportState.Playing && currentWait < maxWait) + while (_device.TransportState != TransportState.Playing && currentWait < MaxWait) { - await Task.Delay(interval).ConfigureAwait(false); - currentWait += interval; + await Task.Delay(Interval).ConfigureAwait(false); + currentWait += Interval; } await _device.Seek(TimeSpan.FromTicks(positionTicks), cancellationToken).ConfigureAwait(false); diff --git a/Emby.Dlna/PlayTo/PlayToManager.cs b/Emby.Dlna/PlayTo/PlayToManager.cs index e93aef304..f34332d62 100644 --- a/Emby.Dlna/PlayTo/PlayToManager.cs +++ b/Emby.Dlna/PlayTo/PlayToManager.cs @@ -3,13 +3,11 @@ using System; using System.Globalization; using System.Linq; -using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Events; using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dlna; @@ -92,10 +90,10 @@ namespace Emby.Dlna.PlayTo string location = info.Location.ToString(); // It has to report that it's a media renderer - if (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1 && - nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1) + if (!usn.Contains("MediaRenderer:", StringComparison.OrdinalIgnoreCase) + && !nt.Contains("MediaRenderer:", StringComparison.OrdinalIgnoreCase)) { - // _logger.LogDebug("Upnp device {0} does not contain a MediaRenderer device (0).", location); + _logger.LogDebug("Upnp device {0} does not contain a MediaRenderer device (0).", location); return; } @@ -130,24 +128,36 @@ namespace Emby.Dlna.PlayTo } } - private static string GetUuid(string usn) + internal static string GetUuid(string usn) { const string UuidStr = "uuid:"; const string UuidColonStr = "::"; var index = usn.IndexOf(UuidStr, StringComparison.OrdinalIgnoreCase); + if (index == -1) + { + return usn.GetMD5().ToString("N", CultureInfo.InvariantCulture); + } + + ReadOnlySpan<char> tmp = usn.AsSpan()[(index + UuidStr.Length)..]; + + index = tmp.IndexOf(UuidColonStr, StringComparison.OrdinalIgnoreCase); if (index != -1) { - return usn.Substring(index + UuidStr.Length); + tmp = tmp[..index]; } - index = usn.IndexOf(UuidColonStr, StringComparison.OrdinalIgnoreCase); + index = tmp.IndexOf('{'); if (index != -1) { - usn = usn.Substring(0, index + UuidColonStr.Length); + int endIndex = tmp.IndexOf('}'); + if (endIndex != -1) + { + tmp = tmp[(index + 1)..endIndex]; + } } - return usn.GetMD5().ToString("N", CultureInfo.InvariantCulture); + return tmp.ToString(); } private async Task AddDevice(UpnpDeviceInfo info, string location, CancellationToken cancellationToken) @@ -177,15 +187,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, @@ -201,7 +203,6 @@ namespace Emby.Dlna.PlayTo _userDataManager, _localization, _mediaSourceManager, - _config, _mediaEncoder); sessionInfo.AddController(controller); diff --git a/Emby.Dlna/PlayTo/PlaylistItemFactory.cs b/Emby.Dlna/PlayTo/PlaylistItemFactory.cs index bedc8b9ad..e28840a89 100644 --- a/Emby.Dlna/PlayTo/PlaylistItemFactory.cs +++ b/Emby.Dlna/PlayTo/PlaylistItemFactory.cs @@ -8,9 +8,9 @@ using MediaBrowser.Model.Session; namespace Emby.Dlna.PlayTo { - public class PlaylistItemFactory + public static class PlaylistItemFactory { - public PlaylistItem Create(Photo item, DeviceProfile profile) + public static PlaylistItem Create(Photo item, DeviceProfile profile) { var playlistItem = new PlaylistItem { diff --git a/Emby.Dlna/PlayTo/SsdpHttpClient.cs b/Emby.Dlna/PlayTo/SsdpHttpClient.cs index f4d793790..557bc69a7 100644 --- a/Emby.Dlna/PlayTo/SsdpHttpClient.cs +++ b/Emby.Dlna/PlayTo/SsdpHttpClient.cs @@ -4,7 +4,6 @@ using System; using System.Globalization; using System.IO; using System.Net.Http; -using System.Net.Http.Headers; using System.Net.Mime; using System.Text; using System.Threading; @@ -60,7 +59,7 @@ namespace Emby.Dlna.PlayTo return serviceUrl; } - if (!serviceUrl.StartsWith("/", StringComparison.Ordinal)) + if (!serviceUrl.StartsWith('/')) { serviceUrl = "/" + serviceUrl; } diff --git a/Emby.Dlna/PlayTo/TransportCommands.cs b/Emby.Dlna/PlayTo/TransportCommands.cs index fda17a8b4..0865968ad 100644 --- a/Emby.Dlna/PlayTo/TransportCommands.cs +++ b/Emby.Dlna/PlayTo/TransportCommands.cs @@ -78,7 +78,7 @@ namespace Emby.Dlna.PlayTo private static StateVariable FromXml(XElement container) { - var allowedValues = new List<string>(); + var allowedValues = Array.Empty<string>(); var element = container.Descendants(UPnpNamespaces.Svc + "allowedValueList") .FirstOrDefault(); @@ -86,14 +86,14 @@ namespace Emby.Dlna.PlayTo { var values = element.Descendants(UPnpNamespaces.Svc + "allowedValue"); - allowedValues.AddRange(values.Select(child => child.Value)); + allowedValues = values.Select(child => child.Value).ToArray(); } return new StateVariable { Name = container.GetValue(UPnpNamespaces.Svc + "name"), DataType = container.GetValue(UPnpNamespaces.Svc + "dataType"), - AllowedValues = allowedValues.ToArray() + AllowedValues = allowedValues }; } @@ -103,12 +103,12 @@ namespace Emby.Dlna.PlayTo foreach (var arg in action.ArgumentList) { - if (arg.Direction == "out") + if (string.Equals(arg.Direction, "out", StringComparison.Ordinal)) { continue; } - if (arg.Name == "InstanceID") + if (string.Equals(arg.Name, "InstanceID", StringComparison.Ordinal)) { stateString += BuildArgumentXml(arg, "0"); } @@ -127,12 +127,12 @@ namespace Emby.Dlna.PlayTo foreach (var arg in action.ArgumentList) { - if (arg.Direction == "out") + if (string.Equals(arg.Direction, "out", StringComparison.Ordinal)) { continue; } - if (arg.Name == "InstanceID") + if (string.Equals(arg.Name, "InstanceID", StringComparison.Ordinal)) { stateString += BuildArgumentXml(arg, "0"); } @@ -151,7 +151,7 @@ namespace Emby.Dlna.PlayTo foreach (var arg in action.ArgumentList) { - if (arg.Name == "InstanceID") + if (string.Equals(arg.Name, "InstanceID", StringComparison.Ordinal)) { stateString += BuildArgumentXml(arg, "0"); } diff --git a/Emby.Dlna/Properties/AssemblyInfo.cs b/Emby.Dlna/Properties/AssemblyInfo.cs index a2c1e0db8..606ffcf4f 100644 --- a/Emby.Dlna/Properties/AssemblyInfo.cs +++ b/Emby.Dlna/Properties/AssemblyInfo.cs @@ -1,5 +1,6 @@ using System.Reflection; using System.Resources; +using System.Runtime.CompilerServices; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information @@ -13,6 +14,7 @@ using System.Resources; [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: NeutralResourcesLanguage("en")] +[assembly: InternalsVisibleTo("Jellyfin.Dlna.Tests")] // Version information for an assembly consists of the following four values: // diff --git a/Emby.Dlna/Server/DescriptionXmlBuilder.cs b/Emby.Dlna/Server/DescriptionXmlBuilder.cs index bca9e81cd..09525aae4 100644 --- a/Emby.Dlna/Server/DescriptionXmlBuilder.cs +++ b/Emby.Dlna/Server/DescriptionXmlBuilder.cs @@ -40,8 +40,6 @@ namespace Emby.Dlna.Server _serverId = serverId; } - private static bool EnableAbsoluteUrls => false; - public string GetXml() { var builder = new StringBuilder(); @@ -75,13 +73,6 @@ namespace Emby.Dlna.Server builder.Append("<minor>0</minor>"); builder.Append("</specVersion>"); - if (!EnableAbsoluteUrls) - { - builder.Append("<URLBase>") - .Append(SecurityElement.Escape(_serverAddress)) - .Append("</URLBase>"); - } - AppendDeviceInfo(builder); builder.Append("</root>"); @@ -257,14 +248,7 @@ namespace Emby.Dlna.Server return string.Empty; } - url = url.TrimStart('/'); - - url = "/dlna/" + _serverUdn + "/" + url; - - if (EnableAbsoluteUrls) - { - url = _serverAddress.TrimEnd('/') + url; - } + url = _serverAddress.TrimEnd('/') + "/dlna/" + _serverUdn + "/" + url.TrimStart('/'); return SecurityElement.Escape(url); } diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs index 5f83355c8..fd1677473 100644 --- a/Emby.Naming/Video/VideoListResolver.cs +++ b/Emby.Naming/Video/VideoListResolver.cs @@ -227,7 +227,11 @@ namespace Emby.Naming.Video testFilename = cleanName.ToString(); } - testFilename = testFilename.Substring(folderName.Length).Trim(); + if (folderName.Length <= testFilename.Length) + { + testFilename = testFilename.Substring(folderName.Length).Trim(); + } + return string.IsNullOrEmpty(testFilename) || testFilename[0].Equals('-') || testFilename[0].Equals('_') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 5d47d1e40..5498f5a10 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1,14 +1,12 @@ #pragma warning disable CS1591 using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Net; using System.Net.Http; -using System.Net.Sockets; using System.Reflection; using System.Runtime.InteropServices; using System.Security.Cryptography.X509Certificates; @@ -46,10 +44,11 @@ using Emby.Server.Implementations.SyncPlay; using Emby.Server.Implementations.TV; using Emby.Server.Implementations.Updates; using Jellyfin.Api.Helpers; +using Jellyfin.Networking.Configuration; +using Jellyfin.Networking.Manager; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Events; -using MediaBrowser.Common.Json; using MediaBrowser.Common.Net; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Updates; @@ -82,7 +81,6 @@ using MediaBrowser.Controller.SyncPlay; using MediaBrowser.Controller.TV; using MediaBrowser.LocalMetadata.Savers; using MediaBrowser.MediaEncoding.BdInfo; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Globalization; @@ -97,6 +95,7 @@ using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Plugins.Tmdb; 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,14 +116,12 @@ 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 IJsonSerializer _jsonSerializer; private readonly IStartupOptions _startupOptions; private IMediaEncoder _mediaEncoder; private ISessionManager _sessionManager; - private IHttpClientFactory _httpClientFactory; private string[] _urlPrefixes; /// <summary> @@ -159,6 +156,11 @@ namespace Emby.Server.Implementations } /// <summary> + /// Gets the <see cref="INetworkManager"/> singleton instance. + /// </summary> + public INetworkManager NetManager { get; internal set; } + + /// <summary> /// Occurs when [has pending restart changed]. /// </summary> public event EventHandler HasPendingRestartChanged; @@ -210,7 +212,7 @@ namespace Emby.Server.Implementations private readonly List<IDisposable> _disposableParts = new List<IDisposable>(); /// <summary> - /// Gets the configuration manager. + /// Gets or sets the configuration manager. /// </summary> /// <value>The configuration manager.</value> protected IConfigurationManager ConfigurationManager { get; set; } @@ -243,14 +245,12 @@ namespace Emby.Server.Implementations /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param> /// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param> /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> - /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param> /// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param> public ApplicationHost( IServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IStartupOptions options, IFileSystem fileSystem, - INetworkManager networkManager, IServiceCollection serviceCollection) { _xmlSerializer = new MyXmlSerializer(); @@ -258,14 +258,17 @@ namespace Emby.Server.Implementations ServiceCollection = serviceCollection; - _networkManager = networkManager; - networkManager.LocalSubnetsFn = GetConfiguredLocalSubnets; - ApplicationPaths = applicationPaths; LoggerFactory = loggerFactory; _fileSystemManager = fileSystem; ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager); + // Have to migrate settings here as migration subsystem not yet initialised. + MigrateNetworkConfiguration(); + + // Have to pre-register the NetworkConfigurationFactory, as the configuration sub-system is not yet initialised. + ConfigurationManager.RegisterConfiguration<NetworkConfigurationFactory>(); + NetManager = new NetworkManager((IServerConfigurationManager)ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>()); Logger = LoggerFactory.CreateLogger<ApplicationHost>(); @@ -279,8 +282,6 @@ namespace Emby.Server.Implementations fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem)); - _networkManager.NetworkChanged += OnNetworkChanged; - CertificateInfo = new CertificateInfo { Path = ServerConfigurationManager.Configuration.CertificatePath, @@ -293,6 +294,22 @@ namespace Emby.Server.Implementations ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString; } + /// <summary> + /// Temporary function to migration network settings out of system.xml and into network.xml. + /// TODO: remove at the point when a fixed migration path has been decided upon. + /// </summary> + private void MigrateNetworkConfiguration() + { + string path = Path.Combine(ConfigurationManager.CommonApplicationPaths.ConfigurationDirectoryPath, "network.xml"); + if (!File.Exists(path)) + { + var networkSettings = new NetworkConfiguration(); + ClassMigrationHelper.CopyProperties(ServerConfigurationManager.Configuration, networkSettings); + _xmlSerializer.SerializeToFile(networkSettings, path); + Logger?.LogDebug("Successfully migrated network settings."); + } + } + public string ExpandVirtualPath(string path) { var appPaths = ApplicationPaths; @@ -309,16 +326,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(); - } - /// <inheritdoc /> public Version ApplicationVersion { get; } @@ -485,14 +492,15 @@ namespace Emby.Server.Implementations /// <inheritdoc/> public void Init() { - HttpPort = ServerConfigurationManager.Configuration.HttpServerPortNumber; - HttpsPort = ServerConfigurationManager.Configuration.HttpsPortNumber; + var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration(); + HttpPort = networkConfiguration.HttpServerPortNumber; + HttpsPort = networkConfiguration.HttpsPortNumber; // Safeguard against invalid configuration if (HttpPort == HttpsPort) { - HttpPort = ServerConfiguration.DefaultHttpPort; - HttpsPort = ServerConfiguration.DefaultHttpsPort; + HttpPort = NetworkConfiguration.DefaultHttpPort; + HttpsPort = NetworkConfiguration.DefaultHttpsPort; } DiscoverTypes(); @@ -521,7 +529,7 @@ namespace Emby.Server.Implementations ServiceCollection.AddSingleton(_fileSystemManager); ServiceCollection.AddSingleton<TmdbClientManager>(); - ServiceCollection.AddSingleton(_networkManager); + ServiceCollection.AddSingleton(NetManager); ServiceCollection.AddSingleton<IIsoManager, IsoManager>(); @@ -625,7 +633,6 @@ namespace Emby.Server.Implementations ServiceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>(); - ServiceCollection.AddSingleton<IResourceFileManager, ResourceFileManager>(); ServiceCollection.AddSingleton<EncodingHelper>(); ServiceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>(); @@ -647,7 +654,6 @@ namespace Emby.Server.Implementations _mediaEncoder = Resolve<IMediaEncoder>(); _sessionManager = Resolve<ISessionManager>(); - _httpClientFactory = Resolve<IHttpClientFactory>(); ((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize(); @@ -902,9 +908,10 @@ namespace Emby.Server.Implementations // Don't do anything if these haven't been set yet if (HttpPort != 0 && HttpsPort != 0) { + var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration(); // Need to restart if ports have changed - if (ServerConfigurationManager.Configuration.HttpServerPortNumber != HttpPort || - ServerConfigurationManager.Configuration.HttpsPortNumber != HttpsPort) + if (networkConfiguration.HttpServerPortNumber != HttpPort || + networkConfiguration.HttpsPortNumber != HttpsPort) { if (ServerConfigurationManager.Configuration.IsPortAuthorized) { @@ -1034,7 +1041,7 @@ namespace Emby.Server.Implementations metafile = dir.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries)[^1]; int versionIndex = dir.LastIndexOf('_'); - if (versionIndex != -1 && Version.TryParse(dir.Substring(versionIndex + 1), out Version parsedVersion)) + if (versionIndex != -1 && Version.TryParse(dir.AsSpan()[(versionIndex + 1)..], out Version parsedVersion)) { // Versioned folder. versions.Add(new LocalPlugin(Guid.Empty, metafile, parsedVersion, dir)); @@ -1147,6 +1154,9 @@ namespace Emby.Server.Implementations // Xbmc yield return typeof(ArtistNfoProvider).Assembly; + // Network + yield return typeof(NetworkManager).Assembly; + foreach (var i in GetAssembliesWithPartsInternal()) { yield return i; @@ -1158,13 +1168,10 @@ namespace Emby.Server.Implementations /// <summary> /// Gets the system status. /// </summary> - /// <param name="cancellationToken">The cancellation token.</param> + /// <param name="source">Where this request originated.</param> /// <returns>SystemInfo.</returns> - public async Task<SystemInfo> GetSystemInfo(CancellationToken cancellationToken) + public SystemInfo GetSystemInfo(IPAddress source) { - var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false); - var transcodingTempPath = ConfigurationManager.GetTranscodePath(); - return new SystemInfo { HasPendingRestart = HasPendingRestart, @@ -1184,9 +1191,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, @@ -1195,14 +1202,12 @@ namespace Emby.Server.Implementations } public IEnumerable<WakeOnLanInfo> GetWakeOnLanInfo() - => _networkManager.GetMacAddresses() + => NetManager.GetMacAddresses() .Select(i => new WakeOnLanInfo(i)) .ToList(); - public async Task<PublicSystemInfo> GetPublicSystemInfo(CancellationToken cancellationToken) + public PublicSystemInfo GetPublicSystemInfo(IPAddress source) { - var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false); - return new PublicSystemInfo { Version = ApplicationVersionString, @@ -1210,193 +1215,98 @@ namespace Emby.Server.Implementations Id = SystemId, OperatingSystem = OperatingSystem.Id.ToString(), ServerName = FriendlyName, - LocalAddress = localAddress, + LocalAddress = GetSmartApiUrl(source), StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted }; } /// <inheritdoc/> - public bool ListenWithHttps => Certificate != null && ServerConfigurationManager.Configuration.EnableHttps; + public bool ListenWithHttps => Certificate != null && ServerConfigurationManager.GetNetworkConfiguration().EnableHttps; /// <inheritdoc/> - public async Task<string> GetLocalApiUrl(CancellationToken cancellationToken) + public string GetSmartApiUrl(IPAddress ipAddress, int? port = null) { - try + // Published server ends with a / + if (_startupOptions.PublishedServerUrl != null) { - // 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]); + // Published server ends with a '/', so we need to remove it. + return _startupOptions.PublishedServerUrl.ToString().Trim('/'); } - catch (Exception ex) + + string smart = NetManager.GetBindInterface(ipAddress, out port); + // If the smartAPI doesn't start with http then treat it as a host or ip. + if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase)) { - Logger.LogError(ex, "Error getting local Ip address information"); + return smart.Trim('/'); } - return null; + return GetLocalApiUrl(smart.Trim('/'), null, port); } - /// <summary> - /// Removes the scope id from IPv6 addresses. - /// </summary> - /// <param name="address">The IPv6 address.</param> - /// <returns>The IPv6 address without the scope id.</returns> - private ReadOnlySpan<char> RemoveScopeId(ReadOnlySpan<char> address) + /// <inheritdoc/> + public string GetSmartApiUrl(HttpRequest request, int? port = null) { - var index = address.IndexOf('%'); - if (index == -1) + // Published server ends with a / + if (_startupOptions.PublishedServerUrl != null) { - return address; + // Published server ends with a '/', so we need to remove it. + return _startupOptions.PublishedServerUrl.ToString().Trim('/'); } - return address.Slice(0, index); - } - - /// <inheritdoc /> - public string GetLocalApiUrl(IPAddress ipAddress) - { - if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6) + string smart = NetManager.GetBindInterface(request, out port); + // If the smartAPI doesn't start with http then treat it as a host or ip. + if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase)) { - var str = RemoveScopeId(ipAddress.ToString()); - Span<char> span = new char[str.Length + 2]; - span[0] = '['; - str.CopyTo(span.Slice(1)); - span[^1] = ']'; - - return GetLocalApiUrl(span); + return smart.Trim('/'); } - return GetLocalApiUrl(ipAddress.ToString()); + return GetLocalApiUrl(smart.Trim('/'), request.Scheme, port); } /// <inheritdoc/> - public string GetLoopbackHttpApiUrl() - { - return GetLocalApiUrl("127.0.0.1", Uri.UriSchemeHttp, HttpPort); - } - - /// <inheritdoc/> - public string GetLocalApiUrl(ReadOnlySpan<char> 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(), - Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort), - Path = ServerConfigurationManager.Configuration.BaseUrl - }.ToString().TrimEnd('/'); - } - - public Task<List<IPAddress>> GetLocalIpAddresses(CancellationToken cancellationToken) + public string GetSmartApiUrl(string hostname, int? port = null) { - return GetLocalIpAddressesInternal(true, 0, cancellationToken); - } - - private async Task<List<IPAddress>> 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) + // Published server ends with a / + if (_startupOptions.PublishedServerUrl != null) { - addresses.AddRange(_networkManager.GetLocalIpAddresses()); + // Published server ends with a '/', so we need to remove it. + return _startupOptions.PublishedServerUrl.ToString().Trim('/'); } - var resultList = new List<IPAddress>(); + string smart = NetManager.GetBindInterface(hostname, out port); - foreach (var address in addresses) + // If the smartAPI doesn't start with http then treat it as a host or ip. + if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase)) { - 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 smart.Trim('/'); } - return resultList; + return GetLocalApiUrl(smart.Trim('/'), null, port); } - public IPAddress NormalizeConfiguredLocalAddress(ReadOnlySpan<char> address) + /// <inheritdoc/> + public string GetLoopbackHttpApiUrl() { - var index = address.Trim('/').IndexOf('/'); - if (index != -1) + if (NetManager.IsIP6Enabled) { - address = address.Slice(index + 1); + return GetLocalApiUrl("::1", Uri.UriSchemeHttp, HttpPort); } - if (IPAddress.TryParse(address.Trim('/'), out IPAddress result)) - { - return result; - } - - return null; + return GetLocalApiUrl("127.0.0.1", Uri.UriSchemeHttp, HttpPort); } - private readonly ConcurrentDictionary<string, bool> _validAddressResults = new ConcurrentDictionary<string, bool>(StringComparer.OrdinalIgnoreCase); - - private async Task<bool> IsLocalIpAddressValidAsync(IPAddress address, CancellationToken cancellationToken) + /// <inheritdoc/> + public string GetLocalApiUrl(string host, string scheme = null, int? port = null) { - 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(cancellationToken).ConfigureAwait(false); - var result = await System.Text.Json.JsonSerializer.DeserializeAsync<string>(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) + // 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 { - Logger.LogDebug(ex, "Ping test result to {0}. Success: {1}", apiUrl, false); - - _validAddressResults.AddOrUpdate(apiUrl, false, (k, v) => false); - return false; - } + Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp), + Host = host, + Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort), + Path = ServerConfigurationManager.GetNetworkConfiguration().BaseUrl + }.ToString().TrimEnd('/'); } public string FriendlyName => diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs index 3d97a6ca8..57684a429 100644 --- a/Emby.Server.Implementations/Channels/ChannelManager.cs +++ b/Emby.Server.Implementations/Channels/ChannelManager.cs @@ -540,18 +540,18 @@ namespace Emby.Server.Implementations.Channels { IncludeItemTypes = new[] { nameof(Channel) }, OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) } - }).Select(i => GetChannelFeatures(i.ToString("N", CultureInfo.InvariantCulture))).ToArray(); + }).Select(i => GetChannelFeatures(i)).ToArray(); } /// <inheritdoc /> - public ChannelFeatures GetChannelFeatures(string id) + public ChannelFeatures GetChannelFeatures(Guid? id) { - if (string.IsNullOrEmpty(id)) + if (!id.HasValue) { throw new ArgumentNullException(nameof(id)); } - var channel = GetChannel(id); + var channel = GetChannel(id.Value); var channelProvider = GetChannelProvider(channel); return GetChannelFeaturesDto(channel, channelProvider, channelProvider.GetChannelFeatures()); diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 7e01bd4b6..50c7a07d4 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -4519,17 +4519,17 @@ namespace Emby.Server.Implementations.Data if (query.HasImdbId.HasValue) { - whereClauses.Add("ProviderIds like '%imdb=%'"); + whereClauses.Add(GetProviderIdClause(query.HasImdbId.Value, "imdb")); } if (query.HasTmdbId.HasValue) { - whereClauses.Add("ProviderIds like '%tmdb=%'"); + whereClauses.Add(GetProviderIdClause(query.HasTmdbId.Value, "tmdb")); } if (query.HasTvdbId.HasValue) { - whereClauses.Add("ProviderIds like '%tvdb=%'"); + whereClauses.Add(GetProviderIdClause(query.HasTvdbId.Value, "tvdb")); } var includedItemByNameTypes = GetItemByNameTypesInQuery(query).SelectMany(MapIncludeItemTypes).ToList(); @@ -4769,6 +4769,21 @@ namespace Emby.Server.Implementations.Data return whereClauses; } + /// <summary> + /// Formats a where clause for the specified provider. + /// </summary> + /// <param name="includeResults">Whether or not to include items with this provider's ids.</param> + /// <param name="provider">Provider name.</param> + /// <returns>Formatted SQL clause.</returns> + private string GetProviderIdClause(bool includeResults, string provider) + { + return string.Format( + CultureInfo.InvariantCulture, + "ProviderIds {0} like '%{1}=%'", + includeResults ? string.Empty : "not", + provider); + } + private List<string> GetItemByNameTypesInQuery(InternalItemsQuery query) { var list = new List<string>(); diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index d360bb00f..91c4648c6 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -22,7 +22,6 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="IPNetwork2" Version="2.5.226" /> <PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" /> <PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" /> <PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" /> @@ -37,8 +36,8 @@ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" /> <PackageReference Include="Mono.Nat" Version="3.0.1" /> - <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.0" /> - <PackageReference Include="ServiceStack.Text.Core" Version="5.10.0" /> + <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.1" /> + <PackageReference Include="ServiceStack.Text.Core" Version="5.10.2" /> <PackageReference Include="sharpcompress" Version="0.26.0" /> <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" /> <PackageReference Include="DotNet.Glob" Version="3.1.0" /> diff --git a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs index 2e8cc76d2..14201ead2 100644 --- a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs +++ b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs @@ -8,6 +8,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Events; +using Jellyfin.Networking.Configuration; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Plugins; @@ -56,7 +57,7 @@ namespace Emby.Server.Implementations.EntryPoints private string GetConfigIdentifier() { const char Separator = '|'; - var config = _config.Configuration; + var config = _config.GetNetworkConfiguration(); return new StringBuilder(32) .Append(config.EnableUPnP).Append(Separator) @@ -93,7 +94,8 @@ namespace Emby.Server.Implementations.EntryPoints private void Start() { - if (!_config.Configuration.EnableUPnP || !_config.Configuration.EnableRemoteAccess) + var config = _config.GetNetworkConfiguration(); + if (!config.EnableUPnP || !config.EnableRemoteAccess) { return; } @@ -156,11 +158,12 @@ namespace Emby.Server.Implementations.EntryPoints private IEnumerable<Task> CreatePortMaps(INatDevice device) { - yield return CreatePortMap(device, _appHost.HttpPort, _config.Configuration.PublicPort); + var config = _config.GetNetworkConfiguration(); + yield return CreatePortMap(device, _appHost.HttpPort, config.PublicPort); if (_appHost.ListenWithHttps) { - yield return CreatePortMap(device, _appHost.HttpsPort, _config.Configuration.PublicHttpsPort); + yield return CreatePortMap(device, _appHost.HttpsPort, config.PublicHttpsPort); } } diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs index df7a034e8..4a0fc8239 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs @@ -1,5 +1,6 @@ #pragma warning disable CS1591 +using System; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Net; @@ -20,9 +21,15 @@ namespace Emby.Server.Implementations.HttpServer.Security public AuthorizationInfo Authenticate(HttpRequest request) { var auth = _authorizationContext.GetAuthorizationInfo(request); + + if (!auth.HasToken) + { + throw new AuthenticationException("Request does not contain a token."); + } + if (!auth.IsAuthenticated) { - throw new AuthenticationException("Invalid token."); + throw new SecurityException("Invalid token."); } if (auth.User?.HasPermission(PermissionKind.IsDisabled) ?? false) diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs index fdf2e3908..d62e2eefe 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs @@ -102,7 +102,8 @@ namespace Emby.Server.Implementations.HttpServer.Security DeviceId = deviceId, Version = version, Token = token, - IsAuthenticated = false + IsAuthenticated = false, + HasToken = false }; if (string.IsNullOrWhiteSpace(token)) @@ -111,6 +112,7 @@ namespace Emby.Server.Implementations.HttpServer.Security return authInfo; } + authInfo.HasToken = true; var result = _authRepo.Get(new AuthenticationInfoQuery { AccessToken = token diff --git a/Emby.Server.Implementations/Library/ImageFetcherPostScanTask.cs b/Emby.Server.Implementations/Library/ImageFetcherPostScanTask.cs new file mode 100644 index 000000000..d4e790c9a --- /dev/null +++ b/Emby.Server.Implementations/Library/ImageFetcherPostScanTask.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Concurrent; +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Data.Events; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Net; +using Microsoft.Extensions.Logging; + +namespace Emby.Server.Implementations.Library +{ + /// <summary> + /// A library post scan/refresh task for pre-fetching remote images. + /// </summary> + public class ImageFetcherPostScanTask : ILibraryPostScanTask + { + private readonly ILibraryManager _libraryManager; + private readonly IProviderManager _providerManager; + private readonly ILogger<ImageFetcherPostScanTask> _logger; + private readonly SemaphoreSlim _imageFetcherLock; + + private ConcurrentDictionary<Guid, (BaseItem item, ItemUpdateType updateReason)> _queuedItems; + + /// <summary> + /// Initializes a new instance of the <see cref="ImageFetcherPostScanTask"/> class. + /// </summary> + /// <param name="libraryManager">An instance of <see cref="ILibraryManager"/>.</param> + /// <param name="providerManager">An instance of <see cref="IProviderManager"/>.</param> + /// <param name="logger">An instance of <see cref="ILogger{ImageFetcherPostScanTask}"/>.</param> + public ImageFetcherPostScanTask( + ILibraryManager libraryManager, + IProviderManager providerManager, + ILogger<ImageFetcherPostScanTask> logger) + { + _libraryManager = libraryManager; + _providerManager = providerManager; + _logger = logger; + _queuedItems = new ConcurrentDictionary<Guid, (BaseItem item, ItemUpdateType updateReason)>(); + _imageFetcherLock = new SemaphoreSlim(1, 1); + _libraryManager.ItemAdded += OnLibraryManagerItemAddedOrUpdated; + _libraryManager.ItemUpdated += OnLibraryManagerItemAddedOrUpdated; + _providerManager.RefreshCompleted += OnProviderManagerRefreshCompleted; + } + + /// <inheritdoc /> + public async Task Run(IProgress<double> progress, CancellationToken cancellationToken) + { + // Sometimes a library scan will cause this to run twice if there's an item refresh going on. + await _imageFetcherLock.WaitAsync(cancellationToken).ConfigureAwait(false); + + try + { + var now = DateTime.UtcNow; + var itemGuids = _queuedItems.Keys.ToList(); + + for (var i = 0; i < itemGuids.Count; i++) + { + if (!_queuedItems.TryGetValue(itemGuids[i], out var queuedItem)) + { + continue; + } + + var itemId = queuedItem.item.Id.ToString("N", CultureInfo.InvariantCulture); + var itemType = queuedItem.item.GetType(); + _logger.LogDebug( + "Updating remote images for item {ItemId} with media type {ItemMediaType}", + itemId, + itemType); + try + { + await _libraryManager.UpdateImagesAsync(queuedItem.item, queuedItem.updateReason >= ItemUpdateType.ImageUpdate).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to fetch images for {Type} item with id {ItemId}", itemType, itemId); + } + + _queuedItems.TryRemove(queuedItem.item.Id, out _); + } + + if (itemGuids.Count > 0) + { + _logger.LogInformation( + "Finished updating/pre-fetching {NumberOfImages} images. Elapsed time: {TimeElapsed}s.", + itemGuids.Count.ToString(CultureInfo.InvariantCulture), + (DateTime.UtcNow - now).TotalSeconds.ToString(CultureInfo.InvariantCulture)); + } + else + { + _logger.LogDebug("No images were updated."); + } + } + finally + { + _imageFetcherLock.Release(); + } + } + + private void OnLibraryManagerItemAddedOrUpdated(object sender, ItemChangeEventArgs itemChangeEventArgs) + { + if (!_queuedItems.ContainsKey(itemChangeEventArgs.Item.Id) && itemChangeEventArgs.Item.ImageInfos.Length > 0) + { + _queuedItems.AddOrUpdate( + itemChangeEventArgs.Item.Id, + (itemChangeEventArgs.Item, itemChangeEventArgs.UpdateReason), + (key, existingValue) => existingValue); + } + } + + private void OnProviderManagerRefreshCompleted(object sender, GenericEventArgs<BaseItem> e) + { + if (!_queuedItems.ContainsKey(e.Argument.Id) && e.Argument.ImageInfos.Length > 0) + { + _queuedItems.AddOrUpdate( + e.Argument.Id, + (e.Argument, ItemUpdateType.None), + (key, existingValue) => existingValue); + } + + // The RefreshCompleted event is a bit awkward in that it seems to _only_ be fired on + // the item that was refreshed regardless of children refreshes. So we take it as a signal + // that the refresh is entirely completed. + Run(null, CancellationToken.None).GetAwaiter().GetResult(); + } + } +} diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 8ffb05e1c..5b926b0f4 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -858,7 +858,21 @@ namespace Emby.Server.Implementations.Library /// <returns>Task{Person}.</returns> public Person GetPerson(string name) { - return CreateItemByName<Person>(Person.GetPath, name, new DtoOptions(true)); + var path = Person.GetPath(name); + var id = GetItemByNameId<Person>(path); + if (!(GetItemById(id) is Person item)) + { + item = new Person + { + Name = name, + Id = id, + DateCreated = DateTime.UtcNow, + DateModified = DateTime.UtcNow, + Path = path + }; + } + + return item; } /// <summary> @@ -1941,19 +1955,9 @@ namespace Emby.Server.Implementations.Library } /// <inheritdoc /> - public async Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) + public Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) { - foreach (var item in items) - { - if (item.IsFileProtocol) - { - ProviderManager.SaveMetadata(item, updateReason); - } - - item.DateLastSaved = DateTime.UtcNow; - - await UpdateImagesAsync(item, updateReason >= ItemUpdateType.ImageUpdate).ConfigureAwait(false); - } + RunMetadataSavers(items, updateReason); _itemRepository.SaveItems(items, cancellationToken); @@ -1984,12 +1988,27 @@ namespace Emby.Server.Implementations.Library } } } + + return Task.CompletedTask; } /// <inheritdoc /> public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) => UpdateItemsAsync(new[] { item }, parent, updateReason, cancellationToken); + public void RunMetadataSavers(IReadOnlyList<BaseItem> items, ItemUpdateType updateReason) + { + foreach (var item in items) + { + if (item.IsFileProtocol) + { + ProviderManager.SaveMetadata(item, updateReason); + } + + item.DateLastSaved = DateTime.UtcNow; + } + } + /// <summary> /// Reports the item removed. /// </summary> @@ -2443,9 +2462,19 @@ namespace Emby.Server.Implementations.Library public BaseItem GetParentItem(string parentId, Guid? userId) { - if (!string.IsNullOrEmpty(parentId)) + if (string.IsNullOrEmpty(parentId)) + { + return GetParentItem((Guid?)null, userId); + } + + return GetParentItem(new Guid(parentId), userId); + } + + public BaseItem GetParentItem(Guid? parentId, Guid? userId) + { + if (parentId.HasValue) { - return GetItemById(new Guid(parentId)); + return GetItemById(parentId.Value); } if (userId.HasValue && userId != Guid.Empty) diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs index c850e3a08..1d9529dff 100644 --- a/Emby.Server.Implementations/Library/SearchEngine.cs +++ b/Emby.Server.Implementations/Library/SearchEngine.cs @@ -156,8 +156,8 @@ namespace Emby.Server.Implementations.Library ExcludeItemTypes = excludeItemTypes.ToArray(), IncludeItemTypes = includeItemTypes.ToArray(), Limit = query.Limit, - IncludeItemsByName = string.IsNullOrEmpty(query.ParentId), - ParentId = string.IsNullOrEmpty(query.ParentId) ? Guid.Empty : new Guid(query.ParentId), + IncludeItemsByName = !query.ParentId.HasValue, + ParentId = query.ParentId ?? Guid.Empty, OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }, Recursive = true, diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index 5d17ba1de..1084ddf74 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; -using System.IO; using System.Linq; using System.Net; using System.Net.Http; @@ -19,7 +18,6 @@ using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.LiveTv; -using MediaBrowser.Model.Net; using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; @@ -36,6 +34,9 @@ namespace Emby.Server.Implementations.LiveTv.Listings private readonly IApplicationHost _appHost; private readonly ICryptoProvider _cryptoProvider; + private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>(); + private DateTime _lastErrorResponse; + public SchedulesDirect( ILogger<SchedulesDirect> logger, IJsonSerializer jsonSerializer, @@ -50,8 +51,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings _cryptoProvider = cryptoProvider; } - private string UserAgent => _appHost.ApplicationUserAgent; - /// <inheritdoc /> public string Name => "Schedules Direct"; @@ -307,7 +306,8 @@ namespace Emby.Server.Implementations.LiveTv.Listings if (details.contentRating != null && details.contentRating.Count > 0) { - info.OfficialRating = details.contentRating[0].code.Replace("TV", "TV-").Replace("--", "-"); + info.OfficialRating = details.contentRating[0].code.Replace("TV", "TV-", StringComparison.Ordinal) + .Replace("--", "-", StringComparison.Ordinal); var invalid = new[] { "N/A", "Approved", "Not Rated", "Passed" }; if (invalid.Contains(info.OfficialRating, StringComparer.OrdinalIgnoreCase)) @@ -450,7 +450,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings private async Task<List<ScheduleDirect.ShowImages>> GetImageForPrograms( ListingsProviderInfo info, - List<string> programIds, + IReadOnlyList<string> programIds, CancellationToken cancellationToken) { if (programIds.Count == 0) @@ -458,23 +458,21 @@ namespace Emby.Server.Implementations.LiveTv.Listings return new List<ScheduleDirect.ShowImages>(); } - var imageIdString = "["; - - foreach (var i in programIds) + StringBuilder str = new StringBuilder("[", 1 + (programIds.Count * 13)); + foreach (ReadOnlySpan<char> i in programIds) { - var imageId = i.Substring(0, 10); - - if (!imageIdString.Contains(imageId, StringComparison.Ordinal)) - { - imageIdString += "\"" + imageId + "\","; - } + str.Append('"') + .Append(i.Slice(0, 10)) + .Append("\","); } - imageIdString = imageIdString.TrimEnd(',') + "]"; + // Remove last , + str.Length--; + str.Append(']'); using var message = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/metadata/programs") { - Content = new StringContent(imageIdString, Encoding.UTF8, MediaTypeNames.Application.Json) + Content = new StringContent(str.ToString(), Encoding.UTF8, MediaTypeNames.Application.Json) }; try @@ -539,9 +537,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings return lineups; } - private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>(); - private DateTime _lastErrorResponse; - private async Task<string> GetToken(ListingsProviderInfo info, CancellationToken cancellationToken) { var username = info.Username; @@ -564,8 +559,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings return null; } - NameValuePair savedToken; - if (!_tokens.TryGetValue(username, out savedToken)) + if (!_tokens.TryGetValue(username, out NameValuePair savedToken)) { savedToken = new NameValuePair(); _tokens.TryAdd(username, savedToken); @@ -647,13 +641,15 @@ namespace Emby.Server.Implementations.LiveTv.Listings { using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/token"); var hashedPasswordBytes = _cryptoProvider.ComputeHash("SHA1", Encoding.ASCII.GetBytes(password), Array.Empty<byte>()); - string hashedPassword = Hex.Encode(hashedPasswordBytes); + // TODO: remove ToLower when Convert.ToHexString supports lowercase + // Schedules Direct requires the hex to be lowercase + string hashedPassword = Convert.ToHexString(hashedPasswordBytes).ToLowerInvariant(); options.Content = new StringContent("{\"username\":\"" + username + "\",\"password\":\"" + hashedPassword + "\"}", Encoding.UTF8, MediaTypeNames.Application.Json); using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Token>(stream).ConfigureAwait(false); - if (root.message == "OK") + if (string.Equals(root.message, "OK", StringComparison.Ordinal)) { _logger.LogInformation("Authenticated with Schedules Direct token: " + root.token); return root.token; @@ -777,24 +773,28 @@ namespace Emby.Server.Implementations.LiveTv.Listings using var options = new HttpRequestMessage(HttpMethod.Get, ApiUrl + "/lineups/" + listingsId); options.Headers.TryAddWithoutValidation("token", token); - var list = new List<ChannelInfo>(); - using var httpResponse = await Send(options, true, info, cancellationToken).ConfigureAwait(false); await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Channel>(stream).ConfigureAwait(false); _logger.LogInformation("Found {ChannelCount} channels on the lineup on ScheduleDirect", root.map.Count); _logger.LogInformation("Mapping Stations to Channel"); - var allStations = root.stations ?? Enumerable.Empty<ScheduleDirect.Station>(); + var allStations = root.stations ?? new List<ScheduleDirect.Station>(); - foreach (ScheduleDirect.Map map in root.map) + var map = root.map; + int len = map.Count; + var array = new List<ChannelInfo>(len); + for (int i = 0; i < len; i++) { - var channelNumber = GetChannelNumber(map); + var channelNumber = GetChannelNumber(map[i]); - var station = allStations.FirstOrDefault(item => string.Equals(item.stationID, map.stationID, StringComparison.OrdinalIgnoreCase)); + var station = allStations.Find(item => string.Equals(item.stationID, map[i].stationID, StringComparison.OrdinalIgnoreCase)); if (station == null) { - station = new ScheduleDirect.Station { stationID = map.stationID }; + station = new ScheduleDirect.Station + { + stationID = map[i].stationID + }; } var channelInfo = new ChannelInfo @@ -810,32 +810,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings channelInfo.ImageUrl = station.logo.URL; } - list.Add(channelInfo); - } - - return list; - } - - private ScheduleDirect.Station GetStation(List<ScheduleDirect.Station> allStations, string channelNumber, string channelName) - { - if (!string.IsNullOrWhiteSpace(channelName)) - { - channelName = NormalizeName(channelName); - - var result = allStations.FirstOrDefault(i => string.Equals(NormalizeName(i.callsign ?? string.Empty), channelName, StringComparison.OrdinalIgnoreCase)); - - if (result != null) - { - return result; - } - } - - if (!string.IsNullOrWhiteSpace(channelNumber)) - { - return allStations.FirstOrDefault(i => string.Equals(NormalizeName(i.stationID ?? string.Empty), channelNumber, StringComparison.OrdinalIgnoreCase)); + array[i] = channelInfo; } - return null; + return array; } private static string NormalizeName(string value) @@ -1044,7 +1022,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings } } - // public class Title { public string title120 { get; set; } diff --git a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs index 8a0c0043a..3a738fd5d 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs @@ -76,7 +76,6 @@ namespace Emby.Server.Implementations.LiveTv } var list = sources.ToList(); - var serverUrl = await _appHost.GetLocalApiUrl(cancellationToken).ConfigureAwait(false); foreach (var source in list) { @@ -103,7 +102,7 @@ namespace Emby.Server.Implementations.LiveTv // Dummy this up so that direct play checks can still run if (string.IsNullOrEmpty(source.Path) && source.Protocol == MediaProtocol.Http) { - source.Path = serverUrl; + source.Path = _appHost.GetSmartApiUrl(string.Empty); } } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index c0a4d1228..b6444b172 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -237,8 +237,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun if (!inside) { - buffer[bufferIndex] = let; - bufferIndex++; + buffer[bufferIndex++] = let; } } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs index 63d41ec83..cf653f87d 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs @@ -3,7 +3,9 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Net; +using System.Net.NetworkInformation; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; @@ -50,6 +52,26 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun EnableStreamSharing = true; } + /// <summary> + /// Returns an unused UDP port number in the range specified. + /// Temporarily placed here until future network PR merged. + /// </summary> + /// <param name="range">Upper and Lower boundary of ports to select.</param> + /// <returns>System.Int32.</returns> + private static int GetUdpPortFromRange((int Min, int Max) range) + { + var properties = IPGlobalProperties.GetIPGlobalProperties(); + + // Get active udp listeners. + var udpListenerPorts = properties.GetActiveUdpListeners() + .Where(n => n.Port >= range.Min && n.Port <= range.Max) + .Select(n => n.Port); + + return Enumerable + .Range(range.Min, range.Max) + .FirstOrDefault(i => !udpListenerPorts.Contains(i)); + } + public override async Task Open(CancellationToken openCancellationToken) { LiveStreamCancellationTokenSource.Token.ThrowIfCancellationRequested(); @@ -57,7 +79,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun var mediaSource = OriginalMediaSource; var uri = new Uri(mediaSource.Path); - var localPort = _networkManager.GetRandomUnusedUdpPort(); + // Temporary code to reduce PR size. This will be updated by a future network pr. + var localPort = GetUdpPortFromRange((49152, 65535)); Directory.CreateDirectory(Path.GetDirectoryName(TempFilePath)); diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs index 1d6c26c13..c82b67b41 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs @@ -95,7 +95,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts extInf = line.Substring(ExtInfPrefix.Length).Trim(); _logger.LogInformation("Found m3u channel: {0}", extInf); } - else if (!string.IsNullOrWhiteSpace(extInf) && !line.StartsWith("#", StringComparison.OrdinalIgnoreCase)) + else if (!string.IsNullOrWhiteSpace(extInf) && !line.StartsWith('#')) { var channel = GetChannelnfo(extInf, tunerHostId, line); if (string.IsNullOrWhiteSpace(channel.Id)) diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs index 2de447ad9..f7507e6ba 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs @@ -63,7 +63,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts var extension = "ts"; var requiresRemux = false; - var contentType = response.Content.Headers.ContentType.ToString(); + var contentType = response.Content.Headers.ContentType?.ToString() ?? string.Empty; if (contentType.IndexOf("matroska", StringComparison.OrdinalIgnoreCase) != -1) { requiresRemux = true; diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json index b29ad94ef..4ee4eb989 100644 --- a/Emby.Server.Implementations/Localization/Core/da.json +++ b/Emby.Server.Implementations/Localization/Core/da.json @@ -113,5 +113,10 @@ "TaskCleanTranscodeDescription": "Fjern transcode filer som er mere end en dag gammel.", "TaskCleanTranscode": "Rengør Transcode Mappen", "TaskRefreshPeople": "Genopfrisk Personer", - "TaskRefreshPeopleDescription": "Opdatere metadata for skuespillere og instruktører i dit bibliotek." + "TaskRefreshPeopleDescription": "Opdatere metadata for skuespillere og instruktører i dit bibliotek.", + "TaskCleanActivityLogDescription": "Sletter linjer i aktivitetsloggen ældre end den konfigureret alder.", + "TaskCleanActivityLog": "Ryd Aktivitetslog", + "Undefined": "Udefineret", + "Forced": "Tvunget", + "Default": "Standard" } diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json index dcf3311cd..23d45b473 100644 --- a/Emby.Server.Implementations/Localization/Core/el.json +++ b/Emby.Server.Implementations/Localization/Core/el.json @@ -114,7 +114,7 @@ "TaskUpdatePluginsDescription": "Κατεβάζει και εγκαθιστά ενημερώσεις για τις προσθήκες που έχουν ρυθμιστεί για αυτόματη ενημέρωση.", "TaskUpdatePlugins": "Ενημέρωση Προσθηκών", "TaskRefreshPeopleDescription": "Ενημερώνει μεταδεδομένα για ηθοποιούς και σκηνοθέτες στην βιβλιοθήκη των πολυμέσων σας.", - "TaskCleanActivityLogDescription": "Διαγράφει καταχωρήσεις απο το αρχείο καταγραφής δραστηριοτήτων παλαιότερες από την ηλικία που έχει διαμορφωθεί.", + "TaskCleanActivityLogDescription": "Διαγράφει καταχωρήσεις απο το αρχείο καταγραφής παλαιότερες από την επιλεγμένη ηλικία.", "TaskCleanActivityLog": "Καθαρό Αρχείο Καταγραφής Δραστηριοτήτων", "Undefined": "Απροσδιόριστο", "Forced": "Εξαναγκασμένο", diff --git a/Emby.Server.Implementations/Localization/Core/es-MX.json b/Emby.Server.Implementations/Localization/Core/es-MX.json index ab54c0ea6..05181116d 100644 --- a/Emby.Server.Implementations/Localization/Core/es-MX.json +++ b/Emby.Server.Implementations/Localization/Core/es-MX.json @@ -113,5 +113,9 @@ "TasksChannelsCategory": "Canales de Internet", "TasksApplicationCategory": "Aplicación", "TasksLibraryCategory": "Biblioteca", - "TasksMaintenanceCategory": "Mantenimiento" + "TasksMaintenanceCategory": "Mantenimiento", + "TaskCleanActivityLogDescription": "Elimina entradas del registro de actividad que sean más antiguas al periodo establecido.", + "TaskCleanActivityLog": "Limpiar registro de actividades", + "Undefined": "Sin definir", + "Forced": "Forzado" } diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json index fe674cf36..16fde325f 100644 --- a/Emby.Server.Implementations/Localization/Core/es.json +++ b/Emby.Server.Implementations/Localization/Core/es.json @@ -70,7 +70,7 @@ "ScheduledTaskFailedWithName": "{0} falló", "ScheduledTaskStartedWithName": "{0} iniciada", "ServerNameNeedsToBeRestarted": "{0} necesita ser reiniciado", - "Shows": "Mostrar", + "Shows": "Series de Televisión", "Songs": "Canciones", "StartupEmbyServerIsLoading": "Jellyfin Server se está cargando. Vuelve a intentarlo en breve.", "SubtitleDownloadFailureForItem": "Error al descargar subtítulos para {0}", diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json index 8e219a9ce..61bef29ed 100644 --- a/Emby.Server.Implementations/Localization/Core/fi.json +++ b/Emby.Server.Implementations/Localization/Core/fi.json @@ -112,5 +112,7 @@ "TaskCleanCache": "Tyhjennä välimuisti-hakemisto", "TasksChannelsCategory": "Internet kanavat", "TasksApplicationCategory": "Sovellus", - "TasksLibraryCategory": "Kirjasto" + "TasksLibraryCategory": "Kirjasto", + "Forced": "Pakotettu", + "Default": "Oletus" } diff --git a/Emby.Server.Implementations/Localization/Core/fr-CA.json b/Emby.Server.Implementations/Localization/Core/fr-CA.json index 3d7592e3c..5aa65a525 100644 --- a/Emby.Server.Implementations/Localization/Core/fr-CA.json +++ b/Emby.Server.Implementations/Localization/Core/fr-CA.json @@ -113,5 +113,6 @@ "TaskCleanCache": "Nettoyer le répertoire des fichiers temporaires", "TasksApplicationCategory": "Application", "TaskCleanCacheDescription": "Supprime les fichiers temporaires qui ne sont plus nécessaire pour le système.", - "TasksChannelsCategory": "Canaux Internet" + "TasksChannelsCategory": "Canaux Internet", + "Default": "Par défaut" } diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json index 3d5d69f36..1e195378f 100644 --- a/Emby.Server.Implementations/Localization/Core/fr.json +++ b/Emby.Server.Implementations/Localization/Core/fr.json @@ -93,8 +93,8 @@ "ValueSpecialEpisodeName": "Spécial - {0}", "VersionNumber": "Version {0}", "TasksChannelsCategory": "Chaines en ligne", - "TaskDownloadMissingSubtitlesDescription": "Cherche les sous-titres manquant sur internet en se basant sur la configuration des métadonnées.", - "TaskDownloadMissingSubtitles": "Télécharger les sous-titres manquant", + "TaskDownloadMissingSubtitlesDescription": "Recherche les sous-titres manquants sur internet en se basant sur la configuration des métadonnées.", + "TaskDownloadMissingSubtitles": "Télécharger les sous-titres manquants", "TaskRefreshChannelsDescription": "Rafraîchit les informations des chaines en ligne.", "TaskRefreshChannels": "Rafraîchir les chaines", "TaskCleanTranscodeDescription": "Supprime les fichiers transcodés de plus d'un jour.", diff --git a/Emby.Server.Implementations/Localization/Core/id.json b/Emby.Server.Implementations/Localization/Core/id.json index ef3ed2580..105ef7be9 100644 --- a/Emby.Server.Implementations/Localization/Core/id.json +++ b/Emby.Server.Implementations/Localization/Core/id.json @@ -112,5 +112,10 @@ "TaskRefreshPeople": "Muat ulang Orang", "TaskCleanLogsDescription": "Menghapus file log yang lebih dari {0} hari.", "TaskCleanLogs": "Bersihkan Log Direktori", - "TaskRefreshLibrary": "Pindai Pustaka Media" + "TaskRefreshLibrary": "Pindai Pustaka Media", + "TaskCleanActivityLogDescription": "Menghapus log aktivitas yang lebih tua dari umur yang dikonfigurasi.", + "TaskCleanActivityLog": "Bersihkan Log Aktivitas", + "Undefined": "Tidak terdefinisi", + "Forced": "Dipaksa", + "Default": "Bawaan" } diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json index 9e37ddc27..110f8043d 100644 --- a/Emby.Server.Implementations/Localization/Core/it.json +++ b/Emby.Server.Implementations/Localization/Core/it.json @@ -115,5 +115,8 @@ "TasksLibraryCategory": "Libreria", "TasksMaintenanceCategory": "Manutenzione", "TaskCleanActivityLog": "Attività di Registro Completate", - "TaskCleanActivityLogDescription": "Elimina gli inserimenti nel registro delle attività più vecchie dell’età configurata." + "TaskCleanActivityLogDescription": "Elimina gli inserimenti nel registro delle attività più vecchie dell’età configurata.", + "Undefined": "Non Definito", + "Forced": "Forzato", + "Default": "Predefinito" } diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index e1e88cc9b..b6672a554 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -87,7 +87,7 @@ "UserOnlineFromDevice": "{0} heeft verbinding met {1}", "UserPasswordChangedWithName": "Wachtwoord voor {0} is gewijzigd", "UserPolicyUpdatedWithName": "Gebruikersbeleid gewijzigd voor {0}", - "UserStartedPlayingItemWithValues": "{0} heeft afspelen van {1} gestart op {2}", + "UserStartedPlayingItemWithValues": "{0} speelt {1} af op {2}", "UserStoppedPlayingItemWithValues": "{0} heeft afspelen van {1} gestopt op {2}", "ValueHasBeenAddedToLibrary": "{0} is toegevoegd aan je mediabibliotheek", "ValueSpecialEpisodeName": "Speciaal - {0}", @@ -115,5 +115,8 @@ "TasksLibraryCategory": "Bibliotheek", "TasksMaintenanceCategory": "Onderhoud", "TaskCleanActivityLogDescription": "Verwijder activiteiten logs ouder dan de ingestelde tijd.", - "TaskCleanActivityLog": "Leeg activiteiten logboek" + "TaskCleanActivityLog": "Leeg activiteiten logboek", + "Undefined": "Niet gedefinieerd", + "Forced": "Geforceerd", + "Default": "Standaard" } diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json index 90a4941c5..8c41edf96 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-PT.json +++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json @@ -113,5 +113,10 @@ "TasksChannelsCategory": "Canais da Internet", "TasksApplicationCategory": "Aplicação", "TasksLibraryCategory": "Biblioteca", - "TasksMaintenanceCategory": "Manutenção" + "TasksMaintenanceCategory": "Manutenção", + "TaskCleanActivityLogDescription": "Apaga as entradas do registo de atividade anteriores à data configurada.", + "TaskCleanActivityLog": "Limpar registo de atividade", + "Undefined": "Indefinido", + "Forced": "Forçado", + "Default": "Padrão" } diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json index 2079940cd..f1a78b2d3 100644 --- a/Emby.Server.Implementations/Localization/Core/pt.json +++ b/Emby.Server.Implementations/Localization/Core/pt.json @@ -112,5 +112,10 @@ "TaskUpdatePluginsDescription": "Download e instala as atualizações para plug-ins configurados para atualização automática.", "TaskRefreshPeopleDescription": "Atualiza os metadados para atores e diretores na tua biblioteca de media.", "TaskRefreshPeople": "Atualizar pessoas", - "TaskRefreshLibraryDescription": "Pesquisa a tua biblioteca de media por novos ficheiros e atualiza os metadados." + "TaskRefreshLibraryDescription": "Pesquisa a tua biblioteca de media por novos ficheiros e atualiza os metadados.", + "TaskCleanActivityLog": "Limpar registo de atividade", + "Undefined": "Indefinido", + "Forced": "Forçado", + "Default": "Predefinição", + "TaskCleanActivityLogDescription": "Apaga itens no registro com idade acima do que é configurado." } diff --git a/Emby.Server.Implementations/Localization/Core/ro.json b/Emby.Server.Implementations/Localization/Core/ro.json index 5e4022292..510aac11c 100644 --- a/Emby.Server.Implementations/Localization/Core/ro.json +++ b/Emby.Server.Implementations/Localization/Core/ro.json @@ -114,5 +114,8 @@ "TasksLibraryCategory": "Librărie", "TasksMaintenanceCategory": "Mentenanță", "TaskCleanActivityLogDescription": "Șterge intrările din jurnalul de activitate mai vechi de data configurată.", - "TaskCleanActivityLog": "Curăță Jurnalul de Activitate" + "TaskCleanActivityLog": "Curăță Jurnalul de Activitate", + "Undefined": "Nedefinit", + "Forced": "Forțat", + "Default": "Implicit" } diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json index c0db2cf7f..ca6172fce 100644 --- a/Emby.Server.Implementations/Localization/Core/ru.json +++ b/Emby.Server.Implementations/Localization/Core/ru.json @@ -115,5 +115,8 @@ "TaskRefreshChapterImagesDescription": "Создаются эскизы для видео, которые содержат сцены.", "TaskCleanCacheDescription": "Удаляются файлы кэша, которые больше не нужны системе.", "TaskCleanActivityLogDescription": "Удаляет записи журнала активности старше установленного возраста.", - "TaskCleanActivityLog": "Очистить журнал активности" + "TaskCleanActivityLog": "Очистить журнал активности", + "Undefined": "Не определено", + "Forced": "Форсир-ые", + "Default": "По умолчанию" } diff --git a/Emby.Server.Implementations/Localization/Core/sk.json b/Emby.Server.Implementations/Localization/Core/sk.json index 8e5026944..99fbd3954 100644 --- a/Emby.Server.Implementations/Localization/Core/sk.json +++ b/Emby.Server.Implementations/Localization/Core/sk.json @@ -2,7 +2,7 @@ "Albums": "Albumy", "AppDeviceValues": "Aplikácia: {0}, Zariadenie: {1}", "Application": "Aplikácia", - "Artists": "Umelci", + "Artists": "Interpreti", "AuthenticationSucceededWithUserName": "{0} úspešne overený", "Books": "Knihy", "CameraImageUploadedFrom": "Z {0} bola nahraná nová fotografia", @@ -15,13 +15,13 @@ "Favorites": "Obľúbené", "Folders": "Priečinky", "Genres": "Žánre", - "HeaderAlbumArtists": "Umelci albumu", + "HeaderAlbumArtists": "Interpreti albumu", "HeaderContinueWatching": "Pokračovať v pozeraní", "HeaderFavoriteAlbums": "Obľúbené albumy", - "HeaderFavoriteArtists": "Obľúbení umelci", + "HeaderFavoriteArtists": "Obľúbení interpreti", "HeaderFavoriteEpisodes": "Obľúbené epizódy", "HeaderFavoriteShows": "Obľúbené seriály", - "HeaderFavoriteSongs": "Obľúbené piesne", + "HeaderFavoriteSongs": "Obľúbené skladby", "HeaderLiveTV": "Živá TV", "HeaderNextUp": "Nasleduje", "HeaderRecordingGroups": "Skupiny nahrávok", @@ -33,13 +33,13 @@ "LabelRunningTimeValue": "Dĺžka: {0}", "Latest": "Najnovšie", "MessageApplicationUpdated": "Jellyfin Server bol aktualizovaný", - "MessageApplicationUpdatedTo": "Jellyfin Server bol aktualizový na verziu {0}", + "MessageApplicationUpdatedTo": "Jellyfin Server bol aktualizovaný na verziu {0}", "MessageNamedServerConfigurationUpdatedWithValue": "Sekcia {0} konfigurácie servera bola aktualizovaná", "MessageServerConfigurationUpdated": "Konfigurácia servera bola aktualizovaná", "MixedContent": "Zmiešaný obsah", "Movies": "Filmy", "Music": "Hudba", - "MusicVideos": "Hudobné videá", + "MusicVideos": "Hudobné videoklipy", "NameInstallFailed": "Inštalácia {0} zlyhala", "NameSeasonNumber": "Séria {0}", "NameSeasonUnknown": "Neznáma séria", @@ -71,7 +71,7 @@ "ScheduledTaskStartedWithName": "{0} zahájených", "ServerNameNeedsToBeRestarted": "{0} vyžaduje reštart", "Shows": "Seriály", - "Songs": "Piesne", + "Songs": "Skladby", "StartupEmbyServerIsLoading": "Jellyfin Server sa spúšťa. Prosím, skúste to o chvíľu znova.", "SubtitleDownloadFailureForItem": "Sťahovanie titulkov pre {0} zlyhalo", "SubtitleDownloadFailureFromForItem": "Sťahovanie titulkov z {0} pre {1} zlyhalo", @@ -89,29 +89,34 @@ "UserPolicyUpdatedWithName": "Používateľské zásady pre {0} boli aktualizované", "UserStartedPlayingItemWithValues": "{0} spustil prehrávanie {1} na {2}", "UserStoppedPlayingItemWithValues": "{0} ukončil prehrávanie {1} na {2}", - "ValueHasBeenAddedToLibrary": "{0} bol pridané do vašej knižnice médií", + "ValueHasBeenAddedToLibrary": "{0} bol pridaný do vašej knižnice médií", "ValueSpecialEpisodeName": "Špeciál - {0}", "VersionNumber": "Verzia {0}", "TaskDownloadMissingSubtitlesDescription": "Vyhľadá na internete chýbajúce titulky podľa toho, ako sú nakonfigurované metadáta.", "TaskDownloadMissingSubtitles": "Stiahnuť chýbajúce titulky", "TaskRefreshChannelsDescription": "Obnoví informácie o internetových kanáloch.", "TaskRefreshChannels": "Obnoviť kanály", - "TaskCleanTranscodeDescription": "Vymaže súbory transkódovania, ktoré sú staršie ako jeden deň.", - "TaskCleanTranscode": "Vyčistiť priečinok pre transkódovanie", + "TaskCleanTranscodeDescription": "Vymaže prekódované súbory, ktoré sú staršie ako jeden deň.", + "TaskCleanTranscode": "Vyčistiť priečinok pre prekódovanie", "TaskUpdatePluginsDescription": "Stiahne a nainštaluje aktualizácie pre zásuvné moduly, ktoré sú nastavené tak, aby sa aktualizovali automaticky.", "TaskUpdatePlugins": "Aktualizovať zásuvné moduly", "TaskRefreshPeopleDescription": "Aktualizuje metadáta pre hercov a režisérov vo vašej mediálnej knižnici.", "TaskRefreshPeople": "Obnoviť osoby", - "TaskCleanLogsDescription": "Vymaže log súbory, ktoré su staršie ako {0} deň/dni/dní.", + "TaskCleanLogsDescription": "Vymaže log súbory, ktoré sú staršie ako {0} deň/dni/dní.", "TaskCleanLogs": "Vyčistiť priečinok s logmi", "TaskRefreshLibraryDescription": "Hľadá vo vašej mediálnej knižnici nové súbory a obnovuje metadáta.", "TaskRefreshLibrary": "Prehľadávať knižnicu medií", "TaskRefreshChapterImagesDescription": "Vytvorí náhľady pre videá, ktoré majú kapitoly.", "TaskRefreshChapterImages": "Extrahovať obrázky kapitol", - "TaskCleanCacheDescription": "Vymaže cache súbory, ktoré nie sú už potrebné pre systém.", - "TaskCleanCache": "Vyčistiť Cache priečinok", + "TaskCleanCacheDescription": "Vymaže súbory vyrovnávacej pamäte, ktoré už nie sú potrebné pre systém.", + "TaskCleanCache": "Vyčistiť priečinok vyrovnávacej pamäte", "TasksChannelsCategory": "Internetové kanály", "TasksApplicationCategory": "Aplikácia", "TasksLibraryCategory": "Knižnica", - "TasksMaintenanceCategory": "Údržba" + "TasksMaintenanceCategory": "Údržba", + "TaskCleanActivityLogDescription": "Vymaže záznamy aktivít v logu, ktoré sú staršie ako zadaná doba.", + "TaskCleanActivityLog": "Vyčistiť log aktivít", + "Undefined": "Nedefinované", + "Forced": "Vynútené", + "Default": "Predvolené" } diff --git a/Emby.Server.Implementations/Localization/Core/ta.json b/Emby.Server.Implementations/Localization/Core/ta.json index 5fcdb1f74..c737ba42b 100644 --- a/Emby.Server.Implementations/Localization/Core/ta.json +++ b/Emby.Server.Implementations/Localization/Core/ta.json @@ -21,7 +21,7 @@ "Inherit": "மரபுரிமையாகப் பெறு", "HeaderRecordingGroups": "பதிவு குழுக்கள்", "Folders": "கோப்புறைகள்", - "FailedLoginAttemptWithUserName": "{0} இலிருந்து உள்நுழைவு முயற்சி தோல்வியடைந்தது", + "FailedLoginAttemptWithUserName": "{0} இல் இருந்து உள்நுழைவு முயற்சி தோல்வியடைந்தது", "DeviceOnlineWithName": "{0} இணைக்கப்பட்டது", "DeviceOfflineWithName": "{0} துண்டிக்கப்பட்டது", "Collections": "தொகுப்புகள்", @@ -99,7 +99,7 @@ "MessageNamedServerConfigurationUpdatedWithValue": "சேவையக உள்ளமைவு பிரிவு {0} புதுப்பிக்கப்பட்டது", "TaskCleanCacheDescription": "கணினிக்கு இனி தேவைப்படாத தற்காலிக கோப்புகளை நீக்கு.", "UserOfflineFromDevice": "{0} இலிருந்து {1} துண்டிக்கப்பட்டுள்ளது", - "SubtitleDownloadFailureFromForItem": "வசன வரிகள் {0} இலிருந்து {1} க்கு பதிவிறக்கத் தவறிவிட்டன", + "SubtitleDownloadFailureFromForItem": "வசன வரிகள் {0} இல் இருந்து {1} க்கு பதிவிறக்கத் தவறிவிட்டன", "TaskDownloadMissingSubtitlesDescription": "மீத்தரவு உள்ளமைவின் அடிப்படையில் வசன வரிகள் காணாமல் போனதற்கு இணையத்தைத் தேடுகிறது.", "TaskCleanTranscodeDescription": "ஒரு நாளைக்கு மேற்பட்ட பழைய டிரான்ஸ்கோட் கோப்புகளை நீக்குகிறது.", "TaskUpdatePluginsDescription": "தானாகவே புதுப்பிக்க கட்டமைக்கப்பட்ட உட்செருகிகளுக்கான புதுப்பிப்புகளை பதிவிறக்குகிறது மற்றும் நிறுவுகிறது.", diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json index 54d3a65f0..885663eed 100644 --- a/Emby.Server.Implementations/Localization/Core/tr.json +++ b/Emby.Server.Implementations/Localization/Core/tr.json @@ -12,7 +12,7 @@ "DeviceOfflineWithName": "{0} bağlantısı kesildi", "DeviceOnlineWithName": "{0} bağlı", "FailedLoginAttemptWithUserName": "{0} adresinden giriş başarısız oldu", - "Favorites": "Favoriler", + "Favorites": "Favorilerim", "Folders": "Klasörler", "Genres": "Türler", "HeaderAlbumArtists": "Albüm Sanatçıları", @@ -115,5 +115,7 @@ "TaskRefreshChapterImages": "Bölüm Resimlerini Çıkar", "TaskCleanCacheDescription": "Sistem tarafından artık ihtiyaç duyulmayan önbellek dosyalarını siler.", "TaskCleanActivityLog": "İşlem Günlüğünü Temizle", - "TaskCleanActivityLogDescription": "Belirtilen sureden daha eski etkinlik log kayıtları silindi." + "TaskCleanActivityLogDescription": "Belirtilen sureden daha eski etkinlik log kayıtları silindi.", + "Undefined": "Bilinmeyen", + "Default": "Varsayılan" } diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json index 06cc5f633..b6073bf6a 100644 --- a/Emby.Server.Implementations/Localization/Core/uk.json +++ b/Emby.Server.Implementations/Localization/Core/uk.json @@ -27,7 +27,7 @@ "Channels": "Канали", "CameraImageUploadedFrom": "Нова фотографія завантажена з {0}", "Books": "Книги", - "AuthenticationSucceededWithUserName": "{0} успішно авторизований", + "AuthenticationSucceededWithUserName": "{0} успішно автентифіковано", "Artists": "Виконавці", "Application": "Додаток", "AppDeviceValues": "Додаток: {0}, Пристрій: {1}", @@ -112,5 +112,10 @@ "MessageServerConfigurationUpdated": "Конфігурація сервера оновлена", "MessageNamedServerConfigurationUpdatedWithValue": "Розділ конфігурації сервера {0} оновлено", "Inherit": "Успадкувати", - "HeaderRecordingGroups": "Групи запису" + "HeaderRecordingGroups": "Групи запису", + "Forced": "Примусово", + "TaskCleanActivityLogDescription": "Видаляє старші за встановлений термін записи з журналу активності.", + "TaskCleanActivityLog": "Очистити журнал активності", + "Undefined": "Не визначено", + "Default": "За замовчуванням" } diff --git a/Emby.Server.Implementations/Localization/Core/vi.json b/Emby.Server.Implementations/Localization/Core/vi.json index 0549995c8..40368d464 100644 --- a/Emby.Server.Implementations/Localization/Core/vi.json +++ b/Emby.Server.Implementations/Localization/Core/vi.json @@ -16,7 +16,7 @@ "Albums": "Albums", "Artists": "Các Nghệ Sĩ", "TaskDownloadMissingSubtitlesDescription": "Tìm kiếm phụ đề bị thiếu trên Internet dựa trên cấu hình dữ liệu mô tả.", - "TaskDownloadMissingSubtitles": "Tải xuống phụ đề bị thiếu", + "TaskDownloadMissingSubtitles": "Tải Xuống Phụ Đề Bị Thiếu", "TaskRefreshChannelsDescription": "Làm mới thông tin kênh internet.", "TaskRefreshChannels": "Làm Mới Kênh", "TaskCleanTranscodeDescription": "Xóa các tệp chuyển mã cũ hơn một ngày.", @@ -24,11 +24,11 @@ "TaskUpdatePluginsDescription": "Tải xuống và cài đặt các bản cập nhật cho các plugin được định cấu hình để cập nhật tự động.", "TaskUpdatePlugins": "Cập Nhật Plugins", "TaskRefreshPeopleDescription": "Cập nhật thông tin chi tiết cho diễn viên và đạo diễn trong thư viện phương tiện của bạn.", - "TaskRefreshPeople": "Làm mới Người dùng", + "TaskRefreshPeople": "Làm Mới Người Dùng", "TaskCleanLogsDescription": "Xóa tập tin nhật ký cũ hơn {0} ngày.", - "TaskCleanLogs": "Làm sạch nhật ký", - "TaskRefreshLibraryDescription": "Quét thư viện phương tiện của bạn để tìm các tệp mới và làm mới thông tin chi tiết.", - "TaskRefreshLibrary": "Quét Thư viện Phương tiện", + "TaskCleanLogs": "Làm Sạch Thư Mục Nhật Ký", + "TaskRefreshLibraryDescription": "Quét thư viện phương tiện của bạn để tìm tệp mới và làm mới dữ liệu mô tả.", + "TaskRefreshLibrary": "Quét Thư Viện Phương Tiện", "TaskRefreshChapterImagesDescription": "Tạo hình thu nhỏ cho video có các phân cảnh.", "TaskRefreshChapterImages": "Trích Xuất Ảnh Phân Cảnh", "TaskCleanCacheDescription": "Xóa các tệp cache không còn cần thiết của hệ thống.", @@ -80,7 +80,7 @@ "NotificationOptionApplicationUpdateAvailable": "Bản cập nhật ứng dụng hiện sẵn có", "NewVersionIsAvailable": "Một phiên bản mới của Jellyfin Server sẵn có để tải.", "NameSeasonUnknown": "Không Rõ Mùa", - "NameSeasonNumber": "Mùa {0}", + "NameSeasonNumber": "Phần {0}", "NameInstallFailed": "{0} cài đặt thất bại", "MusicVideos": "Video Nhạc", "Music": "Nhạc", diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json index d2e3d77a3..6494c0b54 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-TW.json +++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json @@ -114,5 +114,8 @@ "TasksApplicationCategory": "應用程式", "TasksMaintenanceCategory": "維護", "TaskCleanActivityLogDescription": "刪除超過所設時間的活動紀錄。", - "TaskCleanActivityLog": "清除活動紀錄" + "TaskCleanActivityLog": "清除活動紀錄", + "Undefined": "未定義的", + "Forced": "強制", + "Default": "原本" } diff --git a/Emby.Server.Implementations/Localization/countries.json b/Emby.Server.Implementations/Localization/countries.json index 581e9f835..b08a3ae79 100644 --- a/Emby.Server.Implementations/Localization/countries.json +++ b/Emby.Server.Implementations/Localization/countries.json @@ -558,6 +558,12 @@ "TwoLetterISORegionName": "OM" }, { + "DisplayName": "Palestine", + "Name": "PS", + "ThreeLetterISORegionName": "PSE", + "TwoLetterISORegionName": "PS" + }, + { "DisplayName": "Panama", "Name": "PA", "ThreeLetterISORegionName": "PAN", diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs index 438bbe24a..f27305cbe 100644 --- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs +++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs @@ -12,6 +12,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; @@ -81,12 +82,7 @@ namespace Emby.Server.Implementations.MediaEncoder return false; } - if (video.VideoType == VideoType.Iso) - { - return false; - } - - if (video.VideoType == VideoType.BluRay || video.VideoType == VideoType.Dvd) + if (video.VideoType == VideoType.Dvd) { return false; } @@ -140,15 +136,19 @@ namespace Emby.Server.Implementations.MediaEncoder // Add some time for the first chapter to make sure we don't end up with a black image var time = chapter.StartPositionTicks == 0 ? TimeSpan.FromTicks(Math.Min(_firstChapterTicks, video.RunTimeTicks ?? 0)) : TimeSpan.FromTicks(chapter.StartPositionTicks); - var protocol = MediaProtocol.File; - - var inputPath = MediaEncoderHelpers.GetInputArgument(_fileSystem, video.Path, null, Array.Empty<string>()); + var inputPath = video.Path; Directory.CreateDirectory(Path.GetDirectoryName(path)); var container = video.Container; + var mediaSource = new MediaSourceInfo + { + VideoType = video.VideoType, + IsoType = video.IsoType, + Protocol = video.PathProtocol.Value, + }; - var tempFile = await _encoder.ExtractVideoImage(inputPath, container, protocol, video.GetDefaultVideoStream(), video.Video3DFormat, time, cancellationToken).ConfigureAwait(false); + var tempFile = await _encoder.ExtractVideoImage(inputPath, container, mediaSource, video.GetDefaultVideoStream(), video.Video3DFormat, time, cancellationToken).ConfigureAwait(false); File.Copy(tempFile, path, true); try diff --git a/Emby.Server.Implementations/Networking/NetworkManager.cs b/Emby.Server.Implementations/Networking/NetworkManager.cs deleted file mode 100644 index ff0a2a361..000000000 --- a/Emby.Server.Implementations/Networking/NetworkManager.cs +++ /dev/null @@ -1,566 +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 -{ - /// <summary> - /// Class to take care of network interface management. - /// </summary> - public class NetworkManager : INetworkManager - { - private readonly ILogger<NetworkManager> _logger; - private readonly object _localIpAddressSyncLock = new object(); - private readonly object _subnetLookupLock = new object(); - private readonly Dictionary<string, List<string>> _subnetLookup = new Dictionary<string, List<string>>(StringComparer.Ordinal); - - private IPAddress[] _localIpAddresses; - - private List<PhysicalAddress> _macAddresses; - - /// <summary> - /// Initializes a new instance of the <see cref="NetworkManager"/> class. - /// </summary> - /// <param name="logger">Logger to use for messages.</param> - public NetworkManager(ILogger<NetworkManager> logger) - { - _logger = logger; - - NetworkChange.NetworkAddressChanged += OnNetworkAddressChanged; - NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged; - } - - /// <inheritdoc/> - public event EventHandler NetworkChanged; - - /// <inheritdoc/> - public Func<string[]> 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); - } - - /// <inheritdoc/> - public IPAddress[] GetLocalIpAddresses() - { - lock (_localIpAddressSyncLock) - { - if (_localIpAddresses == null) - { - var addresses = GetLocalIpAddressesInternal().ToArray(); - - _localIpAddresses = addresses; - } - - return _localIpAddresses; - } - } - - private List<IPAddress> GetLocalIpAddressesInternal() - { - var list = GetIPsDefault().ToList(); - - if (list.Count == 0) - { - list = GetLocalIpAddressesFallback().GetAwaiter().GetResult().ToList(); - } - - var listClone = new List<IPAddress>(); - - 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(); - } - - /// <inheritdoc/> - 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; - } - - // GetAddressBytes - Span<byte> 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 - (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; - } - - /// <inheritdoc/> - 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<string> GetSubnets(string endpointFirstPart) - { - lock (_subnetLookupLock) - { - if (_subnetLookup.TryGetValue(endpointFirstPart, out var subnets)) - { - return subnets; - } - - subnets = new List<string>(); - - 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; - } - } - - /// <inheritdoc/> - public bool IsInLocalNetwork(string endpoint) - { - return IsInLocalNetworkInternal(endpoint, true); - } - - /// <inheritdoc/> - public bool IsAddressInSubnets(string addressString, string[] subnets) - { - return IsAddressInSubnets(IPAddress.Parse(addressString), addressString, subnets); - } - - /// <inheritdoc/> - public bool IsAddressInSubnets(IPAddress address, bool excludeInterfaces, bool excludeRFC) - { - // GetAddressBytes - Span<byte> 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 - { - // 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); - } - - /// <summary> - /// 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. - /// </summary> - /// <param name="address">IPAddress version of the address.</param> - /// <param name="addressString">The address to check.</param> - /// <param name="subnets">If true, check against addresses in the LAN settings which have [] arroud and return true if it matches the address give in address.</param> - /// <returns><c>false</c>if the address isn't in the subnets, <c>true</c> otherwise.</returns> - 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<IPAddress[]> GetIpAddresses(string hostName) - { - return Dns.GetHostAddressesAsync(hostName); - } - - private IEnumerable<IPAddress> GetIPsDefault() - { - IEnumerable<NetworkInterface> 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<IPAddress>(); - } - - 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<IEnumerable<IPAddress>> 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(); - } - - /// <summary> - /// Gets a random port number that is currently available. - /// </summary> - /// <returns>System.Int32.</returns> - public int GetRandomUnusedTcpPort() - { - var listener = new TcpListener(IPAddress.Any, 0); - listener.Start(); - var port = ((IPEndPoint)listener.LocalEndpoint).Port; - listener.Stop(); - return port; - } - - /// <inheritdoc/> - public int GetRandomUnusedUdpPort() - { - var localEndPoint = new IPEndPoint(IPAddress.Any, 0); - using (var udpClient = new UdpClient(localEndPoint)) - { - return ((IPEndPoint)udpClient.Client.LocalEndPoint).Port; - } - } - - /// <inheritdoc/> - public List<PhysicalAddress> GetMacAddresses() - { - return _macAddresses ??= GetMacAddressesInternal().ToList(); - } - - private static IEnumerable<PhysicalAddress> GetMacAddressesInternal() - => NetworkInterface.GetAllNetworkInterfaces() - .Where(i => i.NetworkInterfaceType != NetworkInterfaceType.Loopback) - .Select(x => x.GetPhysicalAddress()) - .Where(x => !x.Equals(PhysicalAddress.None)); - - /// <inheritdoc/> - 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) - { - int size = address.AddressFamily == AddressFamily.InterNetwork ? 4 : 16; - - // GetAddressBytes - Span<byte> ipAddressBytes = stackalloc byte[size]; - address.TryWriteBytes(ipAddressBytes, out _); - - // GetAddressBytes - Span<byte> subnetMaskBytes = stackalloc byte[size]; - subnetMask.TryWriteBytes(subnetMaskBytes, out _); - - if (ipAddressBytes.Length != subnetMaskBytes.Length) - { - throw new ArgumentException("Lengths of IP address and subnet mask do not match."); - } - - byte[] broadcastAddress = new byte[ipAddressBytes.Length]; - for (int i = 0; i < broadcastAddress.Length; i++) - { - broadcastAddress[i] = (byte)(ipAddressBytes[i] & subnetMaskBytes[i]); - } - - return new IPAddress(broadcastAddress); - } - - /// <inheritdoc/> - 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/QuickConnect/QuickConnectManager.cs b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs index 140a67541..7bed06de3 100644 --- a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs +++ b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs @@ -243,7 +243,7 @@ namespace Emby.Server.Implementations.QuickConnect Span<byte> bytes = stackalloc byte[length]; _rng.GetBytes(bytes); - return Hex.Encode(bytes); + return Convert.ToHexString(bytes); } /// <inheritdoc/> diff --git a/Emby.Server.Implementations/ResourceFileManager.cs b/Emby.Server.Implementations/ResourceFileManager.cs deleted file mode 100644 index 22fc62293..000000000 --- a/Emby.Server.Implementations/ResourceFileManager.cs +++ /dev/null @@ -1,45 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.IO; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.IO; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations -{ - public class ResourceFileManager : IResourceFileManager - { - private readonly IFileSystem _fileSystem; - private readonly ILogger<ResourceFileManager> _logger; - - public ResourceFileManager(ILogger<ResourceFileManager> logger, IFileSystem fileSystem) - { - _logger = logger; - _fileSystem = fileSystem; - } - - public string GetResourcePath(string basePath, string virtualPath) - { - var fullPath = Path.Combine(basePath, virtualPath.Replace('/', Path.DirectorySeparatorChar)); - - try - { - fullPath = Path.GetFullPath(fullPath); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error retrieving full path"); - } - - // Don't allow file system access outside of the source folder - if (!_fileSystem.ContainsSubPath(basePath, fullPath)) - { - throw new SecurityException("Access denied"); - } - - return fullPath; - } - } -} diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs index 26ef19354..b13fc7fc6 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs @@ -5,10 +5,10 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; -using MediaBrowser.Model.Globalization; namespace Emby.Server.Implementations.ScheduledTasks.Tasks { @@ -23,8 +23,12 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks private readonly ILocalizationManager _localization; /// <summary> - /// Initializes a new instance of the <see cref="DeleteTranscodeFileTask" /> class. + /// Initializes a new instance of the <see cref="DeleteTranscodeFileTask"/> class. /// </summary> + /// <param name="logger">Instance of the <see cref="ILogger{DeleteTranscodeFileTask}"/> interface.</param> + /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> + /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param> + /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param> public DeleteTranscodeFileTask( ILogger<DeleteTranscodeFileTask> logger, IFileSystem fileSystem, @@ -37,11 +41,42 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks _localization = localization; } + /// <inheritdoc /> + public string Name => _localization.GetLocalizedString("TaskCleanTranscode"); + + /// <inheritdoc /> + public string Description => _localization.GetLocalizedString("TaskCleanTranscodeDescription"); + + /// <inheritdoc /> + public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory"); + + /// <inheritdoc /> + public string Key => "DeleteTranscodeFiles"; + + /// <inheritdoc /> + public bool IsHidden => false; + + /// <inheritdoc /> + public bool IsEnabled => true; + + /// <inheritdoc /> + public bool IsLogged => true; + /// <summary> /// Creates the triggers that define when the task will run. /// </summary> /// <returns>IEnumerable{BaseTaskTrigger}.</returns> - public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() => new List<TaskTriggerInfo>(); + public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() + { + return new[] + { + new TaskTriggerInfo + { + Type = TaskTriggerInfo.TriggerInterval, + IntervalTicks = TimeSpan.FromHours(24).Ticks + } + }; + } /// <summary> /// Returns the task to be executed. @@ -131,26 +166,5 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks _logger.LogError(ex, "Error deleting file {path}", path); } } - - /// <inheritdoc /> - public string Name => _localization.GetLocalizedString("TaskCleanTranscode"); - - /// <inheritdoc /> - public string Description => _localization.GetLocalizedString("TaskCleanTranscodeDescription"); - - /// <inheritdoc /> - public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory"); - - /// <inheritdoc /> - public string Key => "DeleteTranscodeFiles"; - - /// <inheritdoc /> - public bool IsHidden => false; - - /// <inheritdoc /> - public bool IsEnabled => true; - - /// <inheritdoc /> - public bool IsLogged => true; } } diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs index ccd1446dd..447c587f9 100644 --- a/Emby.Server.Implementations/TV/TVSeriesManager.cs +++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs @@ -56,13 +56,11 @@ namespace Emby.Server.Implementations.TV return GetResult(GetNextUpEpisodes(request, user, new[] { presentationUniqueKey }, dtoOptions), request); } - var parentIdGuid = string.IsNullOrEmpty(request.ParentId) ? (Guid?)null : new Guid(request.ParentId); - BaseItem[] parents; - if (parentIdGuid.HasValue) + if (request.ParentId.HasValue) { - var parent = _libraryManager.GetItemById(parentIdGuid.Value); + var parent = _libraryManager.GetItemById(request.ParentId.Value); if (parent != null) { @@ -146,28 +144,10 @@ namespace Emby.Server.Implementations.TV var allNextUp = seriesKeys .Select(i => GetNextUp(i, currentUser, dtoOptions)); - // allNextUp = allNextUp.OrderByDescending(i => i.Item1); - - // If viewing all next up for all series, remove first episodes - // But if that returns empty, keep those first episodes (avoid completely empty view) - var alwaysEnableFirstEpisode = !string.IsNullOrEmpty(request.SeriesId); - var anyFound = false; - return allNextUp .Where(i => { - if (alwaysEnableFirstEpisode || i.Item1 != DateTime.MinValue) - { - anyFound = true; - return true; - } - - if (!anyFound && i.Item1 == DateTime.MinValue) - { - return true; - } - - return false; + return i.Item1 != DateTime.MinValue; }) .Select(i => i.Item2()) .Where(i => i != null); @@ -210,7 +190,7 @@ namespace Emby.Server.Implementations.TV Func<Episode> getEpisode = () => { - return _libraryManager.GetItemList(new InternalItemsQuery(user) + var nextEpisode = _libraryManager.GetItemList(new InternalItemsQuery(user) { AncestorWithPresentationUniqueKey = null, SeriesPresentationUniqueKey = seriesKey, @@ -223,6 +203,18 @@ namespace Emby.Server.Implementations.TV MinSortName = lastWatchedEpisode?.SortName, DtoOptions = dtoOptions }).Cast<Episode>().FirstOrDefault(); + + if (nextEpisode != null) + { + var userData = _userDataManager.GetUserData(user, nextEpisode); + + if (userData.PlaybackPositionTicks > 0) + { + return null; + } + } + + return nextEpisode; }; if (lastWatchedEpisode != null) diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs index b7a59cee2..4fd7ac0c1 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(((IPEndPoint)endpoint).Address); if (!string.IsNullOrEmpty(localUrl)) { diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index 7a071c071..ef346dd5d 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -6,13 +6,15 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; -using System.Runtime.Serialization; +using System.Net.Http.Json; using System.Security.Cryptography; +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; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Updates; @@ -21,8 +23,6 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Events; using MediaBrowser.Controller.Events.Updates; using MediaBrowser.Model.IO; -using MediaBrowser.Model.Net; -using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Updates; using Microsoft.Extensions.Logging; @@ -40,9 +40,9 @@ namespace Emby.Server.Implementations.Updates private readonly IApplicationPaths _appPaths; private readonly IEventManager _eventManager; private readonly IHttpClientFactory _httpClientFactory; - private readonly IJsonSerializer _jsonSerializer; private readonly IServerConfigurationManager _config; private readonly IFileSystem _fileSystem; + private readonly JsonSerializerOptions _jsonSerializerOptions; /// <summary> /// Gets the application host. @@ -70,7 +70,6 @@ namespace Emby.Server.Implementations.Updates IApplicationPaths appPaths, IEventManager eventManager, IHttpClientFactory httpClientFactory, - IJsonSerializer jsonSerializer, IServerConfigurationManager config, IFileSystem fileSystem, IZipClient zipClient) @@ -83,10 +82,10 @@ namespace Emby.Server.Implementations.Updates _appPaths = appPaths; _eventManager = eventManager; _httpClientFactory = httpClientFactory; - _jsonSerializer = jsonSerializer; _config = config; _fileSystem = fileSystem; _zipClient = zipClient; + _jsonSerializerOptions = JsonDefaults.GetOptions(); } /// <inheritdoc /> @@ -97,31 +96,29 @@ namespace Emby.Server.Implementations.Updates { try { - using var response = await _httpClientFactory.CreateClient(NamedClient.Default) - .GetAsync(new Uri(manifest), cancellationToken).ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - - try + var packages = await _httpClientFactory.CreateClient(NamedClient.Default) + .GetFromJsonAsync<List<PackageInfo>>(new Uri(manifest), _jsonSerializerOptions, cancellationToken).ConfigureAwait(false); + if (packages == null) { - var package = await _jsonSerializer.DeserializeFromStreamAsync<IList<PackageInfo>>(stream).ConfigureAwait(false); + return Array.Empty<PackageInfo>(); + } - // Store the repository and repository url with each version, as they may be spread apart. - foreach (var entry in package) + // 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) { - foreach (var ver in entry.versions) - { - ver.repositoryName = manifestName; - ver.repositoryUrl = manifest; - } + ver.repositoryName = manifestName; + ver.repositoryUrl = manifest; } - - return package; - } - catch (SerializationException ex) - { - _logger.LogError(ex, "Failed to deserialize the plugin manifest retrieved from {Manifest}", manifest); - return Array.Empty<PackageInfo>(); } + + return packages; + } + catch (JsonException ex) + { + _logger.LogError(ex, "Failed to deserialize the plugin manifest retrieved from {Manifest}", manifest); + return Array.Empty<PackageInfo>(); } catch (UriFormatException ex) { @@ -187,7 +184,13 @@ namespace Emby.Server.Implementations.Updates // 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)) { - var existing = FilterPackages(result, package.name, Guid.Parse(package.guid)).FirstOrDefault(); + 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(); if (existing != null) { // Assumption is both lists are ordered, so slot these into the correct place. @@ -411,7 +414,7 @@ namespace Emby.Server.Implementations.Updates using var md5 = MD5.Create(); cancellationToken.ThrowIfCancellationRequested(); - var hash = Hex.Encode(md5.ComputeHash(stream)); + var hash = Convert.ToHexString(md5.ComputeHash(stream)); if (!string.Equals(package.Checksum, hash, StringComparison.OrdinalIgnoreCase)) { _logger.LogError( diff --git a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs index 27a1f61be..c56233794 100644 --- a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs +++ b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs @@ -18,6 +18,7 @@ namespace Jellyfin.Api.Auth public class CustomAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions> { private readonly IAuthService _authService; + private readonly ILogger<CustomAuthenticationHandler> _logger; /// <summary> /// Initializes a new instance of the <see cref="CustomAuthenticationHandler" /> class. @@ -35,6 +36,7 @@ namespace Jellyfin.Api.Auth ISystemClock clock) : base(options, logger, encoder, clock) { _authService = authService; + _logger = logger.CreateLogger<CustomAuthenticationHandler>(); } /// <inheritdoc /> @@ -70,7 +72,8 @@ namespace Jellyfin.Api.Auth } catch (AuthenticationException ex) { - return Task.FromResult(AuthenticateResult.Fail(ex)); + _logger.LogDebug(ex, "Error authenticating with {Handler}", nameof(CustomAuthenticationHandler)); + return Task.FromResult(AuthenticateResult.NoResult()); } catch (SecurityException ex) { diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs index c65dc8620..fed7ed3e5 100644 --- a/Jellyfin.Api/Controllers/ArtistsController.cs +++ b/Jellyfin.Api/Controllers/ArtistsController.cs @@ -3,7 +3,6 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; -using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Entities; using MediaBrowser.Controller.Dto; @@ -87,7 +86,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] string? searchTerm, - [FromQuery] string? parentId, + [FromQuery] Guid? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, @@ -119,16 +118,11 @@ namespace Jellyfin.Api.Controllers .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); User? user = null; - BaseItem parentItem; + BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId); if (userId.HasValue && !userId.Equals(Guid.Empty)) { user = _userManager.GetUserById(userId.Value); - parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId); - } - else - { - parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId); } var query = new InternalItemsQuery(user) @@ -157,15 +151,15 @@ namespace Jellyfin.Api.Controllers EnableTotalRecordCount = enableTotalRecordCount }; - if (!string.IsNullOrWhiteSpace(parentId)) + if (parentId.HasValue) { if (parentItem is Folder) { - query.AncestorIds = new[] { new Guid(parentId) }; + query.AncestorIds = new[] { parentId.Value }; } else { - query.ItemIds = new[] { new Guid(parentId) }; + query.ItemIds = new[] { parentId.Value }; } } @@ -291,7 +285,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] string? searchTerm, - [FromQuery] string? parentId, + [FromQuery] Guid? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, @@ -323,16 +317,11 @@ namespace Jellyfin.Api.Controllers .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); User? user = null; - BaseItem parentItem; + BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId); if (userId.HasValue && !userId.Equals(Guid.Empty)) { user = _userManager.GetUserById(userId.Value); - parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId); - } - else - { - parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId); } var query = new InternalItemsQuery(user) @@ -361,15 +350,15 @@ namespace Jellyfin.Api.Controllers EnableTotalRecordCount = enableTotalRecordCount }; - if (!string.IsNullOrWhiteSpace(parentId)) + if (parentId.HasValue) { if (parentItem is Folder) { - query.AncestorIds = new[] { new Guid(parentId) }; + query.AncestorIds = new[] { parentId.Value }; } else { - query.ItemIds = new[] { new Guid(parentId) }; + query.ItemIds = new[] { parentId.Value }; } } diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index c22979495..616fe5b91 100644 --- a/Jellyfin.Api/Controllers/AudioController.cs +++ b/Jellyfin.Api/Controllers/AudioController.cs @@ -78,7 +78,7 @@ namespace Jellyfin.Api.Controllers /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param> /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param> /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param> - /// <param name="transcodingReasons">Optional. The transcoding reason.</param> + /// <param name="transcodeReasons">Optional. The transcoding reason.</param> /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param> /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param> /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param> @@ -134,7 +134,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? enableMpegtsM2TsMode, [FromQuery] string? videoCodec, [FromQuery] string? subtitleCodec, - [FromQuery] string? transcodingReasons, + [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, [FromQuery] EncodingContext? context, @@ -186,7 +186,7 @@ namespace Jellyfin.Api.Controllers EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, VideoCodec = videoCodec, SubtitleCodec = subtitleCodec, - TranscodeReasons = transcodingReasons, + TranscodeReasons = transcodeReasons, AudioStreamIndex = audioStreamIndex, VideoStreamIndex = videoStreamIndex, Context = context ?? EncodingContext.Static, @@ -243,7 +243,7 @@ namespace Jellyfin.Api.Controllers /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param> /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param> /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param> - /// <param name="transcodingReasons">Optional. The transcoding reason.</param> + /// <param name="transcodeReasons">Optional. The transcoding reason.</param> /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param> /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param> /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param> @@ -299,7 +299,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? enableMpegtsM2TsMode, [FromQuery] string? videoCodec, [FromQuery] string? subtitleCodec, - [FromQuery] string? transcodingReasons, + [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, [FromQuery] EncodingContext? context, @@ -351,7 +351,7 @@ namespace Jellyfin.Api.Controllers EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, VideoCodec = videoCodec, SubtitleCodec = subtitleCodec, - TranscodeReasons = transcodingReasons, + TranscodeReasons = transcodeReasons, AudioStreamIndex = audioStreamIndex, VideoStreamIndex = videoStreamIndex, Context = context ?? EncodingContext.Static, diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs index c4dc44cc3..b70c76e80 100644 --- a/Jellyfin.Api/Controllers/ChannelsController.cs +++ b/Jellyfin.Api/Controllers/ChannelsController.cs @@ -92,7 +92,7 @@ namespace Jellyfin.Api.Controllers /// <response code="200">Channel features returned.</response> /// <returns>An <see cref="OkResult"/> containing the channel features.</returns> [HttpGet("{channelId}/Features")] - public ActionResult<ChannelFeatures> GetChannelFeatures([FromRoute, Required] string channelId) + public ActionResult<ChannelFeatures> GetChannelFeatures([FromRoute, Required] Guid channelId) { return _channelManager.GetChannelFeatures(channelId); } diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs index 4e6455eaa..4fd9c2fbf 100644 --- a/Jellyfin.Api/Controllers/DlnaServerController.cs +++ b/Jellyfin.Api/Controllers/DlnaServerController.cs @@ -252,7 +252,7 @@ namespace Jellyfin.Api.Controllers private string GetAbsoluteUri() { - return $"{Request.Scheme}://{Request.Host}{Request.Path}"; + return $"{Request.Scheme}://{Request.Host}{Request.PathBase}{Request.Path}"; } private Task<ControlResponse> ProcessControlRequestInternalAsync(string id, Stream requestStream, IUpnpService service) diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index eff5bd54a..6e85737d2 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -160,7 +160,7 @@ namespace Jellyfin.Api.Controllers /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param> /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param> /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param> - /// <param name="transcodingReasons">Optional. The transcoding reason.</param> + /// <param name="transcodeReasons">Optional. The transcoding reason.</param> /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param> /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param> /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param> @@ -216,7 +216,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? enableMpegtsM2TsMode, [FromQuery] string? videoCodec, [FromQuery] string? subtitleCodec, - [FromQuery] string? transcodingReasons, + [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, [FromQuery] EncodingContext context, @@ -268,7 +268,7 @@ namespace Jellyfin.Api.Controllers EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, VideoCodec = videoCodec, SubtitleCodec = subtitleCodec, - TranscodeReasons = transcodingReasons, + TranscodeReasons = transcodeReasons, AudioStreamIndex = audioStreamIndex, VideoStreamIndex = videoStreamIndex, Context = context, @@ -326,7 +326,7 @@ namespace Jellyfin.Api.Controllers /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param> /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param> /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param> - /// <param name="transcodingReasons">Optional. The transcoding reason.</param> + /// <param name="transcodeReasons">Optional. The transcoding reason.</param> /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param> /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param> /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param> @@ -383,7 +383,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? enableMpegtsM2TsMode, [FromQuery] string? videoCodec, [FromQuery] string? subtitleCodec, - [FromQuery] string? transcodingReasons, + [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, [FromQuery] EncodingContext context, @@ -435,7 +435,7 @@ namespace Jellyfin.Api.Controllers EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, VideoCodec = videoCodec, SubtitleCodec = subtitleCodec, - TranscodeReasons = transcodingReasons, + TranscodeReasons = transcodeReasons, AudioStreamIndex = audioStreamIndex, VideoStreamIndex = videoStreamIndex, Context = context, @@ -492,7 +492,7 @@ namespace Jellyfin.Api.Controllers /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param> /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param> /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param> - /// <param name="transcodingReasons">Optional. The transcoding reason.</param> + /// <param name="transcodeReasons">Optional. The transcoding reason.</param> /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param> /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param> /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param> @@ -546,7 +546,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? enableMpegtsM2TsMode, [FromQuery] string? videoCodec, [FromQuery] string? subtitleCodec, - [FromQuery] string? transcodingReasons, + [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, [FromQuery] EncodingContext context, @@ -598,7 +598,7 @@ namespace Jellyfin.Api.Controllers EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, VideoCodec = videoCodec, SubtitleCodec = subtitleCodec, - TranscodeReasons = transcodingReasons, + TranscodeReasons = transcodeReasons, AudioStreamIndex = audioStreamIndex, VideoStreamIndex = videoStreamIndex, Context = context, @@ -656,7 +656,7 @@ namespace Jellyfin.Api.Controllers /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param> /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param> /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param> - /// <param name="transcodingReasons">Optional. The transcoding reason.</param> + /// <param name="transcodeReasons">Optional. The transcoding reason.</param> /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param> /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param> /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param> @@ -711,7 +711,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? enableMpegtsM2TsMode, [FromQuery] string? videoCodec, [FromQuery] string? subtitleCodec, - [FromQuery] string? transcodingReasons, + [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, [FromQuery] EncodingContext context, @@ -763,7 +763,7 @@ namespace Jellyfin.Api.Controllers EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, VideoCodec = videoCodec, SubtitleCodec = subtitleCodec, - TranscodeReasons = transcodingReasons, + TranscodeReasons = transcodeReasons, AudioStreamIndex = audioStreamIndex, VideoStreamIndex = videoStreamIndex, Context = context, @@ -823,7 +823,7 @@ namespace Jellyfin.Api.Controllers /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param> /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param> /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param> - /// <param name="transcodingReasons">Optional. The transcoding reason.</param> + /// <param name="transcodeReasons">Optional. The transcoding reason.</param> /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param> /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param> /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param> @@ -881,7 +881,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? enableMpegtsM2TsMode, [FromQuery] string? videoCodec, [FromQuery] string? subtitleCodec, - [FromQuery] string? transcodingReasons, + [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, [FromQuery] EncodingContext context, @@ -933,7 +933,7 @@ namespace Jellyfin.Api.Controllers EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, VideoCodec = videoCodec, SubtitleCodec = subtitleCodec, - TranscodeReasons = transcodingReasons, + TranscodeReasons = transcodeReasons, AudioStreamIndex = audioStreamIndex, VideoStreamIndex = videoStreamIndex, Context = context, @@ -994,7 +994,7 @@ namespace Jellyfin.Api.Controllers /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param> /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param> /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param> - /// <param name="transcodingReasons">Optional. The transcoding reason.</param> + /// <param name="transcodeReasons">Optional. The transcoding reason.</param> /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param> /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param> /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param> @@ -1053,7 +1053,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? enableMpegtsM2TsMode, [FromQuery] string? videoCodec, [FromQuery] string? subtitleCodec, - [FromQuery] string? transcodingReasons, + [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, [FromQuery] EncodingContext context, @@ -1105,7 +1105,7 @@ namespace Jellyfin.Api.Controllers EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, VideoCodec = videoCodec, SubtitleCodec = subtitleCodec, - TranscodeReasons = transcodingReasons, + TranscodeReasons = transcodeReasons, AudioStreamIndex = audioStreamIndex, VideoStreamIndex = videoStreamIndex, Context = context, diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs index 31cb9e273..9220b988f 100644 --- a/Jellyfin.Api/Controllers/FilterController.cs +++ b/Jellyfin.Api/Controllers/FilterController.cs @@ -50,33 +50,24 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<QueryFiltersLegacy> GetQueryFiltersLegacy( [FromQuery] Guid? userId, - [FromQuery] string? parentId, + [FromQuery] Guid? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes) { - var parentItem = string.IsNullOrEmpty(parentId) - ? null - : _libraryManager.GetItemById(parentId); - var user = userId.HasValue && !userId.Equals(Guid.Empty) ? _userManager.GetUserById(userId.Value) : null; - if (includeItemTypes.Length == 1 - && (string.Equals(includeItemTypes[0], nameof(BoxSet), StringComparison.OrdinalIgnoreCase) - || string.Equals(includeItemTypes[0], nameof(Playlist), StringComparison.OrdinalIgnoreCase) - || string.Equals(includeItemTypes[0], nameof(Trailer), StringComparison.OrdinalIgnoreCase) - || string.Equals(includeItemTypes[0], "Program", StringComparison.OrdinalIgnoreCase))) + BaseItem? item = null; + if (includeItemTypes.Length != 1 + || !(string.Equals(includeItemTypes[0], nameof(BoxSet), StringComparison.OrdinalIgnoreCase) + || string.Equals(includeItemTypes[0], nameof(Playlist), StringComparison.OrdinalIgnoreCase) + || string.Equals(includeItemTypes[0], nameof(Trailer), StringComparison.OrdinalIgnoreCase) + || string.Equals(includeItemTypes[0], "Program", StringComparison.OrdinalIgnoreCase))) { - parentItem = null; + item = _libraryManager.GetParentItem(parentId, user?.Id); } - var item = string.IsNullOrEmpty(parentId) - ? user == null - ? _libraryManager.RootFolder - : _libraryManager.GetUserRootFolder() - : parentItem; - var query = new InternalItemsQuery { User = user, @@ -92,7 +83,12 @@ namespace Jellyfin.Api.Controllers } }; - var itemList = ((Folder)item!).GetItemList(query); + if (item is not Folder folder) + { + return new QueryFiltersLegacy(); + } + + var itemList = folder.GetItemList(query); return new QueryFiltersLegacy { Years = itemList.Select(i => i.ProductionYear ?? -1) @@ -140,7 +136,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<QueryFilters> GetQueryFilters( [FromQuery] Guid? userId, - [FromQuery] string? parentId, + [FromQuery] Guid? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, [FromQuery] bool? isAiring, [FromQuery] bool? isMovie, @@ -150,14 +146,11 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? isSeries, [FromQuery] bool? recursive) { - var parentItem = string.IsNullOrEmpty(parentId) - ? null - : _libraryManager.GetItemById(parentId); - var user = userId.HasValue && !userId.Equals(Guid.Empty) ? _userManager.GetUserById(userId.Value) : null; + BaseItem? parentItem = null; if (includeItemTypes.Length == 1 && (string.Equals(includeItemTypes[0], nameof(BoxSet), StringComparison.OrdinalIgnoreCase) || string.Equals(includeItemTypes[0], nameof(Playlist), StringComparison.OrdinalIgnoreCase) @@ -166,6 +159,10 @@ namespace Jellyfin.Api.Controllers { parentItem = null; } + else if (parentId.HasValue) + { + parentItem = _libraryManager.GetItemById(parentId.Value); + } var filters = new QueryFilters(); var genreQuery = new InternalItemsQuery(user) diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs index d2b41e0a8..b6755ed5e 100644 --- a/Jellyfin.Api/Controllers/GenresController.cs +++ b/Jellyfin.Api/Controllers/GenresController.cs @@ -72,7 +72,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] string? searchTerm, - [FromQuery] string? parentId, + [FromQuery] Guid? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, @@ -109,15 +109,15 @@ namespace Jellyfin.Api.Controllers EnableTotalRecordCount = enableTotalRecordCount }; - if (!string.IsNullOrWhiteSpace(parentId)) + if (parentId.HasValue) { if (parentItem is Folder) { - query.AncestorIds = new[] { new Guid(parentId) }; + query.AncestorIds = new[] { parentId.Value }; } else { - query.ItemIds = new[] { new Guid(parentId) }; + query.ItemIds = new[] { parentId.Value }; } } diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index b0979fbcf..7e9035f80 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -176,7 +176,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? recursive, [FromQuery] string? searchTerm, [FromQuery] string? sortOrder, - [FromQuery] string? parentId, + [FromQuery] Guid? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, @@ -239,14 +239,8 @@ namespace Jellyfin.Api.Controllers parentId = null; } - BaseItem? item = null; + var item = _libraryManager.GetParentItem(parentId, userId); QueryResult<BaseItem> result; - if (!string.IsNullOrEmpty(parentId)) - { - item = _libraryManager.GetItemById(parentId); - } - - item ??= _libraryManager.GetUserRootFolder(); if (!(item is Folder folder)) { @@ -343,7 +337,7 @@ namespace Jellyfin.Api.Controllers ItemIds = ids, MinCommunityRating = minCommunityRating, MinCriticRating = minCriticRating, - ParentId = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId), + ParentId = parentId ?? Guid.Empty, ParentIndexNumber = parentIndexNumber, EnableTotalRecordCount = enableTotalRecordCount, ExcludeItemIds = excludeItemIds, @@ -615,7 +609,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? recursive, [FromQuery] string? searchTerm, [FromQuery] string? sortOrder, - [FromQuery] string? parentId, + [FromQuery] Guid? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, @@ -773,7 +767,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] string? searchTerm, - [FromQuery] string? parentId, + [FromQuery] Guid? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, [FromQuery] bool? enableUserData, @@ -785,7 +779,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? enableImages = true) { var user = _userManager.GetUserById(userId); - var parentIdGuid = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId); + var parentIdGuid = parentId ?? Guid.Empty; var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index 3ff77e8e0..184843b39 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -362,7 +362,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] - public ActionResult DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] ids) + public ActionResult DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids) { if (ids.Length == 0) { diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 410f3a340..56d4b3933 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; @@ -17,7 +17,6 @@ using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.LiveTvDtos; using Jellyfin.Data.Enums; -using MediaBrowser.Common; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Dto; @@ -1015,7 +1014,9 @@ namespace Jellyfin.Api.Controllers if (!string.IsNullOrEmpty(pw)) { using var sha = SHA1.Create(); - listingsProviderInfo.Password = Hex.Encode(sha.ComputeHash(Encoding.UTF8.GetBytes(pw))); + // TODO: remove ToLower when Convert.ToHexString supports lowercase + // Schedules Direct requires the hex to be lowercase + listingsProviderInfo.Password = Convert.ToHexString(sha.ComputeHash(Encoding.UTF8.GetBytes(pw))).ToLowerInvariant(); } return await _liveTvManager.SaveListingProvider(listingsProviderInfo, validateLogin, validateListings).ConfigureAwait(false); diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs index b42e6686e..a76dc057a 100644 --- a/Jellyfin.Api/Controllers/MediaInfoController.cs +++ b/Jellyfin.Api/Controllers/MediaInfoController.cs @@ -8,7 +8,6 @@ using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.MediaInfoDtos; -using Jellyfin.Api.Models.VideoDtos; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; @@ -81,6 +80,9 @@ namespace Jellyfin.Api.Controllers /// <summary> /// Gets live playback media info for an item. /// </summary> + /// <remarks> + /// For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence. + /// </remarks> /// <param name="itemId">The item id.</param> /// <param name="userId">The user id.</param> /// <param name="maxStreamingBitrate">The maximum streaming bitrate.</param> @@ -90,13 +92,13 @@ namespace Jellyfin.Api.Controllers /// <param name="maxAudioChannels">The maximum number of audio channels.</param> /// <param name="mediaSourceId">The media source id.</param> /// <param name="liveStreamId">The livestream id.</param> - /// <param name="deviceProfile">The device profile.</param> /// <param name="autoOpenLiveStream">Whether to auto open the livestream.</param> /// <param name="enableDirectPlay">Whether to enable direct play. Default: true.</param> /// <param name="enableDirectStream">Whether to enable direct stream. Default: true.</param> /// <param name="enableTranscoding">Whether to enable transcoding. Default: true.</param> /// <param name="allowVideoStreamCopy">Whether to allow to copy the video stream. Default: true.</param> /// <param name="allowAudioStreamCopy">Whether to allow to copy the audio stream. Default: true.</param> + /// <param name="playbackInfoDto">The playback info.</param> /// <response code="200">Playback info returned.</response> /// <returns>A <see cref="Task"/> containing a <see cref="PlaybackInfoResponse"/> with the playback info.</returns> [HttpPost("Items/{itemId}/PlaybackInfo")] @@ -111,18 +113,17 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? maxAudioChannels, [FromQuery] string? mediaSourceId, [FromQuery] string? liveStreamId, - [FromBody] DeviceProfileDto? deviceProfile, - [FromQuery] bool autoOpenLiveStream = false, - [FromQuery] bool enableDirectPlay = true, - [FromQuery] bool enableDirectStream = true, - [FromQuery] bool enableTranscoding = true, - [FromQuery] bool allowVideoStreamCopy = true, - [FromQuery] bool allowAudioStreamCopy = true) + [FromQuery] bool? autoOpenLiveStream, + [FromQuery] bool? enableDirectPlay, + [FromQuery] bool? enableDirectStream, + [FromQuery] bool? enableTranscoding, + [FromQuery] bool? allowVideoStreamCopy, + [FromQuery] bool? allowAudioStreamCopy, + [FromBody] PlaybackInfoDto? playbackInfoDto) { var authInfo = _authContext.GetAuthorizationInfo(Request); - var profile = deviceProfile?.DeviceProfile; - + var profile = playbackInfoDto?.DeviceProfile; _logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", profile); if (profile == null) @@ -134,6 +135,23 @@ namespace Jellyfin.Api.Controllers } } + // Copy params from posted body + // TODO clean up when breaking API compatibility. + userId ??= playbackInfoDto?.UserId; + maxStreamingBitrate ??= playbackInfoDto?.MaxStreamingBitrate; + startTimeTicks ??= playbackInfoDto?.StartTimeTicks; + audioStreamIndex ??= playbackInfoDto?.AudioStreamIndex; + subtitleStreamIndex ??= playbackInfoDto?.SubtitleStreamIndex; + maxAudioChannels ??= playbackInfoDto?.MaxAudioChannels; + mediaSourceId ??= playbackInfoDto?.MediaSourceId; + liveStreamId ??= playbackInfoDto?.LiveStreamId; + autoOpenLiveStream ??= playbackInfoDto?.AutoOpenLiveStream ?? false; + enableDirectPlay ??= playbackInfoDto?.EnableDirectPlay ?? true; + enableDirectStream ??= playbackInfoDto?.EnableDirectStream ?? true; + enableTranscoding ??= playbackInfoDto?.EnableTranscoding ?? true; + allowVideoStreamCopy ??= playbackInfoDto?.AllowVideoStreamCopy ?? true; + allowAudioStreamCopy ??= playbackInfoDto?.AllowAudioStreamCopy ?? true; + var info = await _mediaInfoHelper.GetPlaybackInfo( itemId, userId, @@ -161,18 +179,18 @@ namespace Jellyfin.Api.Controllers maxAudioChannels, info!.PlaySessionId!, userId ?? Guid.Empty, - enableDirectPlay, - enableDirectStream, - enableTranscoding, - allowVideoStreamCopy, - allowAudioStreamCopy, + enableDirectPlay.Value, + enableDirectStream.Value, + enableTranscoding.Value, + allowVideoStreamCopy.Value, + allowAudioStreamCopy.Value, Request.HttpContext.GetNormalizedRemoteIp()); } _mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate); } - if (autoOpenLiveStream) + if (autoOpenLiveStream.Value) { var mediaSource = string.IsNullOrWhiteSpace(mediaSourceId) ? info.MediaSources[0] : info.MediaSources.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.Ordinal)); @@ -183,9 +201,9 @@ namespace Jellyfin.Api.Controllers new LiveStreamRequest { AudioStreamIndex = audioStreamIndex, - DeviceProfile = deviceProfile?.DeviceProfile, - EnableDirectPlay = enableDirectPlay, - EnableDirectStream = enableDirectStream, + DeviceProfile = playbackInfoDto?.DeviceProfile, + EnableDirectPlay = enableDirectPlay.Value, + EnableDirectStream = enableDirectStream.Value, ItemId = itemId, MaxAudioChannels = maxAudioChannels, MaxStreamingBitrate = maxStreamingBitrate, diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs index 75dfd4e68..4d788ad7d 100644 --- a/Jellyfin.Api/Controllers/MoviesController.cs +++ b/Jellyfin.Api/Controllers/MoviesController.cs @@ -65,7 +65,7 @@ namespace Jellyfin.Api.Controllers [HttpGet("Recommendations")] public ActionResult<IEnumerable<RecommendationDto>> GetMovieRecommendations( [FromQuery] Guid? userId, - [FromQuery] string? parentId, + [FromQuery] Guid? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery] int categoryLimit = 5, [FromQuery] int itemLimit = 8) @@ -78,7 +78,7 @@ namespace Jellyfin.Api.Controllers var categories = new List<RecommendationDto>(); - var parentIdGuid = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId); + var parentIdGuid = parentId ?? Guid.Empty; var query = new InternalItemsQuery(user) { diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs index e7d0a61c5..2608a9cd0 100644 --- a/Jellyfin.Api/Controllers/MusicGenresController.cs +++ b/Jellyfin.Api/Controllers/MusicGenresController.cs @@ -72,7 +72,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] string? searchTerm, - [FromQuery] string? parentId, + [FromQuery] Guid? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, @@ -109,15 +109,15 @@ namespace Jellyfin.Api.Controllers EnableTotalRecordCount = enableTotalRecordCount }; - if (!string.IsNullOrWhiteSpace(parentId)) + if (parentId.HasValue) { if (parentItem is Folder) { - query.AncestorIds = new[] { new Guid(parentId) }; + query.AncestorIds = new[] { parentId.Value }; } else { - query.ItemIds = new[] { new Guid(parentId) }; + query.ItemIds = new[] { parentId.Value }; } } diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs index 83b359766..6295dfc05 100644 --- a/Jellyfin.Api/Controllers/PackageController.cs +++ b/Jellyfin.Api/Controllers/PackageController.cs @@ -45,13 +45,13 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public async Task<ActionResult<PackageInfo>> GetPackageInfo( [FromRoute, Required] string name, - [FromQuery] string? assemblyGuid) + [FromQuery] Guid? assemblyGuid) { var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false); var result = _installationManager.FilterPackages( packages, name, - string.IsNullOrEmpty(assemblyGuid) ? default : Guid.Parse(assemblyGuid)) + assemblyGuid ?? default) .FirstOrDefault(); if (result == null) @@ -92,7 +92,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.RequiresElevation)] public async Task<ActionResult> InstallPackage( [FromRoute, Required] string name, - [FromQuery] string? assemblyGuid, + [FromQuery] Guid? assemblyGuid, [FromQuery] string? version, [FromQuery] string? repositoryUrl) { @@ -106,7 +106,7 @@ namespace Jellyfin.Api.Controllers var package = _installationManager.GetCompatibleVersions( packages, name, - string.IsNullOrEmpty(assemblyGuid) ? Guid.Empty : Guid.Parse(assemblyGuid), + assemblyGuid ?? Guid.Empty, specificVersion: string.IsNullOrEmpty(version) ? null : Version.Parse(version)) .FirstOrDefault(); diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs index aaad36551..17e631197 100644 --- a/Jellyfin.Api/Controllers/PersonsController.cs +++ b/Jellyfin.Api/Controllers/PersonsController.cs @@ -79,7 +79,7 @@ namespace Jellyfin.Api.Controllers [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludePersonTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes, - [FromQuery] string? appearsInItemId, + [FromQuery] Guid? appearsInItemId, [FromQuery] Guid? userId, [FromQuery] bool? enableImages = true) { @@ -102,7 +102,7 @@ namespace Jellyfin.Api.Controllers NameContains = searchTerm, User = user, IsFavorite = !isFavorite.HasValue && isFavoriteInFilters ? true : isFavorite, - AppearsInItemId = string.IsNullOrEmpty(appearsInItemId) ? Guid.Empty : Guid.Parse(appearsInItemId), + AppearsInItemId = appearsInItemId ?? Guid.Empty, Limit = limit ?? 0 }); diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs index 076fe58f1..08255ff8f 100644 --- a/Jellyfin.Api/Controllers/SearchController.cs +++ b/Jellyfin.Api/Controllers/SearchController.cs @@ -86,7 +86,7 @@ namespace Jellyfin.Api.Controllers [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, - [FromQuery] string? parentId, + [FromQuery] Guid? parentId, [FromQuery] bool? isMovie, [FromQuery] bool? isSeries, [FromQuery] bool? isNews, diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index e59c6e1dd..d9cb34557 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Models.StartupDtos; +using Jellyfin.Networking.Configuration; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; @@ -89,9 +90,10 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult SetRemoteAccess([FromBody, Required] StartupRemoteAccessDto startupRemoteAccessDto) { - _config.Configuration.EnableRemoteAccess = startupRemoteAccessDto.EnableRemoteAccess; - _config.Configuration.EnableUPnP = startupRemoteAccessDto.EnableAutomaticPortMapping; - _config.SaveConfiguration(); + NetworkConfiguration settings = _config.GetNetworkConfiguration(); + settings.EnableRemoteAccess = startupRemoteAccessDto.EnableRemoteAccess; + settings.EnableUPnP = startupRemoteAccessDto.EnableAutomaticPortMapping; + _config.SaveConfiguration("network", settings); return NoContent(); } diff --git a/Jellyfin.Api/Controllers/StudiosController.cs b/Jellyfin.Api/Controllers/StudiosController.cs index 5090bf1de..bb54c59f6 100644 --- a/Jellyfin.Api/Controllers/StudiosController.cs +++ b/Jellyfin.Api/Controllers/StudiosController.cs @@ -71,7 +71,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] string? searchTerm, - [FromQuery] string? parentId, + [FromQuery] Guid? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, @@ -109,15 +109,15 @@ namespace Jellyfin.Api.Controllers EnableTotalRecordCount = enableTotalRecordCount }; - if (!string.IsNullOrWhiteSpace(parentId)) + if (parentId.HasValue) { if (parentItem is Folder) { - query.AncestorIds = new[] { new Guid(parentId) }; + query.AncestorIds = new[] { parentId.Value }; } else { - query.ItemIds = new[] { new Guid(parentId) }; + query.ItemIds = new[] { parentId.Value }; } } diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index 92875d735..7784e8a11 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -64,9 +64,9 @@ namespace Jellyfin.Api.Controllers [HttpGet("Info")] [Authorize(Policy = Policies.FirstTimeSetupOrIgnoreParentalControl)] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task<ActionResult<SystemInfo>> GetSystemInfo() + public ActionResult<SystemInfo> GetSystemInfo() { - return await _appHost.GetSystemInfo(CancellationToken.None).ConfigureAwait(false); + return _appHost.GetSystemInfo(Request.HttpContext.Connection.RemoteIpAddress); } /// <summary> @@ -76,9 +76,9 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="PublicSystemInfo"/> with public info about the system.</returns> [HttpGet("Info/Public")] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task<ActionResult<PublicSystemInfo>> GetPublicSystemInfo() + public ActionResult<PublicSystemInfo> GetPublicSystemInfo() { - return await _appHost.GetPublicSystemInfo(CancellationToken.None).ConfigureAwait(false); + return _appHost.GetPublicSystemInfo(Request.HttpContext.Connection.RemoteIpAddress); } /// <summary> diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs index 5b71fed5a..8e9ece14f 100644 --- a/Jellyfin.Api/Controllers/TrailersController.cs +++ b/Jellyfin.Api/Controllers/TrailersController.cs @@ -145,7 +145,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? recursive, [FromQuery] string? searchTerm, [FromQuery] string? sortOrder, - [FromQuery] string? parentId, + [FromQuery] Guid? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index 57b056f50..03fd1846d 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -76,7 +76,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? limit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery] string? seriesId, - [FromQuery] string? parentId, + [FromQuery] Guid? parentId, [FromQuery] bool? enableImges, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, @@ -132,7 +132,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery] string? parentId, + [FromQuery] Guid? parentId, [FromQuery] bool? enableImges, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, @@ -144,7 +144,7 @@ namespace Jellyfin.Api.Controllers var minPremiereDate = DateTime.Now.Date.ToUniversalTime().AddDays(-1); - var parentIdGuid = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId); + var parentIdGuid = parentId ?? Guid.Empty; var options = new DtoOptions { Fields = fields } .AddClientFields(Request) @@ -194,14 +194,14 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult<QueryResult<BaseItemDto>> GetEpisodes( - [FromRoute, Required] string seriesId, + [FromRoute, Required] Guid seriesId, [FromQuery] Guid? userId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery] int? season, - [FromQuery] string? seasonId, + [FromQuery] Guid? seasonId, [FromQuery] bool? isMissing, [FromQuery] string? adjacentTo, - [FromQuery] string? startItemId, + [FromQuery] Guid? startItemId, [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] bool? enableImages, @@ -220,9 +220,9 @@ namespace Jellyfin.Api.Controllers .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!); - if (!string.IsNullOrWhiteSpace(seasonId)) // Season id was supplied. Get episodes by season id. + if (seasonId.HasValue) // Season id was supplied. Get episodes by season id. { - var item = _libraryManager.GetItemById(new Guid(seasonId)); + var item = _libraryManager.GetItemById(seasonId.Value); if (!(item is Season seasonItem)) { return NotFound("No season exists with Id " + seasonId); @@ -264,10 +264,10 @@ namespace Jellyfin.Api.Controllers .ToList(); } - if (!string.IsNullOrWhiteSpace(startItemId)) + if (startItemId.HasValue) { episodes = episodes - .SkipWhile(i => !string.Equals(i.Id.ToString("N", CultureInfo.InvariantCulture), startItemId, StringComparison.OrdinalIgnoreCase)) + .SkipWhile(i => startItemId.Value.Equals(i.Id)) .ToList(); } @@ -316,7 +316,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult<QueryResult<BaseItemDto>> GetSeasons( - [FromRoute, Required] string seriesId, + [FromRoute, Required] Guid seriesId, [FromQuery] Guid? userId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery] bool? isSpecialSeason, diff --git a/Jellyfin.Api/Controllers/VideoHlsController.cs b/Jellyfin.Api/Controllers/VideoHlsController.cs index 2ac16de6b..7e743ee0c 100644 --- a/Jellyfin.Api/Controllers/VideoHlsController.cs +++ b/Jellyfin.Api/Controllers/VideoHlsController.cs @@ -153,7 +153,7 @@ namespace Jellyfin.Api.Controllers /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param> /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param> /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param> - /// <param name="transcodingReasons">Optional. The transcoding reason.</param> + /// <param name="transcodeReasons">Optional. The transcoding reason.</param> /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param> /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param> /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param> @@ -211,7 +211,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? enableMpegtsM2TsMode, [FromQuery] string? videoCodec, [FromQuery] string? subtitleCodec, - [FromQuery] string? transcodingReasons, + [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, [FromQuery] EncodingContext context, @@ -266,7 +266,7 @@ namespace Jellyfin.Api.Controllers EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, VideoCodec = videoCodec, SubtitleCodec = subtitleCodec, - TranscodeReasons = transcodingReasons, + TranscodeReasons = transcodeReasons, AudioStreamIndex = audioStreamIndex, VideoStreamIndex = videoStreamIndex, Context = context, diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index 8e17b843a..d8bc9df1f 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -320,7 +320,7 @@ namespace Jellyfin.Api.Controllers /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param> /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param> /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param> - /// <param name="transcodingReasons">Optional. The transcoding reason.</param> + /// <param name="transcodeReasons">Optional. The transcoding reason.</param> /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param> /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param> /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param> @@ -376,7 +376,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? enableMpegtsM2TsMode, [FromQuery] string? videoCodec, [FromQuery] string? subtitleCodec, - [FromQuery] string? transcodingReasons, + [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, [FromQuery] EncodingContext context, @@ -430,7 +430,7 @@ namespace Jellyfin.Api.Controllers EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, VideoCodec = videoCodec, SubtitleCodec = subtitleCodec, - TranscodeReasons = transcodingReasons, + TranscodeReasons = transcodeReasons, AudioStreamIndex = audioStreamIndex, VideoStreamIndex = videoStreamIndex, Context = context, @@ -576,7 +576,7 @@ namespace Jellyfin.Api.Controllers /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param> /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param> /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param> - /// <param name="transcodingReasons">Optional. The transcoding reason.</param> + /// <param name="transcodeReasons">Optional. The transcoding reason.</param> /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param> /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param> /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param> @@ -632,7 +632,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? enableMpegtsM2TsMode, [FromQuery] string? videoCodec, [FromQuery] string? subtitleCodec, - [FromQuery] string? transcodingReasons, + [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, [FromQuery] EncodingContext context, @@ -683,7 +683,7 @@ namespace Jellyfin.Api.Controllers enableMpegtsM2TsMode, videoCodec, subtitleCodec, - transcodingReasons, + transcodeReasons, audioStreamIndex, videoStreamIndex, context, diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs index ec7c3de97..48c639b08 100644 --- a/Jellyfin.Api/Controllers/YearsController.cs +++ b/Jellyfin.Api/Controllers/YearsController.cs @@ -71,7 +71,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] string? sortOrder, - [FromQuery] string? parentId, + [FromQuery] Guid? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, @@ -89,16 +89,11 @@ namespace Jellyfin.Api.Controllers .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); User? user = null; - BaseItem parentItem; + BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId); if (userId.HasValue && !userId.Equals(Guid.Empty)) { user = _userManager.GetUserById(userId.Value); - parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId); - } - else - { - parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId); } IList<BaseItem> items; diff --git a/Jellyfin.Api/Helpers/ClassMigrationHelper.cs b/Jellyfin.Api/Helpers/ClassMigrationHelper.cs new file mode 100644 index 000000000..a911a3324 --- /dev/null +++ b/Jellyfin.Api/Helpers/ClassMigrationHelper.cs @@ -0,0 +1,71 @@ +using System; +using System.Reflection; + +namespace Jellyfin.Api.Helpers +{ + /// <summary> + /// A static class for copying matching properties from one object to another. + /// TODO: remove at the point when a fixed migration path has been decided upon. + /// </summary> + public static class ClassMigrationHelper + { + /// <summary> + /// Extension for 'Object' that copies the properties to a destination object. + /// </summary> + /// <param name="source">The source.</param> + /// <param name="destination">The destination.</param> + public static void CopyProperties(this object source, object destination) + { + // If any this null throw an exception. + if (source == null || destination == null) + { + throw new Exception("Source or/and Destination Objects are null"); + } + + // Getting the Types of the objects. + Type typeDest = destination.GetType(); + Type typeSrc = source.GetType(); + + // Iterate the Properties of the source instance and populate them from their destination counterparts. + PropertyInfo[] srcProps = typeSrc.GetProperties(); + foreach (PropertyInfo srcProp in srcProps) + { + if (!srcProp.CanRead) + { + continue; + } + + var targetProperty = typeDest.GetProperty(srcProp.Name); + if (targetProperty == null) + { + continue; + } + + if (!targetProperty.CanWrite) + { + continue; + } + + var obj = targetProperty.GetSetMethod(true); + if (obj != null && obj.IsPrivate) + { + continue; + } + + var target = targetProperty.GetSetMethod(); + if (target != null && (target.Attributes & MethodAttributes.Static) != 0) + { + continue; + } + + if (!targetProperty.PropertyType.IsAssignableFrom(srcProp.PropertyType)) + { + continue; + } + + // Passed all tests, lets set the value. + targetProperty.SetValue(destination, srcProp.GetValue(source, null), null); + } + } + } +} diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index 99c90c315..bb2265dba 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -762,11 +762,6 @@ namespace Jellyfin.Api.Helpers private async Task AcquireResources(StreamState state, CancellationTokenSource cancellationTokenSource) { - if (state.VideoType == VideoType.Iso && state.IsoType.HasValue && _isoManager.CanMount(state.MediaPath)) - { - state.IsoMount = await _isoManager.Mount(state.MediaPath, cancellationTokenSource.Token).ConfigureAwait(false); - } - if (state.MediaSource.RequiresOpening && string.IsNullOrWhiteSpace(state.Request.LiveStreamId)) { var liveStreamResponse = await _mediaSourceManager.OpenLiveStream( diff --git a/Jellyfin.Api/Models/MediaInfoDtos/PlaybackInfoDto.cs b/Jellyfin.Api/Models/MediaInfoDtos/PlaybackInfoDto.cs new file mode 100644 index 000000000..2cfdba507 --- /dev/null +++ b/Jellyfin.Api/Models/MediaInfoDtos/PlaybackInfoDto.cs @@ -0,0 +1,86 @@ +using System; +using MediaBrowser.Model.Dlna; + +namespace Jellyfin.Api.Models.MediaInfoDtos +{ + /// <summary> + /// Plabyback info dto. + /// </summary> + public class PlaybackInfoDto + { + /// <summary> + /// Gets or sets the playback userId. + /// </summary> + public Guid? UserId { get; set; } + + /// <summary> + /// Gets or sets the max streaming bitrate. + /// </summary> + public int? MaxStreamingBitrate { get; set; } + + /// <summary> + /// Gets or sets the start time in ticks. + /// </summary> + public long? StartTimeTicks { get; set; } + + /// <summary> + /// Gets or sets the audio stream index. + /// </summary> + public int? AudioStreamIndex { get; set; } + + /// <summary> + /// Gets or sets the subtitle stream index. + /// </summary> + public int? SubtitleStreamIndex { get; set; } + + /// <summary> + /// Gets or sets the max audio channels. + /// </summary> + public int? MaxAudioChannels { get; set; } + + /// <summary> + /// Gets or sets the media source id. + /// </summary> + public string? MediaSourceId { get; set; } + + /// <summary> + /// Gets or sets the live stream id. + /// </summary> + public string? LiveStreamId { get; set; } + + /// <summary> + /// Gets or sets the device profile. + /// </summary> + public DeviceProfile? DeviceProfile { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether to enable direct play. + /// </summary> + public bool? EnableDirectPlay { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether to enable direct stream. + /// </summary> + public bool? EnableDirectStream { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether to enable transcoding. + /// </summary> + public bool? EnableTranscoding { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether to enable video stream copy. + /// </summary> + public bool? AllowVideoStreamCopy { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether to allow audio stream copy. + /// </summary> + public bool? AllowAudioStreamCopy { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether to auto open the live stream. + /// </summary> + public bool? AutoOpenLiveStream { get; set; } + } +}
\ No newline at end of file diff --git a/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs b/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs index ac1259ef2..e58095536 100644 --- a/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs +++ b/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; +using System.Text.Json.Serialization; using MediaBrowser.Common.Json.Converters; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Session; -using Newtonsoft.Json; namespace Jellyfin.Api.Models.SessionDtos { @@ -15,6 +15,7 @@ namespace Jellyfin.Api.Models.SessionDtos /// <summary> /// Gets or sets the list of playable media types. /// </summary> + [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] public IReadOnlyList<string> PlayableMediaTypes { get; set; } = Array.Empty<string>(); /// <summary> diff --git a/Jellyfin.Api/Models/VideoDtos/DeviceProfileDto.cs b/Jellyfin.Api/Models/VideoDtos/DeviceProfileDto.cs deleted file mode 100644 index db55dc34b..000000000 --- a/Jellyfin.Api/Models/VideoDtos/DeviceProfileDto.cs +++ /dev/null @@ -1,15 +0,0 @@ -using MediaBrowser.Model.Dlna; - -namespace Jellyfin.Api.Models.VideoDtos -{ - /// <summary> - /// Device profile dto. - /// </summary> - public class DeviceProfileDto - { - /// <summary> - /// Gets or sets device profile. - /// </summary> - public DeviceProfile? DeviceProfile { get; set; } - } -} diff --git a/Jellyfin.Networking/Manager/INetworkManager.cs b/Jellyfin.Networking/Manager/INetworkManager.cs deleted file mode 100644 index eababa6a9..000000000 --- a/Jellyfin.Networking/Manager/INetworkManager.cs +++ /dev/null @@ -1,234 +0,0 @@ -#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 -{ - /// <summary> - /// Interface for the NetworkManager class. - /// </summary> - public interface INetworkManager - { - /// <summary> - /// Event triggered on network changes. - /// </summary> - event EventHandler NetworkChanged; - - /// <summary> - /// Gets the published server urls list. - /// </summary> - Dictionary<IPNetAddress, string> PublishedServerUrls { get; } - - /// <summary> - /// Gets a value indicating whether is all IPv6 interfaces are trusted as internal. - /// </summary> - bool TrustAllIP6Interfaces { get; } - - /// <summary> - /// Gets the remote address filter. - /// </summary> - Collection<IPObject> RemoteAddressFilter { get; } - - /// <summary> - /// Gets or sets a value indicating whether iP6 is enabled. - /// </summary> - bool IsIP6Enabled { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether iP4 is enabled. - /// </summary> - bool IsIP4Enabled { get; set; } - - /// <summary> - /// Calculates the list of interfaces to use for Kestrel. - /// </summary> - /// <returns>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.</returns> - /// <param name="individualInterfaces">When false, return <see cref="IPAddress.Any"/> or <see cref="IPAddress.IPv6Any"/> for all interfaces.</param> - Collection<IPObject> GetAllBindInterfaces(bool individualInterfaces = false); - - /// <summary> - /// Returns a collection containing the loopback interfaces. - /// </summary> - /// <returns>Collection{IPObject}.</returns> - Collection<IPObject> GetLoopbacks(); - - /// <summary> - /// 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. - /// </summary> - /// <param name="source">Source of the request.</param> - /// <param name="port">Optional port returned, if it's part of an override.</param> - /// <returns>IP Address to use, or loopback address if all else fails.</returns> - string GetBindInterface(IPObject source, out int? port); - - /// <summary> - /// 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 <see cref="GetBindInterface(IPObject, out int?)"/>. - /// </summary> - /// <param name="source">Source of the request.</param> - /// <param name="port">Optional port returned, if it's part of an override.</param> - /// <returns>IP Address to use, or loopback address if all else fails.</returns> - string GetBindInterface(HttpRequest source, out int? port); - - /// <summary> - /// 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 <see cref="GetBindInterface(IPObject, out int?)"/>. - /// </summary> - /// <param name="source">IP address of the request.</param> - /// <param name="port">Optional port returned, if it's part of an override.</param> - /// <returns>IP Address to use, or loopback address if all else fails.</returns> - string GetBindInterface(IPAddress source, out int? port); - - /// <summary> - /// 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 <see cref="GetBindInterface(IPObject, out int?)"/>. - /// </summary> - /// <param name="source">Source of the request.</param> - /// <param name="port">Optional port returned, if it's part of an override.</param> - /// <returns>IP Address to use, or loopback address if all else fails.</returns> - string GetBindInterface(string source, out int? port); - - /// <summary> - /// Checks to see if the ip address is specifically excluded in LocalNetworkAddresses. - /// </summary> - /// <param name="address">IP address to check.</param> - /// <returns>True if it is.</returns> - bool IsExcludedInterface(IPAddress address); - - /// <summary> - /// Get a list of all the MAC addresses associated with active interfaces. - /// </summary> - /// <returns>List of MAC addresses.</returns> - IReadOnlyCollection<PhysicalAddress> GetMacAddresses(); - - /// <summary> - /// Checks to see if the IP Address provided matches an interface that has a gateway. - /// </summary> - /// <param name="addressObj">IP to check. Can be an IPAddress or an IPObject.</param> - /// <returns>Result of the check.</returns> - bool IsGatewayInterface(IPObject? addressObj); - - /// <summary> - /// Checks to see if the IP Address provided matches an interface that has a gateway. - /// </summary> - /// <param name="addressObj">IP to check. Can be an IPAddress or an IPObject.</param> - /// <returns>Result of the check.</returns> - bool IsGatewayInterface(IPAddress? addressObj); - - /// <summary> - /// Returns true if the address is a private address. - /// The config option TrustIP6Interfaces overrides this functions behaviour. - /// </summary> - /// <param name="address">Address to check.</param> - /// <returns>True or False.</returns> - bool IsPrivateAddressRange(IPObject address); - - /// <summary> - /// Returns true if the address is part of the user defined LAN. - /// The config option TrustIP6Interfaces overrides this functions behaviour. - /// </summary> - /// <param name="address">IP to check.</param> - /// <returns>True if endpoint is within the LAN range.</returns> - bool IsInLocalNetwork(string address); - - /// <summary> - /// Returns true if the address is part of the user defined LAN. - /// The config option TrustIP6Interfaces overrides this functions behaviour. - /// </summary> - /// <param name="address">IP to check.</param> - /// <returns>True if endpoint is within the LAN range.</returns> - bool IsInLocalNetwork(IPObject address); - - /// <summary> - /// Returns true if the address is part of the user defined LAN. - /// The config option TrustIP6Interfaces overrides this functions behaviour. - /// </summary> - /// <param name="address">IP to check.</param> - /// <returns>True if endpoint is within the LAN range.</returns> - bool IsInLocalNetwork(IPAddress address); - - /// <summary> - /// Attempts to convert the token to an IP address, permitting for interface descriptions and indexes. - /// eg. "eth1", or "TP-LINK Wireless USB Adapter". - /// </summary> - /// <param name="token">Token to parse.</param> - /// <param name="result">Resultant object's ip addresses, if successful.</param> - /// <returns>Success of the operation.</returns> - bool TryParseInterface(string token, out Collection<IPObject>? result); - - /// <summary> - /// Parses an array of strings into a Collection{IPObject}. - /// </summary> - /// <param name="values">Values to parse.</param> - /// <param name="bracketed">When true, only include values in []. When false, ignore bracketed values.</param> - /// <returns>IPCollection object containing the value strings.</returns> - Collection<IPObject> CreateIPCollection(string[] values, bool bracketed = false); - - /// <summary> - /// Returns all the internal Bind interface addresses. - /// </summary> - /// <returns>An internal list of interfaces addresses.</returns> - Collection<IPObject> GetInternalBindAddresses(); - - /// <summary> - /// Checks to see if an IP address is still a valid interface address. - /// </summary> - /// <param name="address">IP address to check.</param> - /// <returns>True if it is.</returns> - bool IsValidInterfaceAddress(IPAddress address); - - /// <summary> - /// Returns true if the IP address is in the excluded list. - /// </summary> - /// <param name="ip">IP to check.</param> - /// <returns>True if excluded.</returns> - bool IsExcluded(IPAddress ip); - - /// <summary> - /// Returns true if the IP address is in the excluded list. - /// </summary> - /// <param name="ip">IP to check.</param> - /// <returns>True if excluded.</returns> - bool IsExcluded(EndPoint ip); - - /// <summary> - /// Gets the filtered LAN ip addresses. - /// </summary> - /// <param name="filter">Optional filter for the list.</param> - /// <returns>Returns a filtered list of LAN addresses.</returns> - Collection<IPObject> GetFilteredLANSubnets(Collection<IPObject>? filter = null); - } -} diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 515ae669a..85da927fb 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -84,7 +84,7 @@ namespace Jellyfin.Networking.Manager private Collection<IPObject> _internalInterfaces; /// <summary> - /// Flag set when no custom LAN has been defined in the config. + /// Flag set when no custom LAN has been defined in the configuration. /// </summary> private bool _usingPrivateAddresses; @@ -228,7 +228,7 @@ namespace Jellyfin.Networking.Manager } /// <inheritdoc/> - public Collection<IPObject> CreateIPCollection(string[] values, bool bracketed = false) + public Collection<IPObject> CreateIPCollection(string[] values, bool negated = false) { Collection<IPObject> col = new Collection<IPObject>(); if (values == null) @@ -242,28 +242,21 @@ namespace Jellyfin.Networking.Manager try { - if (v.StartsWith('[') && v.EndsWith(']')) + if (v.StartsWith('!')) { - if (bracketed) - { - AddToCollection(col, v[1..^1]); - } - } - else if (v.StartsWith('!')) - { - if (bracketed) + if (negated) { AddToCollection(col, v[1..]); } } - else if (!bracketed) + else if (!negated) { AddToCollection(col, v); } } catch (ArgumentException e) { - _logger.LogWarning(e, "Ignoring LAN value {value}.", v); + _logger.LogWarning(e, "Ignoring LAN value {Value}.", v); } } @@ -675,7 +668,6 @@ namespace Jellyfin.Networking.Manager if (address.AddressFamily == AddressFamily.InterNetworkV6) { int i = str.IndexOf("%", StringComparison.OrdinalIgnoreCase); - if (i != -1) { str = str.Substring(0, i); @@ -730,7 +722,7 @@ namespace Jellyfin.Networking.Manager } /// <summary> - /// Parses a string and adds it into the the collection, replacing any interface references. + /// Parses a string and adds it into the collection, replacing any interface references. /// </summary> /// <param name="col"><see cref="Collection{IPObject}"/>Collection.</param> /// <param name="token">String value to parse.</param> @@ -755,7 +747,19 @@ namespace Jellyfin.Networking.Manager } else if (TryParse(token, out IPObject obj)) { - if (!IsIP6Enabled) + // Expand if the ip address is "any". + if ((obj.Address.Equals(IPAddress.Any) && IsIP4Enabled) + || (obj.Address.Equals(IPAddress.IPv6Any) && IsIP6Enabled)) + { + foreach (IPNetAddress iface in _interfaceAddresses) + { + if (obj.AddressFamily == iface.AddressFamily) + { + col.AddItem(iface); + } + } + } + else if (!IsIP6Enabled) { // Remove IP6 addresses from multi-homed IPHosts. obj.Remove(AddressFamily.InterNetworkV6); @@ -872,7 +876,7 @@ namespace Jellyfin.Networking.Manager else { var replacement = parts[1].Trim(); - if (string.Equals(parts[0], "remaining", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(parts[0], "all", StringComparison.OrdinalIgnoreCase)) { _publishedServerUrls[new IPNetAddress(IPAddress.Broadcast)] = replacement; } @@ -956,7 +960,7 @@ namespace Jellyfin.Networking.Manager { _logger.LogDebug("Refreshing LAN information."); - // Get config options. + // Get configuration options. string[] subnets = config.LocalNetworkSubnets; // Create lists from user settings. diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs index 51a882c14..a0bad29e9 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs @@ -59,6 +59,12 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session var user = eventArgs.Users[0]; + var notificationType = GetPlaybackStoppedNotificationType(item.MediaType); + if (notificationType == null) + { + return; + } + await _activityManager.CreateAsync(new ActivityLog( string.Format( CultureInfo.InvariantCulture, @@ -66,7 +72,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session user.Username, GetItemName(item), eventArgs.DeviceName), - GetPlaybackStoppedNotificationType(item.MediaType), + notificationType, user.Id)) .ConfigureAwait(false); } diff --git a/Jellyfin.Server.Implementations/JellyfinDb.cs b/Jellyfin.Server.Implementations/JellyfinDb.cs index 45e71f16e..bf8818f8d 100644 --- a/Jellyfin.Server.Implementations/JellyfinDb.cs +++ b/Jellyfin.Server.Implementations/JellyfinDb.cs @@ -1,5 +1,6 @@ #pragma warning disable CS1591 +using System; using System.Linq; using Jellyfin.Data.Entities; using Jellyfin.Data.Interfaces; @@ -140,6 +141,7 @@ namespace Jellyfin.Server.Implementations /// <inheritdoc /> protected override void OnModelCreating(ModelBuilder modelBuilder) { + modelBuilder.SetDefaultDateTimeKind(DateTimeKind.Utc); base.OnModelCreating(modelBuilder); modelBuilder.HasDefaultSchema("jellyfin"); diff --git a/Jellyfin.Server.Implementations/ModelBuilderExtensions.cs b/Jellyfin.Server.Implementations/ModelBuilderExtensions.cs new file mode 100644 index 000000000..80ad65a42 --- /dev/null +++ b/Jellyfin.Server.Implementations/ModelBuilderExtensions.cs @@ -0,0 +1,48 @@ +using System; +using Jellyfin.Server.Implementations.ValueConverters; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Jellyfin.Server.Implementations +{ + /// <summary> + /// Model builder extensions. + /// </summary> + public static class ModelBuilderExtensions + { + /// <summary> + /// Specify value converter for the object type. + /// </summary> + /// <param name="modelBuilder">The model builder.</param> + /// <param name="converter">The <see cref="ValueConverter{TModel,TProvider}"/>.</param> + /// <typeparam name="T">The type to convert.</typeparam> + /// <returns>The modified <see cref="ModelBuilder"/>.</returns> + public static ModelBuilder UseValueConverterForType<T>(this ModelBuilder modelBuilder, ValueConverter converter) + { + var type = typeof(T); + foreach (var entityType in modelBuilder.Model.GetEntityTypes()) + { + foreach (var property in entityType.GetProperties()) + { + if (property.ClrType == type) + { + property.SetValueConverter(converter); + } + } + } + + return modelBuilder; + } + + /// <summary> + /// Specify the default <see cref="DateTimeKind"/>. + /// </summary> + /// <param name="modelBuilder">The model builder to extend.</param> + /// <param name="kind">The <see cref="DateTimeKind"/> to specify.</param> + public static void SetDefaultDateTimeKind(this ModelBuilder modelBuilder, DateTimeKind kind) + { + modelBuilder.UseValueConverterForType<DateTime>(new DateTimeKindValueConverter(kind)); + modelBuilder.UseValueConverterForType<DateTime?>(new DateTimeKindValueConverter(kind)); + } + } +}
\ No newline at end of file diff --git a/Jellyfin.Server.Implementations/ValueConverters/DateTimeKindValueConverter.cs b/Jellyfin.Server.Implementations/ValueConverters/DateTimeKindValueConverter.cs new file mode 100644 index 000000000..8a510898b --- /dev/null +++ b/Jellyfin.Server.Implementations/ValueConverters/DateTimeKindValueConverter.cs @@ -0,0 +1,21 @@ +using System; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Jellyfin.Server.Implementations.ValueConverters +{ + /// <summary> + /// ValueConverter to specify kind. + /// </summary> + public class DateTimeKindValueConverter : ValueConverter<DateTime, DateTime> + { + /// <summary> + /// Initializes a new instance of the <see cref="DateTimeKindValueConverter"/> class. + /// </summary> + /// <param name="kind">The kind to specify.</param> + /// <param name="mappingHints">The mapping hints.</param> + public DateTimeKindValueConverter(DateTimeKind kind, ConverterMappingHints mappingHints = null) + : base(v => v.ToUniversalTime(), v => DateTime.SpecifyKind(v, kind), mappingHints) + { + } + } +}
\ No newline at end of file diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index cb8ae91f5..78f596a5c 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -38,21 +38,18 @@ namespace Jellyfin.Server /// <param name="loggerFactory">The <see cref="ILoggerFactory" /> to be used by the <see cref="CoreAppHost" />.</param> /// <param name="options">The <see cref="StartupOptions" /> to be used by the <see cref="CoreAppHost" />.</param> /// <param name="fileSystem">The <see cref="IFileSystem" /> to be used by the <see cref="CoreAppHost" />.</param> - /// <param name="networkManager">The <see cref="INetworkManager" /> to be used by the <see cref="CoreAppHost" />.</param> /// <param name="collection">The <see cref="IServiceCollection"/> to be used by the <see cref="CoreAppHost"/>.</param> 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/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs index c7fbfa4d0..6bf6f383f 100644 --- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Jellyfin.Networking.Configuration; using Jellyfin.Server.Middleware; using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Builder; @@ -24,8 +25,8 @@ namespace Jellyfin.Server.Extensions // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.), // specifying the Swagger JSON endpoint. - var baseUrl = serverConfigurationManager.Configuration.BaseUrl.Trim('/'); - var apiDocBaseUrl = serverConfigurationManager.Configuration.BaseUrl; + var baseUrl = serverConfigurationManager.GetNetworkConfiguration().BaseUrl.Trim('/'); + var apiDocBaseUrl = serverConfigurationManager.GetNetworkConfiguration().BaseUrl; if (!string.IsNullOrEmpty(baseUrl)) { baseUrl += '/'; diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 6cb88c9f7..618a4e92b 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -236,18 +236,6 @@ namespace Jellyfin.Server.Extensions Description = "API key header parameter" }); - var securitySchemeRef = new OpenApiSecurityScheme - { - Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = AuthenticationSchemes.CustomAuthentication }, - }; - - // TODO: Apply this with an operation filter instead of globally - // https://github.com/domaindrivendev/Swashbuckle.AspNetCore#add-security-definitions-and-requirements - c.AddSecurityRequirement(new OpenApiSecurityRequirement - { - { securitySchemeRef, Array.Empty<string>() } - }); - // Add all xml doc files to swagger generator. var xmlFiles = Directory.GetFiles( AppContext.BaseDirectory, @@ -277,6 +265,7 @@ namespace Jellyfin.Server.Extensions // TODO - remove when all types are supported in System.Text.Json c.AddSwaggerTypeMappings(); + c.OperationFilter<SecurityRequirementsOperationFilter>(); c.OperationFilter<FileResponseFilter>(); c.DocumentFilter<WebsocketModelFilter>(); }); @@ -285,20 +274,17 @@ namespace Jellyfin.Server.Extensions private static void AddSwaggerTypeMappings(this SwaggerGenOptions options) { /* - * TODO remove when System.Text.Json supports non-string keys. - * Used in Jellyfin.Api.Controller.GetChannels. + * TODO remove when System.Text.Json properly supports non-string keys. + * Used in BaseItemDto.ImageBlurHashes */ options.MapType<Dictionary<ImageType, string>>(() => new OpenApiSchema { Type = "object", - Properties = typeof(ImageType).GetEnumNames().ToDictionary( - name => name, - name => new OpenApiSchema - { - Type = "string", - Format = "string" - }) + AdditionalProperties = new OpenApiSchema + { + Type = "string" + } }); /* @@ -312,16 +298,10 @@ namespace Jellyfin.Server.Extensions name => name, name => new OpenApiSchema { - Type = "object", Properties = new Dictionary<string, OpenApiSchema> + Type = "object", + AdditionalProperties = new OpenApiSchema { - { - "string", - new OpenApiSchema - { - Type = "string", - Format = "string" - } - } + Type = "string" } }) }); diff --git a/Jellyfin.Server/Filters/SecurityRequirementsOperationFilter.cs b/Jellyfin.Server/Filters/SecurityRequirementsOperationFilter.cs new file mode 100644 index 000000000..802662ce2 --- /dev/null +++ b/Jellyfin.Server/Filters/SecurityRequirementsOperationFilter.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Jellyfin.Api.Constants; +using Microsoft.AspNetCore.Authorization; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace Jellyfin.Server.Filters +{ + /// <summary> + /// Security requirement operation filter. + /// </summary> + public class SecurityRequirementsOperationFilter : IOperationFilter + { + /// <inheritdoc /> + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + var requiredScopes = new List<string>(); + + // Add all method scopes. + foreach (var attribute in context.MethodInfo.GetCustomAttributes(true)) + { + if (attribute is AuthorizeAttribute authorizeAttribute + && authorizeAttribute.Policy != null + && !requiredScopes.Contains(authorizeAttribute.Policy, StringComparer.Ordinal)) + { + requiredScopes.Add(authorizeAttribute.Policy); + } + } + + // Add controller scopes if any. + var controllerAttributes = context.MethodInfo.DeclaringType?.GetCustomAttributes(true); + if (controllerAttributes != null) + { + foreach (var attribute in controllerAttributes) + { + if (attribute is AuthorizeAttribute authorizeAttribute + && authorizeAttribute.Policy != null + && !requiredScopes.Contains(authorizeAttribute.Policy, StringComparer.Ordinal)) + { + requiredScopes.Add(authorizeAttribute.Policy); + } + } + } + + if (requiredScopes.Count != 0) + { + if (!operation.Responses.ContainsKey("401")) + { + operation.Responses.Add("401", new OpenApiResponse { Description = "Unauthorized" }); + } + + if (!operation.Responses.ContainsKey("403")) + { + operation.Responses.Add("403", new OpenApiResponse { Description = "Forbidden" }); + } + + var scheme = new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = AuthenticationSchemes.CustomAuthentication + } + }; + + operation.Security = new List<OpenApiSecurityRequirement> + { + new OpenApiSecurityRequirement + { + [scheme] = requiredScopes + } + }; + } + } + } +}
\ No newline at end of file diff --git a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs index 9316737bd..c23da2fd6 100644 --- a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs +++ b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using Jellyfin.Networking.Configuration; using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; @@ -42,7 +43,7 @@ namespace Jellyfin.Server.Middleware public async Task Invoke(HttpContext httpContext, IServerConfigurationManager serverConfigurationManager) { var localPath = httpContext.Request.Path.ToString(); - var baseUrlPrefix = serverConfigurationManager.Configuration.BaseUrl; + var baseUrlPrefix = serverConfigurationManager.GetNetworkConfiguration().BaseUrl; if (string.Equals(localPath, baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase) || string.Equals(localPath, baseUrlPrefix, StringComparison.OrdinalIgnoreCase) diff --git a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs index 4bda8f273..525cd9ffe 100644 --- a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs +++ b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs @@ -1,5 +1,6 @@ -using System.Linq; +using System.Net; using System.Threading.Tasks; +using Jellyfin.Networking.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; @@ -34,40 +35,40 @@ namespace Jellyfin.Server.Middleware { if (httpContext.IsLocal()) { + // Running locally. await _next(httpContext).ConfigureAwait(false); return; } - var remoteIp = httpContext.GetNormalizedRemoteIp(); + var remoteIp = httpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback; - if (serverConfigurationManager.Configuration.EnableRemoteAccess) + if (serverConfigurationManager.GetNetworkConfiguration().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. + var 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.ContainsAddress(remoteIp); + if (!serverConfigurationManager.GetNetworkConfiguration().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 9d795145a..8065054a1 100644 --- a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs +++ b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs @@ -1,6 +1,9 @@ using System; using System.Linq; +using System.Net; using System.Threading.Tasks; +using Jellyfin.Networking.Configuration; +using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Http; @@ -32,45 +35,14 @@ namespace Jellyfin.Server.Middleware /// <returns>The async task.</returns> 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 ?? IPAddress.Loopback; - if (hosts.Count == 0) + if (!networkManager.IsInLocalNetwork(host) && !serverConfigurationManager.GetNetworkConfiguration().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); } - - private static string NormalizeConfiguredLocalAddress(string address) - { - var add = address.AsSpan().Trim('/'); - int index = add.IndexOf('/'); - if (index != -1) - { - add = add.Slice(index + 1); - } - - return add.TrimStart('/').ToString(); - } } } diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 97a51c202..a1a7a3053 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -12,9 +12,9 @@ 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 MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Extensions; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Server.Kestrel.Core; @@ -106,6 +106,10 @@ namespace Jellyfin.Server // $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath); + // Enable cl-va P010 interop for tonemapping on Intel VAAPI + Environment.SetEnvironmentVariable("NEOReadDebugKeys", "1"); + Environment.SetEnvironmentVariable("EnableExtendedVaFormats", "1"); + await InitLoggingConfigFile(appPaths).ConfigureAwait(false); // Create an instance of the application configuration to use for application startup @@ -161,7 +165,6 @@ namespace Jellyfin.Server _loggerFactory, options, new ManagedFileSystem(_loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths), - new NetworkManager(_loggerFactory.CreateLogger<NetworkManager>()), serviceCollection); try @@ -272,53 +275,17 @@ 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); - } - - foreach (var address in addresses) - { - _logger.LogInformation("Kestrel listening on {IpAddress}", address); - options.Listen(address, appHost.HttpPort); + var addresses = appHost.NetManager.GetAllBindInterfaces(); - if (appHost.ListenWithHttps) - { - options.Listen( - address, - appHost.HttpsPort, - listenOptions => listenOptions.UseHttps(appHost.Certificate)); - } - else if (builderContext.HostingEnvironment.IsDevelopment()) - { - try - { - options.Listen(address, appHost.HttpsPort, listenOptions => listenOptions.UseHttps()); - } - 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( + options.Listen( + netAdd.Address, appHost.HttpsPort, listenOptions => listenOptions.UseHttps(appHost.Certificate)); } @@ -326,11 +293,18 @@ namespace Jellyfin.Server { try { - options.ListenAnyIP(appHost.HttpsPort, listenOptions => listenOptions.UseHttps()); + options.Listen( + netAdd.Address, + appHost.HttpsPort, + listenOptions => listenOptions.UseHttps()); } - 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/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 6de0dd7ec..7f1d332ee 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -1,5 +1,6 @@ using System.Net.Http.Headers; using System.Net.Mime; +using Jellyfin.Networking.Configuration; using Jellyfin.Server.Extensions; using Jellyfin.Server.Implementations; using Jellyfin.Server.Middleware; @@ -51,7 +52,7 @@ namespace Jellyfin.Server { options.HttpsPort = _serverApplicationHost.HttpsPort; }); - services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.Configuration.KnownProxies); + services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration().KnownProxies); services.AddJellyfinApiSwagger(); @@ -80,7 +81,7 @@ namespace Jellyfin.Server { c.DefaultRequestHeaders.UserAgent.Add(productHeader); c.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue($"({_serverApplicationHost.ApplicationUserAgentAddress})")); - c.DefaultRequestHeaders.Accept.Add(acceptJsonHeader); + c.DefaultRequestHeaders.Accept.Add(acceptXmlHeader); c.DefaultRequestHeaders.Accept.Add(acceptAnyHeader); }) .ConfigurePrimaryHttpMessageHandler(x => new DefaultHttpClientHandler()); @@ -103,7 +104,8 @@ namespace Jellyfin.Server app.UseBaseUrlRedirection(); // Wrap rest of configuration so everything only listens on BaseUrl. - app.Map(_serverConfigurationManager.Configuration.BaseUrl, mainApp => + var config = _serverConfigurationManager.GetNetworkConfiguration(); + app.Map(config.BaseUrl, mainApp => { if (env.IsDevelopment()) { @@ -121,8 +123,7 @@ namespace Jellyfin.Server mainApp.UseCors(); - if (_serverConfigurationManager.Configuration.RequireHttps - && _serverApplicationHost.ListenWithHttps) + if (config.RequireHttps && _serverApplicationHost.ListenWithHttps) { mainApp.UseHttpsRedirection(); } diff --git a/MediaBrowser.Common/Cryptography/PasswordHash.cs b/MediaBrowser.Common/Cryptography/PasswordHash.cs index 3e12536ec..3e2eae1c8 100644 --- a/MediaBrowser.Common/Cryptography/PasswordHash.cs +++ b/MediaBrowser.Common/Cryptography/PasswordHash.cs @@ -101,13 +101,13 @@ namespace MediaBrowser.Common.Cryptography // Check if the string also contains a salt if (splitted.Length - index == 2) { - salt = Hex.Decode(splitted[index++]); - hash = Hex.Decode(splitted[index++]); + salt = Convert.FromHexString(splitted[index++]); + hash = Convert.FromHexString(splitted[index++]); } else { salt = Array.Empty<byte>(); - hash = Hex.Decode(splitted[index++]); + hash = Convert.FromHexString(splitted[index++]); } return new PasswordHash(id, hash, salt, parameters); @@ -144,11 +144,11 @@ namespace MediaBrowser.Common.Cryptography if (_salt.Length != 0) { str.Append('$') - .Append(Hex.Encode(_salt, false)); + .Append(Convert.ToHexString(_salt)); } return str.Append('$') - .Append(Hex.Encode(_hash, false)).ToString(); + .Append(Convert.ToHexString(_hash)).ToString(); } } } diff --git a/MediaBrowser.Common/Hex.cs b/MediaBrowser.Common/Hex.cs deleted file mode 100644 index 559109f74..000000000 --- a/MediaBrowser.Common/Hex.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; - -namespace MediaBrowser.Common -{ - /// <summary> - /// Encoding and decoding hex strings. - /// </summary> - public static class Hex - { - internal const string HexCharsLower = "0123456789abcdef"; - internal const string HexCharsUpper = "0123456789ABCDEF"; - - internal const int LastHexSymbol = 0x66; // 102: f - - /// <summary> - /// Gets a map from an ASCII char to its hex value shifted, - /// e.g. <c>b</c> -> 11. 0xFF means it's not a hex symbol. - /// </summary> - internal static ReadOnlySpan<byte> HexLookup => new byte[] - { - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f - }; - - /// <summary> - /// Encodes each element of the specified bytes as its hexadecimal string representation. - /// </summary> - /// <param name="bytes">An array of bytes.</param> - /// <param name="lowercase"><c>true</c> to use lowercase hexadecimal characters; otherwise <c>false</c>.</param> - /// <returns><c>bytes</c> as a hex string.</returns> - public static string Encode(ReadOnlySpan<byte> bytes, bool lowercase = true) - { - var hexChars = lowercase ? HexCharsLower : HexCharsUpper; - - // TODO: use string.Create when it's supports spans - // Ref: https://github.com/dotnet/corefx/issues/29120 - char[] s = new char[bytes.Length * 2]; - int j = 0; - for (int i = 0; i < bytes.Length; i++) - { - s[j++] = hexChars[bytes[i] >> 4]; - s[j++] = hexChars[bytes[i] & 0x0f]; - } - - return new string(s); - } - - /// <summary> - /// Decodes a hex string into bytes. - /// </summary> - /// <param name="str">The <see cref="string" />.</param> - /// <returns>The decoded bytes.</returns> - public static byte[] Decode(ReadOnlySpan<char> str) - { - if (str.Length == 0) - { - return Array.Empty<byte>(); - } - - var unHex = HexLookup; - - int byteLen = str.Length / 2; - byte[] bytes = new byte[byteLen]; - int i = 0; - for (int j = 0; j < byteLen; j++) - { - byte a; - byte b; - if (str[i] > LastHexSymbol - || (a = unHex[str[i++]]) == 0xFF - || str[i] > LastHexSymbol - || (b = unHex[str[i++]]) == 0xFF) - { - ThrowArgumentException(nameof(str)); - break; // Unreachable - } - - bytes[j] = (byte)((a * 16) | b); - } - - return bytes; - } - - [DoesNotReturn] - private static void ThrowArgumentException(string paramName) - => throw new ArgumentException("Character is not a hex symbol.", paramName); - } -} diff --git a/MediaBrowser.Common/Json/Converters/JsonDateTimeIso8601Converter.cs b/MediaBrowser.Common/Json/Converters/JsonDateTimeIso8601Converter.cs deleted file mode 100644 index 63b344a9d..000000000 --- a/MediaBrowser.Common/Json/Converters/JsonDateTimeIso8601Converter.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Globalization; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace MediaBrowser.Common.Json.Converters -{ - /// <summary> - /// Returns an ISO8601 formatted datetime. - /// </summary> - /// <remarks> - /// Used for legacy compatibility. - /// </remarks> - public class JsonDateTimeIso8601Converter : JsonConverter<DateTime> - { - /// <inheritdoc /> - public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - => reader.GetDateTime(); - - /// <inheritdoc /> - public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) - => writer.WriteStringValue(value.ToString("O", CultureInfo.InvariantCulture)); - } -} diff --git a/MediaBrowser.Common/Json/Converters/JsonGuidConverter.cs b/MediaBrowser.Common/Json/Converters/JsonGuidConverter.cs index d35a761f3..52e08d071 100644 --- a/MediaBrowser.Common/Json/Converters/JsonGuidConverter.cs +++ b/MediaBrowser.Common/Json/Converters/JsonGuidConverter.cs @@ -11,10 +11,23 @@ namespace MediaBrowser.Common.Json.Converters { /// <inheritdoc /> public override Guid Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - => new Guid(reader.GetString()); + { + var guidStr = reader.GetString(); + + return guidStr == null ? Guid.Empty : new Guid(guidStr); + } /// <inheritdoc /> public override void Write(Utf8JsonWriter writer, Guid value, JsonSerializerOptions options) - => writer.WriteStringValue(value); + { + if (value == Guid.Empty) + { + writer.WriteNullValue(); + } + else + { + writer.WriteStringValue(value); + } + } } } diff --git a/MediaBrowser.Common/Json/Converters/JsonVersionConverter.cs b/MediaBrowser.Common/Json/Converters/JsonVersionConverter.cs new file mode 100644 index 000000000..37e6f64e3 --- /dev/null +++ b/MediaBrowser.Common/Json/Converters/JsonVersionConverter.cs @@ -0,0 +1,20 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MediaBrowser.Common.Json.Converters +{ + /// <summary> + /// Converts a Version object or value to/from JSON. + /// </summary> + public class JsonVersionConverter : JsonConverter<Version> + { + /// <inheritdoc /> + public override Version Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => new Version(reader.GetString()); + + /// <inheritdoc /> + public override void Write(Utf8JsonWriter writer, Version value, JsonSerializerOptions options) + => writer.WriteStringValue(value.ToString()); + } +} diff --git a/MediaBrowser.Common/Json/JsonDefaults.cs b/MediaBrowser.Common/Json/JsonDefaults.cs index 9a94664ac..c5050a21d 100644 --- a/MediaBrowser.Common/Json/JsonDefaults.cs +++ b/MediaBrowser.Common/Json/JsonDefaults.cs @@ -40,9 +40,9 @@ namespace MediaBrowser.Common.Json }; options.Converters.Add(new JsonGuidConverter()); + options.Converters.Add(new JsonVersionConverter()); options.Converters.Add(new JsonStringEnumConverter()); options.Converters.Add(new JsonNullableStructConverterFactory()); - options.Converters.Add(new JsonDateTimeIso8601Converter()); return options; } diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs index 12966a474..b6c390d23 100644 --- a/MediaBrowser.Common/Net/INetworkManager.cs +++ b/MediaBrowser.Common/Net/INetworkManager.cs @@ -1,97 +1,233 @@ -#pragma warning disable CS1591 - +#nullable enable using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Net; using System.Net.NetworkInformation; +using MediaBrowser.Common.Net; +using Microsoft.AspNetCore.Http; namespace MediaBrowser.Common.Net { + /// <summary> + /// Interface for the NetworkManager class. + /// </summary> public interface INetworkManager { + /// <summary> + /// Event triggered on network changes. + /// </summary> event EventHandler NetworkChanged; /// <summary> - /// Gets or sets a function to return the list of user defined LAN addresses. + /// Gets the published server urls list. + /// </summary> + Dictionary<IPNetAddress, string> PublishedServerUrls { get; } + + /// <summary> + /// Gets a value indicating whether is all IPv6 interfaces are trusted as internal. + /// </summary> + bool TrustAllIP6Interfaces { get; } + + /// <summary> + /// Gets the remote address filter. + /// </summary> + Collection<IPObject> RemoteAddressFilter { get; } + + /// <summary> + /// Gets or sets a value indicating whether iP6 is enabled. + /// </summary> + bool IsIP6Enabled { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether iP4 is enabled. + /// </summary> + bool IsIP4Enabled { get; set; } + + /// <summary> + /// Calculates the list of interfaces to use for Kestrel. + /// </summary> + /// <returns>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.</returns> + /// <param name="individualInterfaces">When false, return <see cref="IPAddress.Any"/> or <see cref="IPAddress.IPv6Any"/> for all interfaces.</param> + Collection<IPObject> GetAllBindInterfaces(bool individualInterfaces = false); + + /// <summary> + /// Returns a collection containing the loopback interfaces. + /// </summary> + /// <returns>Collection{IPObject}.</returns> + Collection<IPObject> GetLoopbacks(); + + /// <summary> + /// 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. + /// </summary> + /// <param name="source">Source of the request.</param> + /// <param name="port">Optional port returned, if it's part of an override.</param> + /// <returns>IP Address to use, or loopback address if all else fails.</returns> + string GetBindInterface(IPObject source, out int? port); + + /// <summary> + /// 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 <see cref="GetBindInterface(IPObject, out int?)"/>. + /// </summary> + /// <param name="source">Source of the request.</param> + /// <param name="port">Optional port returned, if it's part of an override.</param> + /// <returns>IP Address to use, or loopback address if all else fails.</returns> + string GetBindInterface(HttpRequest source, out int? port); + + /// <summary> + /// 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 <see cref="GetBindInterface(IPObject, out int?)"/>. + /// </summary> + /// <param name="source">IP address of the request.</param> + /// <param name="port">Optional port returned, if it's part of an override.</param> + /// <returns>IP Address to use, or loopback address if all else fails.</returns> + string GetBindInterface(IPAddress source, out int? port); + + /// <summary> + /// 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 <see cref="GetBindInterface(IPObject, out int?)"/>. + /// </summary> + /// <param name="source">Source of the request.</param> + /// <param name="port">Optional port returned, if it's part of an override.</param> + /// <returns>IP Address to use, or loopback address if all else fails.</returns> + string GetBindInterface(string source, out int? port); + + /// <summary> + /// Checks to see if the ip address is specifically excluded in LocalNetworkAddresses. + /// </summary> + /// <param name="address">IP address to check.</param> + /// <returns>True if it is.</returns> + bool IsExcludedInterface(IPAddress address); + + /// <summary> + /// Get a list of all the MAC addresses associated with active interfaces. + /// </summary> + /// <returns>List of MAC addresses.</returns> + IReadOnlyCollection<PhysicalAddress> GetMacAddresses(); + + /// <summary> + /// Checks to see if the IP Address provided matches an interface that has a gateway. + /// </summary> + /// <param name="addressObj">IP to check. Can be an IPAddress or an IPObject.</param> + /// <returns>Result of the check.</returns> + bool IsGatewayInterface(IPObject? addressObj); + + /// <summary> + /// Checks to see if the IP Address provided matches an interface that has a gateway. /// </summary> - Func<string[]> LocalSubnetsFn { get; set; } + /// <param name="addressObj">IP to check. Can be an IPAddress or an IPObject.</param> + /// <returns>Result of the check.</returns> + bool IsGatewayInterface(IPAddress? addressObj); /// <summary> - /// Gets a random port TCP number that is currently available. + /// Returns true if the address is a private address. + /// The configuration option TrustIP6Interfaces overrides this functions behaviour. /// </summary> - /// <returns>System.Int32.</returns> - int GetRandomUnusedTcpPort(); + /// <param name="address">Address to check.</param> + /// <returns>True or False.</returns> + bool IsPrivateAddressRange(IPObject address); /// <summary> - /// Gets a random port UDP number that is currently available. + /// Returns true if the address is part of the user defined LAN. + /// The configuration option TrustIP6Interfaces overrides this functions behaviour. /// </summary> - /// <returns>System.Int32.</returns> - int GetRandomUnusedUdpPort(); + /// <param name="address">IP to check.</param> + /// <returns>True if endpoint is within the LAN range.</returns> + bool IsInLocalNetwork(string address); /// <summary> - /// Returns the MAC Address from first Network Card in Computer. + /// Returns true if the address is part of the user defined LAN. + /// The configuration option TrustIP6Interfaces overrides this functions behaviour. /// </summary> - /// <returns>The MAC Address.</returns> - List<PhysicalAddress> GetMacAddresses(); + /// <param name="address">IP to check.</param> + /// <returns>True if endpoint is within the LAN range.</returns> + bool IsInLocalNetwork(IPObject address); /// <summary> - /// Determines whether [is in private address space] [the specified endpoint]. + /// Returns true if the address is part of the user defined LAN. + /// The configuration option TrustIP6Interfaces overrides this functions behaviour. /// </summary> - /// <param name="endpoint">The endpoint.</param> - /// <returns><c>true</c> if [is in private address space] [the specified endpoint]; otherwise, <c>false</c>.</returns> - bool IsInPrivateAddressSpace(string endpoint); + /// <param name="address">IP to check.</param> + /// <returns>True if endpoint is within the LAN range.</returns> + bool IsInLocalNetwork(IPAddress address); /// <summary> - /// Determines whether [is in private address space 10.x.x.x] [the specified endpoint] and exists in the subnets returned by GetSubnets(). + /// Attempts to convert the token to an IP address, permitting for interface descriptions and indexes. + /// eg. "eth1", or "TP-LINK Wireless USB Adapter". /// </summary> - /// <param name="endpoint">The endpoint.</param> - /// <returns><c>true</c> if [is in private address space 10.x.x.x] [the specified endpoint]; otherwise, <c>false</c>.</returns> - bool IsInPrivateAddressSpaceAndLocalSubnet(string endpoint); + /// <param name="token">Token to parse.</param> + /// <param name="result">Resultant object's ip addresses, if successful.</param> + /// <returns>Success of the operation.</returns> + bool TryParseInterface(string token, out Collection<IPObject>? result); /// <summary> - /// Determines whether [is in local network] [the specified endpoint]. + /// Parses an array of strings into a Collection{IPObject}. /// </summary> - /// <param name="endpoint">The endpoint.</param> - /// <returns><c>true</c> if [is in local network] [the specified endpoint]; otherwise, <c>false</c>.</returns> - bool IsInLocalNetwork(string endpoint); + /// <param name="values">Values to parse.</param> + /// <param name="negated">When true, only include values beginning with !. When false, ignore ! values.</param> + /// <returns>IPCollection object containing the value strings.</returns> + Collection<IPObject> CreateIPCollection(string[] values, bool negated = false); /// <summary> - /// Investigates an caches a list of interface addresses, excluding local link and LAN excluded addresses. + /// Returns all the internal Bind interface addresses. /// </summary> - /// <returns>The list of ip addresses.</returns> - IPAddress[] GetLocalIpAddresses(); + /// <returns>An internal list of interfaces addresses.</returns> + Collection<IPObject> GetInternalBindAddresses(); /// <summary> - /// 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. + /// Checks to see if an IP address is still a valid interface address. /// </summary> - /// <param name="addressString">The address to check.</param> - /// <param name="subnets">If true, check against addresses in the LAN settings surrounded by brackets ([]).</param> - /// <returns><c>true</c>if the address is in at least one of the given subnets, <c>false</c> otherwise.</returns> - bool IsAddressInSubnets(string addressString, string[] subnets); + /// <param name="address">IP address to check.</param> + /// <returns>True if it is.</returns> + bool IsValidInterfaceAddress(IPAddress address); /// <summary> - /// Returns true if address is in the LAN list in the config file. + /// Returns true if the IP address is in the excluded list. /// </summary> - /// <param name="address">The address to check.</param> - /// <param name="excludeInterfaces">If true, check against addresses in the LAN settings which have [] around and return true if it matches the address give in address.</param> - /// <param name="excludeRFC">If true, returns false if address is in the 127.x.x.x or 169.128.x.x range.</param> - /// <returns><c>false</c>if the address isn't in the LAN list, <c>true</c> if the address has been defined as a LAN address.</returns> - bool IsAddressInSubnets(IPAddress address, bool excludeInterfaces, bool excludeRFC); + /// <param name="ip">IP to check.</param> + /// <returns>True if excluded.</returns> + bool IsExcluded(IPAddress ip); /// <summary> - /// Checks if address is in the LAN list in the config file. + /// Returns true if the IP address is in the excluded list. /// </summary> - /// <param name="address1">Source address to check.</param> - /// <param name="address2">Destination address to check against.</param> - /// <param name="subnetMask">Destination subnet to check against.</param> - /// <returns><c>true/false</c>depending on whether address1 is in the same subnet as IPAddress2 with subnetMask.</returns> - bool IsInSameSubnet(IPAddress address1, IPAddress address2, IPAddress subnetMask); + /// <param name="ip">IP to check.</param> + /// <returns>True if excluded.</returns> + bool IsExcluded(EndPoint ip); /// <summary> - /// Returns the subnet mask of an interface with the given address. + /// Gets the filtered LAN ip addresses. /// </summary> - /// <param name="address">The address to check.</param> - /// <returns>Returns the subnet mask of an interface with the given address, or null if an interface match cannot be found.</returns> - IPAddress GetLocalIpSubnetMask(IPAddress address); + /// <param name="filter">Optional filter for the list.</param> + /// <returns>Returns a filtered list of LAN addresses.</returns> + Collection<IPObject> GetFilteredLANSubnets(Collection<IPObject>? filter = null); } } diff --git a/MediaBrowser.Common/Plugins/LocalPlugin.cs b/MediaBrowser.Common/Plugins/LocalPlugin.cs index 7927c663d..c97e75a3b 100644 --- a/MediaBrowser.Common/Plugins/LocalPlugin.cs +++ b/MediaBrowser.Common/Plugins/LocalPlugin.cs @@ -1,11 +1,11 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; namespace MediaBrowser.Common.Plugins { /// <summary> - /// Local plugin struct. + /// Local plugin class. /// </summary> public class LocalPlugin : IEquatable<LocalPlugin> { @@ -106,6 +106,12 @@ namespace MediaBrowser.Common.Plugins /// <inheritdoc /> public bool Equals(LocalPlugin other) { + // Do not use == or != for comparison as this class overrides the operators. + if (object.ReferenceEquals(other, null)) + { + return false; + } + return Name.Equals(other.Name, StringComparison.OrdinalIgnoreCase) && Id.Equals(other.Id); } diff --git a/MediaBrowser.Controller/Channels/IChannelManager.cs b/MediaBrowser.Controller/Channels/IChannelManager.cs index 9a9d22d33..ddae7dbd3 100644 --- a/MediaBrowser.Controller/Channels/IChannelManager.cs +++ b/MediaBrowser.Controller/Channels/IChannelManager.cs @@ -24,7 +24,7 @@ namespace MediaBrowser.Controller.Channels /// </summary> /// <param name="id">The identifier.</param> /// <returns>ChannelFeatures.</returns> - ChannelFeatures GetChannelFeatures(string id); + ChannelFeatures GetChannelFeatures(Guid? id); /// <summary> /// Gets all channel features. diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 1b25fbdbb..d8fad3bfb 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1385,7 +1385,6 @@ namespace MediaBrowser.Controller.Entities new List<FileSystemMetadata>(); var ownedItemsChanged = await RefreshedOwnedItems(options, files, cancellationToken).ConfigureAwait(false); - await LibraryManager.UpdateImagesAsync(this).ConfigureAwait(false); // ensure all image properties in DB are fresh if (ownedItemsChanged) { diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 57d04ddfa..23f4c00c1 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -354,11 +354,6 @@ namespace MediaBrowser.Controller.Entities { await currentChild.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false); } - else - { - // metadata is up-to-date; make sure DB has correct images dimensions and hash - await LibraryManager.UpdateImagesAsync(currentChild).ConfigureAwait(false); - } continue; } diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs index 07f381881..6320b01b8 100644 --- a/MediaBrowser.Controller/Entities/Video.cs +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -143,26 +143,6 @@ namespace MediaBrowser.Controller.Entities /// <value>The video3 D format.</value> public Video3DFormat? Video3DFormat { get; set; } - public string[] GetPlayableStreamFileNames() - { - var videoType = VideoType; - - if (videoType == VideoType.Iso && IsoType == Model.Entities.IsoType.BluRay) - { - videoType = VideoType.BluRay; - } - else if (videoType == VideoType.Iso && IsoType == Model.Entities.IsoType.Dvd) - { - videoType = VideoType.Dvd; - } - else - { - return Array.Empty<string>(); - } - - throw new NotImplementedException(); - } - /// <summary> /// Gets or sets the aspect ratio. /// </summary> @@ -415,31 +395,6 @@ namespace MediaBrowser.Controller.Entities return updateType; } - public static string[] QueryPlayableStreamFiles(string rootPath, VideoType videoType) - { - if (videoType == VideoType.Dvd) - { - return FileSystem.GetFiles(rootPath, new[] { ".vob" }, false, true) - .OrderByDescending(i => i.Length) - .ThenBy(i => i.FullName) - .Take(1) - .Select(i => i.FullName) - .ToArray(); - } - - if (videoType == VideoType.BluRay) - { - return FileSystem.GetFiles(rootPath, new[] { ".m2ts" }, false, true) - .OrderByDescending(i => i.Length) - .ThenBy(i => i.FullName) - .Take(1) - .Select(i => i.FullName) - .ToArray(); - } - - return Array.Empty<string>(); - } - /// <summary> /// Gets a value indicating whether [is3 D]. /// </summary> diff --git a/MediaBrowser.Controller/IResourceFileManager.cs b/MediaBrowser.Controller/IResourceFileManager.cs deleted file mode 100644 index 26f0424b7..000000000 --- a/MediaBrowser.Controller/IResourceFileManager.cs +++ /dev/null @@ -1,9 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Controller -{ - public interface IResourceFileManager - { - string GetResourcePath(string basePath, string virtualPath); - } -} diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index ffbb147b0..2456da826 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using MediaBrowser.Common; using MediaBrowser.Common.Plugins; using MediaBrowser.Model.System; +using Microsoft.AspNetCore.Http; namespace MediaBrowser.Controller { @@ -56,41 +57,42 @@ namespace MediaBrowser.Controller /// <summary> /// Gets the system info. /// </summary> - /// <param name="cancellationToken">A cancellation token that can be used to cancel the task.</param> + /// <param name="source">The originator of the request.</param> /// <returns>SystemInfo.</returns> - Task<SystemInfo> GetSystemInfo(CancellationToken cancellationToken = default); + SystemInfo GetSystemInfo(IPAddress source); - Task<PublicSystemInfo> GetPublicSystemInfo(CancellationToken cancellationToken = default); + PublicSystemInfo GetPublicSystemInfo(IPAddress address); /// <summary> - /// 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. + /// Gets a URL specific for the request. /// </summary> - /// <param name="cancellationToken">A cancellation token that can be used to cancel the task.</param> - /// <returns>A list containing all the local IP addresses of the server.</returns> - Task<List<IPAddress>> GetLocalIpAddresses(CancellationToken cancellationToken = default); + /// <param name="request">The <see cref="HttpRequest"/> instance.</param> + /// <param name="port">Optional port number.</param> + /// <returns>An accessible URL.</returns> + string GetSmartApiUrl(HttpRequest request, int? port = null); /// <summary> - /// 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 <see cref="GetLocalIpAddresses"/>. HTTPS will be preferred when available. + /// Gets a URL specific for the request. /// </summary> - /// <param name="cancellationToken">A cancellation token that can be used to cancel the task.</param> - /// <returns>The server URL.</returns> - Task<string> GetLocalApiUrl(CancellationToken cancellationToken = default); + /// <param name="remoteAddr">The remote <see cref="IPAddress"/> of the connection.</param> + /// <param name="port">Optional port number.</param> + /// <returns>An accessible URL.</returns> + string GetSmartApiUrl(IPAddress remoteAddr, int? port = null); /// <summary> - /// Gets a localhost URL that can be used to access the API using the loop-back IP address (127.0.0.1) - /// over HTTP (not HTTPS). + /// Gets a URL specific for the request. /// </summary> - /// <returns>The API URL.</returns> - string GetLoopbackHttpApiUrl(); + /// <param name="hostname">The hostname used in the connection.</param> + /// <param name="port">Optional port number.</param> + /// <returns>An accessible URL.</returns> + string GetSmartApiUrl(string hostname, int? port = null); /// <summary> - /// Gets a local (LAN) URL that can be used to access the API. HTTPS will be preferred when available. + /// Gets a localhost URL that can be used to access the API using the loop-back IP address. + /// over HTTP (not HTTPS). /// </summary> - /// <param name="address">The IP address to use as the hostname in the URL.</param> /// <returns>The API URL.</returns> - string GetLocalApiUrl(IPAddress address); + string GetLoopbackHttpApiUrl(); /// <summary> /// Gets a local (LAN) URL that can be used to access the API. @@ -106,7 +108,7 @@ namespace MediaBrowser.Controller /// preferring the HTTPS port, if available. /// </param> /// <returns>The API URL.</returns> - string GetLocalApiUrl(ReadOnlySpan<char> hostname, string scheme = null, int? port = null); + string GetLocalApiUrl(string hostname, string scheme = null, int? port = null); /// <summary> /// Open a URL in an external browser window. diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index c7c79df76..24b101694 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -571,6 +571,10 @@ namespace MediaBrowser.Controller.Library string videoPath, string[] files); + void RunMetadataSavers(IReadOnlyList<BaseItem> items, ItemUpdateType updateReason); + BaseItem GetParentItem(string parentId, Guid? userId); + + BaseItem GetParentItem(Guid? parentId, Guid? userId); } } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 9a6f1231f..91a03e647 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -112,6 +112,16 @@ namespace MediaBrowser.Controller.MediaEncoding return _mediaEncoder.SupportsHwaccel("vaapi"); } + private bool IsTonemappingSupported(EncodingJobInfo state, EncodingOptions options) + { + var videoStream = state.VideoStream; + return IsColorDepth10(state) + && _mediaEncoder.SupportsHwaccel("opencl") + && options.EnableTonemapping + && !string.IsNullOrEmpty(videoStream.VideoRange) + && videoStream.VideoRange.Contains("HDR", StringComparison.OrdinalIgnoreCase); + } + /// <summary> /// Gets the name of the output video codec. /// </summary> @@ -380,25 +390,9 @@ namespace MediaBrowser.Controller.MediaEncoding public string GetInputPathArgument(EncodingJobInfo state) { - var protocol = state.InputProtocol; var mediaPath = state.MediaPath ?? string.Empty; - string[] inputPath; - if (state.IsInputVideo - && !(state.VideoType == VideoType.Iso && state.IsoMount == null)) - { - inputPath = MediaEncoderHelpers.GetInputArgument( - _fileSystem, - mediaPath, - state.IsoMount, - state.PlayableStreamFileNames); - } - else - { - inputPath = new[] { mediaPath }; - } - - return _mediaEncoder.GetInputArgument(inputPath, protocol); + return _mediaEncoder.GetInputArgument(mediaPath, state.MediaSource); } /// <summary> @@ -468,6 +462,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); var isMacOS = RuntimeInformation.IsOSPlatform(OSPlatform.OSX); + var isTonemappingSupported = IsTonemappingSupported(state, encodingOptions); if (!IsCopyCodec(outputVideoCodec)) { @@ -477,10 +472,23 @@ namespace MediaBrowser.Controller.MediaEncoding { if (isVaapiDecoder) { - arg.Append("-hwaccel_output_format vaapi ") - .Append("-vaapi_device ") - .Append(encodingOptions.VaapiDevice) - .Append(' '); + if (isTonemappingSupported) + { + arg.Append("-init_hw_device vaapi=va:") + .Append(encodingOptions.VaapiDevice) + .Append(' ') + .Append("-init_hw_device opencl=ocl@va ") + .Append("-hwaccel_device va ") + .Append("-hwaccel_output_format vaapi ") + .Append("-filter_hw_device ocl "); + } + else + { + arg.Append("-hwaccel_output_format vaapi ") + .Append("-vaapi_device ") + .Append(encodingOptions.VaapiDevice) + .Append(' '); + } } else if (!isVaapiDecoder && isVaapiEncoder) { @@ -529,13 +537,7 @@ namespace MediaBrowser.Controller.MediaEncoding && (string.Equals(encodingOptions.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase) && isNvdecHevcDecoder || isSwDecoder) || (string.Equals(encodingOptions.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase) && isD3d11vaDecoder || isSwDecoder)) { - var isColorDepth10 = IsColorDepth10(state); - - if (isColorDepth10 - && _mediaEncoder.SupportsHwaccel("opencl") - && encodingOptions.EnableTonemapping - && !string.IsNullOrEmpty(state.VideoStream.VideoRange) - && state.VideoStream.VideoRange.Contains("HDR", StringComparison.OrdinalIgnoreCase)) + if (isTonemappingSupported) { arg.Append("-init_hw_device opencl=ocl:") .Append(encodingOptions.OpenclDevice) @@ -1866,6 +1868,19 @@ namespace MediaBrowser.Controller.MediaEncoding var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, options) ?? string.Empty; var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + 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 isTonemappingSupported = IsTonemappingSupported(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) + { + return GetOutputSizeParam(state, options, outputVideoCodec); + } + // Setup subtitle scaling if (state.VideoStream != null && state.VideoStream.Width.HasValue && state.VideoStream.Height.HasValue) { @@ -1997,6 +2012,7 @@ namespace MediaBrowser.Controller.MediaEncoding public List<string> GetScalingFilters( EncodingJobInfo state, + EncodingOptions options, int? videoWidth, int? videoHeight, Video3DFormat? threedFormat, @@ -2035,6 +2051,19 @@ namespace MediaBrowser.Controller.MediaEncoding || state.DeInterlace("h265", true) || state.DeInterlace("hevc", true); + var isTonemappingSupported = IsTonemappingSupported(state, options); + var isTonemappingSupportedOnVaapi = string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase) && !qsv_or_vaapi; + + var outputPixFmt = string.Empty; + if (isTonemappingSupported && isTonemappingSupportedOnVaapi) + { + outputPixFmt = "format=p010:out_range=limited"; + } + else + { + outputPixFmt = "format=nv12"; + } + if (!videoWidth.HasValue || outputWidth != videoWidth.Value || !videoHeight.HasValue @@ -2045,10 +2074,11 @@ namespace MediaBrowser.Controller.MediaEncoding filters.Add( string.Format( CultureInfo.InvariantCulture, - "{0}=w={1}:h={2}:format=nv12{3}", + "{0}=w={1}:h={2}{3}{4}", qsv_or_vaapi ? "vpp_qsv" : "scale_vaapi", outputWidth, outputHeight, + ":" + outputPixFmt, (qsv_or_vaapi && isDeintEnabled) ? ":deinterlace=1" : string.Empty)); } else @@ -2056,8 +2086,9 @@ namespace MediaBrowser.Controller.MediaEncoding filters.Add( string.Format( CultureInfo.InvariantCulture, - "{0}=format=nv12{1}", + "{0}={1}{2}", qsv_or_vaapi ? "vpp_qsv" : "scale_vaapi", + outputPixFmt, (qsv_or_vaapi && isDeintEnabled) ? ":deinterlace=1" : string.Empty)); } } @@ -2290,6 +2321,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isSwDecoder = string.IsNullOrEmpty(videoDecoder); var isD3d11vaDecoder = videoDecoder.IndexOf("d3d11va", StringComparison.OrdinalIgnoreCase) != -1; var isVaapiDecoder = videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1; + var isVaapiEncoder = outputVideoCodec.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1; var isVaapiH264Encoder = outputVideoCodec.IndexOf("h264_vaapi", StringComparison.OrdinalIgnoreCase) != -1; var isVaapiHevcEncoder = outputVideoCodec.IndexOf("hevc_vaapi", StringComparison.OrdinalIgnoreCase) != -1; var isQsvH264Encoder = outputVideoCodec.IndexOf("h264_qsv", StringComparison.OrdinalIgnoreCase) != -1; @@ -2300,6 +2332,10 @@ namespace MediaBrowser.Controller.MediaEncoding 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 isTonemappingSupportedOnVaapi = string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isVaapiH264Encoder || isVaapiHevcEncoder); var hasTextSubs = state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; @@ -2311,18 +2347,14 @@ 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 ((string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase) && isNvdecHevcDecoder || isSwDecoder) - || (string.Equals(options.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase) && isD3d11vaDecoder || isSwDecoder)) + if (isTonemappingSupportedOnNvenc || isTonemappingSupportedOnAmf || isTonemappingSupportedOnVaapi) { // 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. // NVIDIA Pascal and Turing or higher are recommended. // AMD Polaris and Vega or higher are recommended. - if (isColorDepth10 - && _mediaEncoder.SupportsHwaccel("opencl") - && options.EnableTonemapping - && !string.IsNullOrEmpty(videoStream.VideoRange) - && videoStream.VideoRange.Contains("HDR", StringComparison.OrdinalIgnoreCase)) + // Intel Kaby Lake or newer is required. + if (isTonemappingSupported) { var parameters = "tonemap_opencl=format=nv12:primaries=bt709:transfer=bt709:matrix=bt709:tonemap={0}:desat={1}:threshold={2}:peak={3}"; @@ -2355,10 +2387,35 @@ namespace MediaBrowser.Controller.MediaEncoding filters.Add("format=p010"); } - // 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"); + if (isNvdecHevcDecoder || 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"); + } + + if (isVaapiDecoder) + { + isScalingInAdvance = true; + filters.AddRange( + GetScalingFilters( + state, + options, + inputWidth, + inputHeight, + threeDFormat, + videoDecoder, + outputVideoCodec, + request.Width, + request.Height, + request.MaxWidth, + request.MaxHeight)); + + // hwmap the HDR data to opencl device by cl-va p010 interop. + filters.Add("hwmap"); + } + filters.Add( string.Format( CultureInfo.InvariantCulture, @@ -2369,33 +2426,46 @@ namespace MediaBrowser.Controller.MediaEncoding options.TonemappingPeak, options.TonemappingParam, options.TonemappingRange)); - filters.Add("hwdownload"); - if (isLibX264Encoder - || isLibX265Encoder - || hasGraphicalSubs - || (isNvdecHevcDecoder && isDeinterlaceHevc) - || (!isNvdecHevcDecoder && isDeinterlaceH264 || isDeinterlaceHevc)) + if (isNvdecHevcDecoder || isSwDecoder || isD3d11vaDecoder) { - filters.Add("format=nv12"); + filters.Add("hwdownload"); + } + + if (isSwDecoder || isD3d11vaDecoder) + { + if (isLibX264Encoder + || isLibX265Encoder + || hasGraphicalSubs + || (isNvdecHevcDecoder && isDeinterlaceHevc) + || (!isNvdecHevcDecoder && isDeinterlaceH264 || isDeinterlaceHevc)) + { + filters.Add("format=nv12"); + } + } + + if (isVaapiDecoder) + { + // Reverse the data route from opencl to vaapi. + filters.Add("hwmap=derive_device=vaapi:reverse=1"); } } } - // When the input may or may not be hardware VAAPI decodable - if (isVaapiH264Encoder || isVaapiHevcEncoder) + // When the input may or may not be hardware VAAPI decodable. + if ((isVaapiH264Encoder || isVaapiHevcEncoder) && !isTonemappingSupported && !isTonemappingSupportedOnVaapi) { filters.Add("format=nv12|vaapi"); filters.Add("hwupload"); } - // When burning in graphical subtitles using overlay_qsv, upload videostream to the same qsv context + // When burning in graphical subtitles using overlay_qsv, upload videostream to the same qsv context. else if (isLinux && hasGraphicalSubs && (isQsvH264Encoder || isQsvHevcEncoder)) { filters.Add("hwupload=extra_hw_frames=64"); } - // If we're hardware VAAPI decoding and software encoding, download frames from the decoder first + // If we're hardware VAAPI decoding and software encoding, download frames from the decoder first. else if (IsVaapiSupported(state) && isVaapiDecoder && (isLibX264Encoder || isLibX265Encoder)) { var codec = videoStream.Codec.ToLowerInvariant(); @@ -2422,7 +2492,7 @@ namespace MediaBrowser.Controller.MediaEncoding } } - // Add hardware deinterlace filter before scaling filter + // Add hardware deinterlace filter before scaling filter. if (isDeinterlaceH264) { if (isVaapiH264Encoder) @@ -2435,7 +2505,7 @@ namespace MediaBrowser.Controller.MediaEncoding } } - // Add software deinterlace filter before scaling filter + // Add software deinterlace filter before scaling filter. if ((isDeinterlaceH264 || isDeinterlaceHevc) && !isVaapiH264Encoder && !isVaapiHevcEncoder @@ -2467,6 +2537,7 @@ namespace MediaBrowser.Controller.MediaEncoding filters.AddRange( GetScalingFilters( state, + options, inputWidth, inputHeight, threeDFormat, @@ -2483,6 +2554,13 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasTextSubs) { + // Convert hw context from ocl to va. + // For tonemapping and text subs burn-in. + if (isTonemappingSupported && isTonemappingSupportedOnVaapi) + { + filters.Add("scale_vaapi"); + } + // Test passed on Intel and AMD gfx filters.Add("hwmap=mode=read+write"); filters.Add("format=nv12"); @@ -2579,18 +2657,10 @@ namespace MediaBrowser.Controller.MediaEncoding } } - public string GetProbeSizeArgument(int numInputFiles) - => numInputFiles > 1 ? "-probesize " + _configuration.GetFFmpegProbeSize() : string.Empty; - - public string GetAnalyzeDurationArgument(int numInputFiles) - => numInputFiles > 1 ? "-analyzeduration " + _configuration.GetFFmpegAnalyzeDuration() : string.Empty; - public string GetInputModifier(EncodingJobInfo state, EncodingOptions encodingOptions) { var inputModifier = string.Empty; - - var numInputFiles = state.PlayableStreamFileNames.Length > 0 ? state.PlayableStreamFileNames.Length : 1; - var probeSizeArgument = GetProbeSizeArgument(numInputFiles); + var probeSizeArgument = string.Empty; string analyzeDurationArgument; if (state.MediaSource.AnalyzeDurationMs.HasValue) @@ -2599,7 +2669,7 @@ namespace MediaBrowser.Controller.MediaEncoding } else { - analyzeDurationArgument = GetAnalyzeDurationArgument(numInputFiles); + analyzeDurationArgument = string.Empty; } if (!string.IsNullOrEmpty(probeSizeArgument)) @@ -2783,32 +2853,6 @@ namespace MediaBrowser.Controller.MediaEncoding state.IsoType = mediaSource.IsoType; - if (mediaSource.VideoType.HasValue) - { - state.VideoType = mediaSource.VideoType.Value; - - if (mediaSource.VideoType.Value == VideoType.BluRay || mediaSource.VideoType.Value == VideoType.Dvd) - { - state.PlayableStreamFileNames = Video.QueryPlayableStreamFiles(state.MediaPath, mediaSource.VideoType.Value).Select(Path.GetFileName).ToArray(); - } - else if (mediaSource.VideoType.Value == VideoType.Iso && state.IsoType == IsoType.BluRay) - { - state.PlayableStreamFileNames = Video.QueryPlayableStreamFiles(state.MediaPath, VideoType.BluRay).Select(Path.GetFileName).ToArray(); - } - else if (mediaSource.VideoType.Value == VideoType.Iso && state.IsoType == IsoType.Dvd) - { - state.PlayableStreamFileNames = Video.QueryPlayableStreamFiles(state.MediaPath, VideoType.Dvd).Select(Path.GetFileName).ToArray(); - } - else - { - state.PlayableStreamFileNames = Array.Empty<string>(); - } - } - else - { - state.PlayableStreamFileNames = Array.Empty<string>(); - } - if (mediaSource.Timestamp.HasValue) { state.InputTimestamp = mediaSource.Timestamp.Value; diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs index 52794a69b..dacd6dea6 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs @@ -33,10 +33,6 @@ namespace MediaBrowser.Controller.MediaEncoding public bool IsInputVideo { get; set; } - public IIsoMount IsoMount { get; set; } - - public string[] PlayableStreamFileNames { get; set; } - public string OutputAudioCodec { get; set; } public int? OutputVideoBitrate { get; set; } @@ -313,7 +309,6 @@ namespace MediaBrowser.Controller.MediaEncoding { TranscodingType = jobType; RemoteHttpHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - PlayableStreamFileNames = Array.Empty<string>(); SupportedAudioCodecs = Array.Empty<string>(); SupportedVideoCodecs = Array.Empty<string>(); SupportedSubtitleCodecs = Array.Empty<string>(); diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index f6bc1f4de..e7f042d2f 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; @@ -61,18 +62,18 @@ namespace MediaBrowser.Controller.MediaEncoding /// <summary> /// Extracts the video image. /// </summary> - Task<string> ExtractVideoImage(string[] inputFiles, string container, MediaProtocol protocol, MediaStream videoStream, Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken); + Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream videoStream, Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken); - Task<string> ExtractVideoImage(string[] inputFiles, string container, MediaProtocol protocol, MediaStream imageStream, int? imageStreamIndex, CancellationToken cancellationToken); + Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, CancellationToken cancellationToken); /// <summary> /// Extracts the video images on interval. /// </summary> Task ExtractVideoImagesOnInterval( - string[] inputFiles, + string inputFile, string container, MediaStream videoStream, - MediaProtocol protocol, + MediaSourceInfo mediaSource, Video3DFormat? threedFormat, TimeSpan interval, string targetDirectory, @@ -91,10 +92,10 @@ namespace MediaBrowser.Controller.MediaEncoding /// <summary> /// Gets the input argument. /// </summary> - /// <param name="inputFiles">The input files.</param> - /// <param name="protocol">The protocol.</param> + /// <param name="inputFile">The input file.</param> + /// <param name="mediaSource">The mediaSource.</param> /// <returns>System.String.</returns> - string GetInputArgument(IReadOnlyList<string> inputFiles, MediaProtocol protocol); + string GetInputArgument(string inputFile, MediaSourceInfo mediaSource); /// <summary> /// Gets the time parameter. diff --git a/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs b/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs index ce53c23ad..281d50372 100644 --- a/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs +++ b/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs @@ -13,38 +13,5 @@ namespace MediaBrowser.Controller.MediaEncoding /// </summary> public static class MediaEncoderHelpers { - /// <summary> - /// Gets the input argument. - /// </summary> - /// <param name="fileSystem">The file system.</param> - /// <param name="videoPath">The video path.</param> - /// <param name="isoMount">The iso mount.</param> - /// <param name="playableStreamFileNames">The playable stream file names.</param> - /// <returns>string[].</returns> - public static string[] GetInputArgument(IFileSystem fileSystem, string videoPath, IIsoMount isoMount, IReadOnlyCollection<string> playableStreamFileNames) - { - if (playableStreamFileNames.Count > 0) - { - if (isoMount == null) - { - return GetPlayableStreamFiles(fileSystem, videoPath, playableStreamFileNames); - } - - return GetPlayableStreamFiles(fileSystem, isoMount.MountedPath, playableStreamFileNames); - } - - return new[] { videoPath }; - } - - private static string[] GetPlayableStreamFiles(IFileSystem fileSystem, string rootPath, IEnumerable<string> filenames) - { - var allFiles = fileSystem - .GetFilePaths(rootPath, true) - .ToArray(); - - return filenames.Select(name => allFiles.FirstOrDefault(f => string.Equals(Path.GetFileName(f), name, StringComparison.OrdinalIgnoreCase))) - .Where(f => !string.IsNullOrEmpty(f)) - .ToArray(); - } } } diff --git a/MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs b/MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs index 59729de49..2cb04bdc4 100644 --- a/MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs +++ b/MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs @@ -1,9 +1,7 @@ #pragma warning disable CS1591 -using System; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; -using MediaBrowser.Model.IO; namespace MediaBrowser.Controller.MediaEncoding { @@ -14,14 +12,5 @@ namespace MediaBrowser.Controller.MediaEncoding public bool ExtractChapters { get; set; } public DlnaProfileType MediaType { get; set; } - - public IIsoMount MountedIso { get; set; } - - public string[] PlayableStreamFileNames { get; set; } - - public MediaInfoRequest() - { - PlayableStreamFileNames = Array.Empty<string>(); - } } } diff --git a/MediaBrowser.Controller/Net/AuthorizationInfo.cs b/MediaBrowser.Controller/Net/AuthorizationInfo.cs index 0194c596f..93573e08e 100644 --- a/MediaBrowser.Controller/Net/AuthorizationInfo.cs +++ b/MediaBrowser.Controller/Net/AuthorizationInfo.cs @@ -58,5 +58,10 @@ namespace MediaBrowser.Controller.Net /// Gets or sets a value indicating whether the token is authenticated. /// </summary> public bool IsAuthenticated { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether the request has a token. + /// </summary> + public bool HasToken { get; set; } } } diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs index bc940d0b8..e8aeabf9d 100644 --- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs +++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs @@ -87,19 +87,19 @@ namespace MediaBrowser.MediaEncoding.Attachments MediaAttachment mediaAttachment, CancellationToken cancellationToken) { - var attachmentPath = await GetReadableFile(mediaSource.Path, mediaSource.Path, mediaSource.Protocol, mediaAttachment, cancellationToken).ConfigureAwait(false); + var attachmentPath = await GetReadableFile(mediaSource.Path, mediaSource.Path, mediaSource, mediaAttachment, cancellationToken).ConfigureAwait(false); return File.OpenRead(attachmentPath); } private async Task<string> GetReadableFile( string mediaPath, string inputFile, - MediaProtocol protocol, + MediaSourceInfo mediaSource, MediaAttachment mediaAttachment, CancellationToken cancellationToken) { - var outputPath = GetAttachmentCachePath(mediaPath, protocol, mediaAttachment.Index); - await ExtractAttachment(inputFile, protocol, mediaAttachment.Index, outputPath, cancellationToken) + var outputPath = GetAttachmentCachePath(mediaPath, mediaSource, mediaAttachment.Index); + await ExtractAttachment(inputFile, mediaSource, mediaAttachment.Index, outputPath, cancellationToken) .ConfigureAwait(false); return outputPath; @@ -107,7 +107,7 @@ namespace MediaBrowser.MediaEncoding.Attachments private async Task ExtractAttachment( string inputFile, - MediaProtocol protocol, + MediaSourceInfo mediaSource, int attachmentStreamIndex, string outputPath, CancellationToken cancellationToken) @@ -121,7 +121,7 @@ namespace MediaBrowser.MediaEncoding.Attachments if (!File.Exists(outputPath)) { await ExtractAttachmentInternal( - _mediaEncoder.GetInputArgument(new[] { inputFile }, protocol), + _mediaEncoder.GetInputArgument(inputFile, mediaSource), attachmentStreamIndex, outputPath, cancellationToken).ConfigureAwait(false); @@ -234,10 +234,10 @@ namespace MediaBrowser.MediaEncoding.Attachments } } - private string GetAttachmentCachePath(string mediaPath, MediaProtocol protocol, int attachmentStreamIndex) + private string GetAttachmentCachePath(string mediaPath, MediaSourceInfo mediaSource, int attachmentStreamIndex) { string filename; - if (protocol == MediaProtocol.File) + if (mediaSource.Protocol == MediaProtocol.File) { var date = _fileSystem.GetLastWriteTimeUtc(mediaPath); filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D", CultureInfo.InvariantCulture); diff --git a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs index 63310fdf6..d0ea0429b 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs @@ -1,53 +1,44 @@ #pragma warning disable CS1591 using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using MediaBrowser.Model.MediaInfo; namespace MediaBrowser.MediaEncoding.Encoder { public static class EncodingUtils { - public static string GetInputArgument(IReadOnlyList<string> inputFiles, MediaProtocol protocol) + public static string GetInputArgument(string inputPrefix, string inputFile, MediaProtocol protocol) { if (protocol != MediaProtocol.File) { - var url = inputFiles[0]; - - return string.Format(CultureInfo.InvariantCulture, "\"{0}\"", url); + return string.Format(CultureInfo.InvariantCulture, "\"{0}\"", inputFile); } - return GetConcatInputArgument(inputFiles); + return GetConcatInputArgument(inputFile, inputPrefix); } /// <summary> /// Gets the concat input argument. /// </summary> - /// <param name="inputFiles">The input files.</param> + /// <param name="inputFile">The input file.</param> + /// <param name="inputPrefix">The input prefix.</param> /// <returns>System.String.</returns> - private static string GetConcatInputArgument(IReadOnlyList<string> inputFiles) + private static string GetConcatInputArgument(string inputFile, string inputPrefix) { // Get all streams // If there's more than one we'll need to use the concat command - if (inputFiles.Count > 1) - { - var files = string.Join("|", inputFiles.Select(NormalizePath)); - - return string.Format(CultureInfo.InvariantCulture, "concat:\"{0}\"", files); - } - // Determine the input path for video files - return GetFileInputArgument(inputFiles[0]); + return GetFileInputArgument(inputFile, inputPrefix); } /// <summary> /// Gets the file input argument. /// </summary> /// <param name="path">The path.</param> + /// <param name="inputPrefix">The path prefix.</param> /// <returns>System.String.</returns> - private static string GetFileInputArgument(string path) + private static string GetFileInputArgument(string path, string inputPrefix) { if (path.IndexOf("://", StringComparison.Ordinal) != -1) { @@ -57,7 +48,7 @@ namespace MediaBrowser.MediaEncoding.Encoder // Quotes are valid path characters in linux and they need to be escaped here with a leading \ path = NormalizePath(path); - return string.Format(CultureInfo.InvariantCulture, "file:\"{0}\"", path); + return string.Format(CultureInfo.InvariantCulture, "{1}:\"{0}\"", path, inputPrefix); } /// <summary> diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 5f60c09ae..380894278 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -18,6 +18,7 @@ using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.MediaEncoding.Probing; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; @@ -34,9 +35,14 @@ namespace MediaBrowser.MediaEncoding.Encoder public class MediaEncoder : IMediaEncoder, IDisposable { /// <summary> - /// The default image extraction timeout in milliseconds. + /// The default SDR image extraction timeout in milliseconds. /// </summary> - internal const int DefaultImageExtractionTimeout = 5000; + internal const int DefaultSdrImageExtractionTimeout = 10000; + + /// <summary> + /// The default HDR image extraction timeout in milliseconds. + /// </summary> + internal const int DefaultHdrImageExtractionTimeout = 20000; /// <summary> /// The us culture. @@ -83,8 +89,6 @@ namespace MediaBrowser.MediaEncoding.Encoder _jsonSerializerOptions = JsonDefaults.GetOptions(); } - private EncodingHelper EncodingHelper => _encodingHelperFactory.Value; - /// <inheritdoc /> public string EncoderPath => _ffmpegPath; @@ -320,33 +324,24 @@ namespace MediaBrowser.MediaEncoding.Encoder public Task<MediaInfo> GetMediaInfo(MediaInfoRequest request, CancellationToken cancellationToken) { var extractChapters = request.MediaType == DlnaProfileType.Video && request.ExtractChapters; + var inputFile = request.MediaSource.Path; - var inputFiles = MediaEncoderHelpers.GetInputArgument(_fileSystem, request.MediaSource.Path, request.MountedIso, request.PlayableStreamFileNames); - - var probeSize = EncodingHelper.GetProbeSizeArgument(inputFiles.Length); - string analyzeDuration; + string analyzeDuration = string.Empty; if (request.MediaSource.AnalyzeDurationMs > 0) { analyzeDuration = "-analyzeduration " + (request.MediaSource.AnalyzeDurationMs * 1000).ToString(); } - else - { - analyzeDuration = EncodingHelper.GetAnalyzeDurationArgument(inputFiles.Length); - } - - probeSize = probeSize + " " + analyzeDuration; - probeSize = probeSize.Trim(); var forceEnableLogging = request.MediaSource.Protocol != MediaProtocol.File; return GetMediaInfoInternal( - GetInputArgument(inputFiles, request.MediaSource.Protocol), + GetInputArgument(inputFile, request.MediaSource), request.MediaSource.Path, request.MediaSource.Protocol, extractChapters, - probeSize, + analyzeDuration, request.MediaType == DlnaProfileType.Audio, request.MediaSource.VideoType, forceEnableLogging, @@ -356,12 +351,20 @@ namespace MediaBrowser.MediaEncoding.Encoder /// <summary> /// Gets the input argument. /// </summary> - /// <param name="inputFiles">The input files.</param> - /// <param name="protocol">The protocol.</param> + /// <param name="inputFile">The input file.</param> + /// <param name="mediaSource">The mediaSource.</param> /// <returns>System.String.</returns> /// <exception cref="ArgumentException">Unrecognized InputType.</exception> - public string GetInputArgument(IReadOnlyList<string> inputFiles, MediaProtocol protocol) - => EncodingUtils.GetInputArgument(inputFiles, protocol); + public string GetInputArgument(string inputFile, MediaSourceInfo mediaSource) + { + var prefix = "file"; + if (mediaSource.VideoType == VideoType.BluRay || mediaSource.VideoType == VideoType.Iso) + { + prefix = "bluray"; + } + + return EncodingUtils.GetInputArgument(prefix, inputFile, mediaSource.Protocol); + } /// <summary> /// Gets the media info internal. @@ -459,31 +462,36 @@ namespace MediaBrowser.MediaEncoding.Encoder public Task<string> ExtractAudioImage(string path, int? imageStreamIndex, CancellationToken cancellationToken) { - return ExtractImage(new[] { path }, null, null, imageStreamIndex, MediaProtocol.File, true, null, null, cancellationToken); + var mediaSource = new MediaSourceInfo + { + Protocol = MediaProtocol.File + }; + + return ExtractImage(path, null, null, imageStreamIndex, mediaSource, true, null, null, cancellationToken); } - public Task<string> ExtractVideoImage(string[] inputFiles, string container, MediaProtocol protocol, MediaStream videoStream, Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken) + public Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream videoStream, Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken) { - return ExtractImage(inputFiles, container, videoStream, null, protocol, false, threedFormat, offset, cancellationToken); + return ExtractImage(inputFile, container, videoStream, null, mediaSource, false, threedFormat, offset, cancellationToken); } - public Task<string> ExtractVideoImage(string[] inputFiles, string container, MediaProtocol protocol, MediaStream imageStream, int? imageStreamIndex, CancellationToken cancellationToken) + public Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, CancellationToken cancellationToken) { - return ExtractImage(inputFiles, container, imageStream, imageStreamIndex, protocol, false, null, null, cancellationToken); + return ExtractImage(inputFile, container, imageStream, imageStreamIndex, mediaSource, false, null, null, cancellationToken); } private async Task<string> ExtractImage( - string[] inputFiles, + string inputFile, string container, MediaStream videoStream, int? imageStreamIndex, - MediaProtocol protocol, + MediaSourceInfo mediaSource, bool isAudio, Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken) { - var inputArgument = GetInputArgument(inputFiles, protocol); + var inputArgument = GetInputArgument(inputFile, mediaSource); if (isAudio) { @@ -495,9 +503,36 @@ namespace MediaBrowser.MediaEncoding.Encoder } else { + // The failure of HDR extraction usually occurs when using custom ffmpeg that does not contain the zscale filter. + try + { + return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, true, cancellationToken).ConfigureAwait(false); + } + catch (ArgumentException) + { + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "I-frame or HDR image extraction failed, will attempt with I-frame extraction disabled. Input: {Arguments}", inputArgument); + } + + try + { + return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, true, cancellationToken).ConfigureAwait(false); + } + catch (ArgumentException) + { + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "HDR image extraction failed, will fallback to SDR image extraction. Input: {Arguments}", inputArgument); + } + try { - return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, cancellationToken).ConfigureAwait(false); + return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, false, cancellationToken).ConfigureAwait(false); } catch (ArgumentException) { @@ -509,10 +544,10 @@ namespace MediaBrowser.MediaEncoding.Encoder } } - return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, cancellationToken).ConfigureAwait(false); + return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, false, cancellationToken).ConfigureAwait(false); } - private async Task<string> ExtractImageInternal(string inputPath, string container, MediaStream videoStream, int? imageStreamIndex, Video3DFormat? threedFormat, TimeSpan? offset, bool useIFrame, CancellationToken cancellationToken) + private async Task<string> ExtractImageInternal(string inputPath, string container, MediaStream videoStream, int? imageStreamIndex, Video3DFormat? threedFormat, TimeSpan? offset, bool useIFrame, bool allowTonemap, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(inputPath)) { @@ -553,15 +588,38 @@ namespace MediaBrowser.MediaEncoding.Encoder var mapArg = imageStreamIndex.HasValue ? (" -map 0:v:" + imageStreamIndex.Value.ToString(CultureInfo.InvariantCulture)) : string.Empty; - var enableThumbnail = !new List<string> { "wtv" }.Contains(container ?? string.Empty, StringComparer.OrdinalIgnoreCase); + var enableHdrExtraction = allowTonemap && string.Equals(videoStream?.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase); + if (enableHdrExtraction) + { + string tonemapFilters = "zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=hable:desat=0:peak=100,zscale=t=bt709:m=bt709,format=yuv420p"; + if (string.IsNullOrEmpty(vf)) + { + vf = "-vf " + tonemapFilters; + } + else + { + vf += "," + tonemapFilters; + } + } + // Use ffmpeg to sample 100 (we can drop this if required using thumbnail=50 for 50 frames) frames and pick the best thumbnail. Have a fall back just in case. - var thumbnail = enableThumbnail ? ",thumbnail=24" : string.Empty; + var enableThumbnail = useIFrame && !string.Equals("wtv", container, StringComparison.OrdinalIgnoreCase); + if (enableThumbnail) + { + if (string.IsNullOrEmpty(vf)) + { + vf = "-vf thumbnail=24"; + } + else + { + vf += ",thumbnail=24"; + } + } - var args = useIFrame ? string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads {5} -v quiet -vframes 1 {2}{4} -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, thumbnail, threads) : - string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads {4} -v quiet -vframes 1 {2} -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, threads); + var args = string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads {4} -v quiet -vframes 1 {2} -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, threads); - var probeSizeArgument = EncodingHelper.GetProbeSizeArgument(1); - var analyzeDurationArgument = EncodingHelper.GetAnalyzeDurationArgument(1); + var probeSizeArgument = string.Empty; + var analyzeDurationArgument = string.Empty; if (!string.IsNullOrWhiteSpace(probeSizeArgument)) { @@ -626,7 +684,7 @@ namespace MediaBrowser.MediaEncoding.Encoder var timeoutMs = _configurationManager.Configuration.ImageExtractionTimeoutMs; if (timeoutMs <= 0) { - timeoutMs = DefaultImageExtractionTimeout; + timeoutMs = enableHdrExtraction ? DefaultHdrImageExtractionTimeout : DefaultSdrImageExtractionTimeout; } ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMilliseconds(timeoutMs)).ConfigureAwait(false); @@ -670,10 +728,10 @@ namespace MediaBrowser.MediaEncoding.Encoder } public async Task ExtractVideoImagesOnInterval( - string[] inputFiles, + string inputFile, string container, MediaStream videoStream, - MediaProtocol protocol, + MediaSourceInfo mediaSource, Video3DFormat? threedFormat, TimeSpan interval, string targetDirectory, @@ -681,7 +739,7 @@ namespace MediaBrowser.MediaEncoding.Encoder int? maxWidth, CancellationToken cancellationToken) { - var inputArgument = GetInputArgument(inputFiles, protocol); + var inputArgument = GetInputArgument(inputFile, mediaSource); var vf = "fps=fps=1/" + interval.TotalSeconds.ToString(_usCulture); @@ -697,8 +755,8 @@ namespace MediaBrowser.MediaEncoding.Encoder var args = string.Format(CultureInfo.InvariantCulture, "-i {0} -threads {3} -v quiet {2} -f image2 \"{1}\"", inputArgument, outputPath, vf, threads); - var probeSizeArgument = EncodingHelper.GetProbeSizeArgument(1); - var analyzeDurationArgument = EncodingHelper.GetAnalyzeDurationArgument(1); + var probeSizeArgument = string.Empty; + var analyzeDurationArgument = string.Empty; if (!string.IsNullOrWhiteSpace(probeSizeArgument)) { diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 3d3d1eb48..bd026bce1 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -694,16 +694,6 @@ namespace MediaBrowser.MediaEncoding.Probing stream.AverageFrameRate = GetFrameRate(streamInfo.AverageFrameRate); stream.RealFrameRate = GetFrameRate(streamInfo.RFrameRate); - // Interlaced video streams in Matroska containers return the field rate instead of the frame rate - // as both the average and real frame rate, so we half the returned frame rates to get the correct values - // - // https://gitlab.com/mbunkus/mkvtoolnix/-/wikis/Wrong-frame-rate-displayed - if (stream.IsInterlaced && formatInfo.FormatName.Contains("matroska", StringComparison.OrdinalIgnoreCase)) - { - stream.AverageFrameRate /= 2; - stream.RealFrameRate /= 2; - } - if (isAudio || string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase) || string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase)) { @@ -788,7 +778,11 @@ namespace MediaBrowser.MediaEncoding.Probing } } - if (bitrate == 0 && formatInfo != null && !string.IsNullOrEmpty(formatInfo.BitRate) && stream.Type == MediaStreamType.Video) + // The bitrate info of FLAC musics and some videos is included in formatInfo. + if (bitrate == 0 + && formatInfo != null + && !string.IsNullOrEmpty(formatInfo.BitRate) + && (stream.Type == MediaStreamType.Video || (isAudio && stream.Type == MediaStreamType.Audio))) { // If the stream info doesn't have a bitrate get the value from the media format info if (int.TryParse(formatInfo.BitRate, NumberStyles.Any, _usCulture, out var value)) diff --git a/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs index a5d641747..db6b47583 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs @@ -51,7 +51,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles { eventsStarted = true; } - else if (!string.IsNullOrEmpty(line) && line.Trim().StartsWith(";", StringComparison.Ordinal)) + else if (!string.IsNullOrEmpty(line) && line.Trim().StartsWith(';')) { // skip comment lines } @@ -151,13 +151,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles try { - var p = new SubtitleTrackEvent(); - - p.StartPositionTicks = GetTimeCodeFromString(start); - p.EndPositionTicks = GetTimeCodeFromString(end); - p.Text = GetFormattedText(text); - - trackEvents.Add(p); + trackEvents.Add( + new SubtitleTrackEvent + { + StartPositionTicks = GetTimeCodeFromString(start), + EndPositionTicks = GetTimeCodeFromString(end), + Text = GetFormattedText(text) + }); } catch { diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index b61b8a0e0..b92c4ee06 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -168,18 +168,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles MediaStream subtitleStream, CancellationToken cancellationToken) { - string[] inputFiles; - - if (mediaSource.VideoType.HasValue - && (mediaSource.VideoType.Value == VideoType.BluRay || mediaSource.VideoType.Value == VideoType.Dvd)) - { - var mediaSourceItem = (Video)_libraryManager.GetItemById(new Guid(mediaSource.Id)); - inputFiles = mediaSourceItem.GetPlayableStreamFileNames(); - } - else - { - inputFiles = new[] { mediaSource.Path }; - } + var inputFile = mediaSource.Path; var protocol = mediaSource.Protocol; if (subtitleStream.IsExternal) @@ -187,7 +176,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles protocol = _mediaSourceManager.GetPathProtocol(subtitleStream.Path); } - var fileInfo = await GetReadableFile(mediaSource.Path, inputFiles, protocol, subtitleStream, cancellationToken).ConfigureAwait(false); + var fileInfo = await GetReadableFile(mediaSource.Path, inputFile, mediaSource, subtitleStream, cancellationToken).ConfigureAwait(false); var stream = await GetSubtitleStream(fileInfo.Path, fileInfo.Protocol, fileInfo.IsExternal, cancellationToken).ConfigureAwait(false); @@ -220,8 +209,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles private async Task<SubtitleInfo> GetReadableFile( string mediaPath, - string[] inputFiles, - MediaProtocol protocol, + string inputFile, + MediaSourceInfo mediaSource, MediaStream subtitleStream, CancellationToken cancellationToken) { @@ -252,9 +241,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles } // Extract - var outputPath = GetSubtitleCachePath(mediaPath, protocol, subtitleStream.Index, "." + outputFormat); + var outputPath = GetSubtitleCachePath(mediaPath, mediaSource, subtitleStream.Index, "." + outputFormat); - await ExtractTextSubtitle(inputFiles, protocol, subtitleStream.Index, outputCodec, outputPath, cancellationToken) + await ExtractTextSubtitle(inputFile, mediaSource, subtitleStream.Index, outputCodec, outputPath, cancellationToken) .ConfigureAwait(false); return new SubtitleInfo(outputPath, MediaProtocol.File, outputFormat, false); @@ -266,14 +255,14 @@ namespace MediaBrowser.MediaEncoding.Subtitles if (GetReader(currentFormat, false) == null) { // Convert - var outputPath = GetSubtitleCachePath(mediaPath, protocol, subtitleStream.Index, ".srt"); + var outputPath = GetSubtitleCachePath(mediaPath, mediaSource, subtitleStream.Index, ".srt"); - await ConvertTextSubtitleToSrt(subtitleStream.Path, subtitleStream.Language, protocol, outputPath, cancellationToken).ConfigureAwait(false); + await ConvertTextSubtitleToSrt(subtitleStream.Path, subtitleStream.Language, mediaSource, outputPath, cancellationToken).ConfigureAwait(false); return new SubtitleInfo(outputPath, MediaProtocol.File, "srt", true); } - return new SubtitleInfo(subtitleStream.Path, protocol, currentFormat, true); + return new SubtitleInfo(subtitleStream.Path, mediaSource.Protocol, currentFormat, true); } private ISubtitleParser GetReader(string format, bool throwIfMissing) @@ -363,11 +352,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles /// </summary> /// <param name="inputPath">The input path.</param> /// <param name="language">The language.</param> - /// <param name="inputProtocol">The input protocol.</param> + /// <param name="mediaSource">The input mediaSource.</param> /// <param name="outputPath">The output path.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> - private async Task ConvertTextSubtitleToSrt(string inputPath, string language, MediaProtocol inputProtocol, string outputPath, CancellationToken cancellationToken) + private async Task ConvertTextSubtitleToSrt(string inputPath, string language, MediaSourceInfo mediaSource, string outputPath, CancellationToken cancellationToken) { var semaphore = GetLock(outputPath); @@ -377,7 +366,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles { if (!File.Exists(outputPath)) { - await ConvertTextSubtitleToSrtInternal(inputPath, language, inputProtocol, outputPath, cancellationToken).ConfigureAwait(false); + await ConvertTextSubtitleToSrtInternal(inputPath, language, mediaSource, outputPath, cancellationToken).ConfigureAwait(false); } } finally @@ -391,14 +380,14 @@ namespace MediaBrowser.MediaEncoding.Subtitles /// </summary> /// <param name="inputPath">The input path.</param> /// <param name="language">The language.</param> - /// <param name="inputProtocol">The input protocol.</param> + /// <param name="mediaSource">The input mediaSource.</param> /// <param name="outputPath">The output path.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> /// <exception cref="ArgumentNullException"> /// The <c>inputPath</c> or <c>outputPath</c> is <c>null</c>. /// </exception> - private async Task ConvertTextSubtitleToSrtInternal(string inputPath, string language, MediaProtocol inputProtocol, string outputPath, CancellationToken cancellationToken) + private async Task ConvertTextSubtitleToSrtInternal(string inputPath, string language, MediaSourceInfo mediaSource, string outputPath, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(inputPath)) { @@ -412,7 +401,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); - var encodingParam = await GetSubtitleFileCharacterSet(inputPath, language, inputProtocol, cancellationToken).ConfigureAwait(false); + var encodingParam = await GetSubtitleFileCharacterSet(inputPath, language, mediaSource.Protocol, cancellationToken).ConfigureAwait(false); // FFmpeg automatically convert character encoding when it is UTF-16 // If we specify character encoding, it rejects with "do not specify a character encoding" and "Unable to recode subtitle event" @@ -515,8 +504,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles /// <summary> /// Extracts the text subtitle. /// </summary> - /// <param name="inputFiles">The input files.</param> - /// <param name="protocol">The protocol.</param> + /// <param name="inputFile">The input file.</param> + /// <param name="mediaSource">The mediaSource.</param> /// <param name="subtitleStreamIndex">Index of the subtitle stream.</param> /// <param name="outputCodec">The output codec.</param> /// <param name="outputPath">The output path.</param> @@ -524,8 +513,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles /// <returns>Task.</returns> /// <exception cref="ArgumentException">Must use inputPath list overload.</exception> private async Task ExtractTextSubtitle( - string[] inputFiles, - MediaProtocol protocol, + string inputFile, + MediaSourceInfo mediaSource, int subtitleStreamIndex, string outputCodec, string outputPath, @@ -540,7 +529,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles if (!File.Exists(outputPath)) { await ExtractTextSubtitleInternal( - _mediaEncoder.GetInputArgument(inputFiles, protocol), + _mediaEncoder.GetInputArgument(inputFile, mediaSource), subtitleStreamIndex, outputCodec, outputPath, @@ -706,9 +695,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles } } - private string GetSubtitleCachePath(string mediaPath, MediaProtocol protocol, int subtitleStreamIndex, string outputSubtitleExtension) + private string GetSubtitleCachePath(string mediaPath, MediaSourceInfo mediaSource, int subtitleStreamIndex, string outputSubtitleExtension) { - if (protocol == MediaProtocol.File) + if (mediaSource.Protocol == MediaProtocol.File) { var ticksParam = string.Empty; diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 9381145e1..0dbd51bdc 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. /// </summary> public bool QuickConnectAvailable { get; set; } = false; - + /// <summary> /// Gets or sets a value indicating whether access outside of the LAN is permitted. /// </summary> diff --git a/MediaBrowser.Model/Dlna/ContainerProfile.cs b/MediaBrowser.Model/Dlna/ContainerProfile.cs index 09afa64bb..56c89d854 100644 --- a/MediaBrowser.Model/Dlna/ContainerProfile.cs +++ b/MediaBrowser.Model/Dlna/ContainerProfile.cs @@ -47,7 +47,7 @@ namespace MediaBrowser.Model.Dlna public static bool ContainsContainer(string profileContainers, string inputContainer) { var isNegativeList = false; - if (profileContainers != null && profileContainers.StartsWith("-", StringComparison.Ordinal)) + if (profileContainers != null && profileContainers.StartsWith('-')) { isNegativeList = true; profileContainers = profileContainers.Substring(1); diff --git a/MediaBrowser.Model/Querying/NextUpQuery.cs b/MediaBrowser.Model/Querying/NextUpQuery.cs index ee13ffc16..4ad336d33 100644 --- a/MediaBrowser.Model/Querying/NextUpQuery.cs +++ b/MediaBrowser.Model/Querying/NextUpQuery.cs @@ -18,7 +18,7 @@ namespace MediaBrowser.Model.Querying /// Gets or sets the parent identifier. /// </summary> /// <value>The parent identifier.</value> - public string ParentId { get; set; } + public Guid? ParentId { get; set; } /// <summary> /// Gets or sets the series id. diff --git a/MediaBrowser.Model/Search/SearchQuery.cs b/MediaBrowser.Model/Search/SearchQuery.cs index 01ad319a4..ce60062cd 100644 --- a/MediaBrowser.Model/Search/SearchQuery.cs +++ b/MediaBrowser.Model/Search/SearchQuery.cs @@ -47,7 +47,7 @@ namespace MediaBrowser.Model.Search public string[] ExcludeItemTypes { get; set; } - public string ParentId { get; set; } + public Guid? ParentId { get; set; } public bool? IsMovie { get; set; } diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index dca8acb7d..6dbce3067 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -229,16 +229,16 @@ namespace MediaBrowser.Providers.Manager await result.Item.UpdateToRepositoryAsync(reason, cancellationToken).ConfigureAwait(false); } - private async Task SavePeopleMetadataAsync(List<PersonInfo> people, LibraryOptions libraryOptions, CancellationToken cancellationToken) + private Task SavePeopleMetadataAsync(List<PersonInfo> people, LibraryOptions libraryOptions, CancellationToken cancellationToken) { + var personsToSave = new List<BaseItem>(); + foreach (var person in people) { cancellationToken.ThrowIfCancellationRequested(); if (person.ProviderIds.Count > 0 || !string.IsNullOrWhiteSpace(person.ImageUrl)) { - var updateType = ItemUpdateType.MetadataDownload; - var saveEntity = false; var personEntity = LibraryManager.GetPerson(person.Name); foreach (var id in person.ProviderIds) @@ -261,15 +261,18 @@ namespace MediaBrowser.Providers.Manager 0); saveEntity = true; - updateType |= ItemUpdateType.ImageUpdate; } if (saveEntity) { - await personEntity.UpdateToRepositoryAsync(updateType, cancellationToken).ConfigureAwait(false); + personsToSave.Add(personEntity); } } } + + LibraryManager.RunMetadataSavers(personsToSave, ItemUpdateType.MetadataDownload); + LibraryManager.CreateItems(personsToSave, null, CancellationToken.None); + return Task.CompletedTask; } protected virtual Task AfterMetadataRefresh(TItemType item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken) diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs index c61187fdf..4fff57273 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs @@ -150,11 +150,6 @@ namespace MediaBrowser.Providers.MediaInfo public Task<ItemUpdateType> FetchVideoInfo<T>(T item, MetadataRefreshOptions options, CancellationToken cancellationToken) where T : Video { - if (item.VideoType == VideoType.Iso) - { - return _cachedTask; - } - if (item.IsPlaceHolder) { return _cachedTask; @@ -208,7 +203,7 @@ namespace MediaBrowser.Providers.MediaInfo { item.ShortcutPath = File.ReadAllLines(item.Path) .Select(NormalizeStrmLine) - .FirstOrDefault(i => !string.IsNullOrWhiteSpace(i) && !i.StartsWith("#", StringComparison.OrdinalIgnoreCase)); + .FirstOrDefault(i => !string.IsNullOrWhiteSpace(i) && !i.StartsWith('#')); } public Task<ItemUpdateType> FetchAudioInfo<T>(T item, MetadataRefreshOptions options, CancellationToken cancellationToken) diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index 776dee780..6d39c091e 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -116,7 +116,7 @@ namespace MediaBrowser.Providers.MediaInfo streamFileNames = Array.Empty<string>(); } - mediaInfoResult = await GetMediaInfo(item, streamFileNames, cancellationToken).ConfigureAwait(false); + mediaInfoResult = await GetMediaInfo(item, cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); } @@ -128,7 +128,6 @@ namespace MediaBrowser.Providers.MediaInfo private Task<Model.MediaInfo.MediaInfo> GetMediaInfo( Video item, - string[] streamFileNames, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -145,7 +144,6 @@ namespace MediaBrowser.Providers.MediaInfo return _mediaEncoder.GetMediaInfo( new MediaInfoRequest { - PlayableStreamFileNames = streamFileNames, ExtractChapters = true, MediaType = DlnaProfileType.Video, MediaSource = new MediaSourceInfo diff --git a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs index fc38d3832..c36c3af6a 100644 --- a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs @@ -9,6 +9,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Drawing; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; @@ -50,7 +51,7 @@ namespace MediaBrowser.Providers.MediaInfo } // No support for this - if (video.VideoType == VideoType.Iso || video.VideoType == VideoType.Dvd || video.VideoType == VideoType.BluRay) + if (video.VideoType == VideoType.Dvd) { return Task.FromResult(new DynamicImageResponse { HasImage = false }); } @@ -69,11 +70,7 @@ namespace MediaBrowser.Providers.MediaInfo { var protocol = item.PathProtocol ?? MediaProtocol.File; - var inputPath = MediaEncoderHelpers.GetInputArgument( - _fileSystem, - item.Path, - null, - item.GetPlayableStreamFileNames()); + var inputPath = item.Path; var mediaStreams = item.GetMediaStreams(); @@ -107,7 +104,14 @@ namespace MediaBrowser.Providers.MediaInfo } } - extractedImagePath = await _mediaEncoder.ExtractVideoImage(inputPath, item.Container, protocol, imageStream, videoIndex, cancellationToken).ConfigureAwait(false); + MediaSourceInfo mediaSource = new MediaSourceInfo + { + VideoType = item.VideoType, + IsoType = item.IsoType, + Protocol = item.PathProtocol.Value, + }; + + extractedImagePath = await _mediaEncoder.ExtractVideoImage(inputPath, item.Container, mediaSource, imageStream, videoIndex, cancellationToken).ConfigureAwait(false); } else { @@ -119,8 +123,14 @@ namespace MediaBrowser.Providers.MediaInfo : TimeSpan.FromSeconds(10); var videoStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video); + var mediaSource = new MediaSourceInfo + { + VideoType = item.VideoType, + IsoType = item.IsoType, + Protocol = item.PathProtocol.Value, + }; - extractedImagePath = await _mediaEncoder.ExtractVideoImage(inputPath, item.Container, protocol, videoStream, item.Video3DFormat, imageOffset, cancellationToken).ConfigureAwait(false); + extractedImagePath = await _mediaEncoder.ExtractVideoImage(inputPath, item.Container, mediaSource, videoStream, item.Video3DFormat, imageOffset, cancellationToken).ConfigureAwait(false); } return new DynamicImageResponse diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs index e3a1decb8..293087da7 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs @@ -103,6 +103,6 @@ namespace MediaBrowser.Providers.Plugins.AudioDb /// <inheritdoc /> public bool Supports(BaseItem item) - => Plugin.Instance.Configuration.Enable && item is MusicAlbum; + => item is MusicAlbum; } } diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs index 6536b303f..97bba10ba 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs @@ -56,13 +56,6 @@ namespace MediaBrowser.Providers.Plugins.AudioDb public async Task<MetadataResult<MusicAlbum>> GetMetadata(AlbumInfo info, CancellationToken cancellationToken) { var result = new MetadataResult<MusicAlbum>(); - - // TODO maybe remove when artist metadata can be disabled - if (!Plugin.Instance.Configuration.Enable) - { - return result; - } - var id = info.GetReleaseGroupId(); if (!string.IsNullOrWhiteSpace(id)) diff --git a/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs index 54851c4d0..d250acfa8 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs @@ -144,6 +144,6 @@ namespace MediaBrowser.Providers.Plugins.AudioDb /// <inheritdoc /> public bool Supports(BaseItem item) - => Plugin.Instance.Configuration.Enable && item is MusicArtist; + => item is MusicArtist; } } diff --git a/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs index 85c92fa7b..a2a03e1f9 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs @@ -57,13 +57,6 @@ namespace MediaBrowser.Providers.Plugins.AudioDb public async Task<MetadataResult<MusicArtist>> GetMetadata(ArtistInfo info, CancellationToken cancellationToken) { var result = new MetadataResult<MusicArtist>(); - - // TODO maybe remove when artist metadata can be disabled - if (!Plugin.Instance.Configuration.Enable) - { - return result; - } - var id = info.GetMusicBrainzArtistId(); if (!string.IsNullOrWhiteSpace(id)) diff --git a/MediaBrowser.Providers/Plugins/AudioDb/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/AudioDb/Configuration/PluginConfiguration.cs index 9657a290f..664474dcd 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/Configuration/PluginConfiguration.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/Configuration/PluginConfiguration.cs @@ -6,8 +6,6 @@ namespace MediaBrowser.Providers.Plugins.AudioDb { public class PluginConfiguration : BasePluginConfiguration { - public bool Enable { get; set; } - public bool ReplaceAlbumName { get; set; } } } diff --git a/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html b/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html index 82f26a8f2..eab252005 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html +++ b/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html @@ -9,10 +9,6 @@ <div class="content-primary"> <form class="configForm"> <label class="checkboxContainer"> - <input is="emby-checkbox" type="checkbox" id="enable" /> - <span>Enable this provider for metadata searches on artists and albums.</span> - </label> - <label class="checkboxContainer"> <input is="emby-checkbox" type="checkbox" id="replaceAlbumName" /> <span>When an album is found during a metadata search, replace the name with the value on the server.</span> </label> @@ -32,9 +28,8 @@ .addEventListener('pageshow', function () { Dashboard.showLoadingMsg(); ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) { - document.querySelector('#enable').checked = config.Enable; document.querySelector('#replaceAlbumName').checked = config.ReplaceAlbumName; - + Dashboard.hideLoadingMsg(); }); }); @@ -42,14 +37,13 @@ document.querySelector('.configForm') .addEventListener('submit', function (e) { Dashboard.showLoadingMsg(); - + ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) { - config.Enable = document.querySelector('#enable').checked; config.ReplaceAlbumName = document.querySelector('#replaceAlbumName').checked; - + ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult); }); - + e.preventDefault(); return false; }); diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs index dc755b600..ce9392402 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs @@ -25,12 +25,6 @@ namespace MediaBrowser.Providers.Music /// <inheritdoc /> public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(ArtistInfo searchInfo, CancellationToken cancellationToken) { - // TODO maybe remove when artist metadata can be disabled - if (!Plugin.Instance.Configuration.Enable) - { - return Enumerable.Empty<RemoteSearchResult>(); - } - var musicBrainzId = searchInfo.GetMusicBrainzArtistId(); if (!string.IsNullOrWhiteSpace(musicBrainzId)) @@ -236,12 +230,6 @@ namespace MediaBrowser.Providers.Music Item = new MusicArtist() }; - // TODO maybe remove when artist metadata can be disabled - if (!Plugin.Instance.Configuration.Enable) - { - return result; - } - var musicBrainzId = id.GetMusicBrainzArtistId(); if (string.IsNullOrWhiteSpace(musicBrainzId)) diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs index 980da9a01..0cec9e359 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs @@ -43,8 +43,6 @@ namespace MediaBrowser.Providers.Plugins.MusicBrainz } } - public bool Enable { get; set; } - public bool ReplaceArtistName { get; set; } } } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html index 1945e6cb4..6f1296bb7 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html @@ -17,10 +17,6 @@ <div class="fieldDescription">Span of time between requests in milliseconds. The official server is limited to one request every two seconds.</div> </div> <label class="checkboxContainer"> - <input is="emby-checkbox" type="checkbox" id="enable" /> - <span>Enable this provider for metadata searches on artists and albums.</span> - </label> - <label class="checkboxContainer"> <input is="emby-checkbox" type="checkbox" id="replaceArtistName" /> <span>When an artist is found during a metadata search, replace the artist name with the value on the server.</span> </label> @@ -46,7 +42,7 @@ bubbles: true, cancelable: false })); - + var rateLimit = document.querySelector('#rateLimit'); rateLimit.value = config.RateLimit; rateLimit.dispatchEvent(new Event('change', { @@ -54,26 +50,24 @@ cancelable: false })); - document.querySelector('#enable').checked = config.Enable; document.querySelector('#replaceArtistName').checked = config.ReplaceArtistName; Dashboard.hideLoadingMsg(); }); }); - + document.querySelector('.musicBrainzConfigForm') .addEventListener('submit', function (e) { Dashboard.showLoadingMsg(); - + ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) { config.Server = document.querySelector('#server').value; config.RateLimit = document.querySelector('#rateLimit').value; - config.Enable = document.querySelector('#enable').checked; config.ReplaceArtistName = document.querySelector('#replaceArtistName').checked; - + ApiClient.updatePluginConfiguration(MusicBrainzPluginConfig.uniquePluginId, config).then(Dashboard.processPluginConfigurationUpdateResult); }); - + e.preventDefault(); return false; }); diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs index 93178d64a..ef7933b1a 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs @@ -78,12 +78,6 @@ namespace MediaBrowser.Providers.Music /// <inheritdoc /> public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(AlbumInfo searchInfo, CancellationToken cancellationToken) { - // TODO maybe remove when artist metadata can be disabled - if (!Plugin.Instance.Configuration.Enable) - { - return Enumerable.Empty<RemoteSearchResult>(); - } - var releaseId = searchInfo.GetReleaseId(); var releaseGroupId = searchInfo.GetReleaseGroupId(); @@ -194,12 +188,6 @@ namespace MediaBrowser.Providers.Music Item = new MusicAlbum() }; - // TODO maybe remove when artist metadata can be disabled - if (!Plugin.Instance.Configuration.Enable) - { - return result; - } - // If we have a release group Id but not a release Id... if (string.IsNullOrWhiteSpace(releaseId) && !string.IsNullOrWhiteSpace(releaseGroupId)) { @@ -768,16 +756,7 @@ namespace MediaBrowser.Providers.Music _stopWatchMusicBrainz.Restart(); using var request = new HttpRequestMessage(HttpMethod.Get, requestUrl); - - // MusicBrainz request a contact email address is supplied, as comment, in user agent field: - // https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting#User-Agent . - request.Headers.UserAgent.ParseAdd(string.Format( - CultureInfo.InvariantCulture, - "{0} ( {1} )", - _appHost.ApplicationUserAgent, - _appHost.ApplicationUserAgentAddress)); - - response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(request).ConfigureAwait(false); + response = await _httpClientFactory.CreateClient(NamedClient.MusicBrainz).SendAsync(request).ConfigureAwait(false); // We retry a finite number of times, and only whilst MB is indicating 503 (throttling). } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs index 3984e4953..bcf9459ef 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs @@ -129,6 +129,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies .GetMovieAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken) .ConfigureAwait(false); + if (movieResult == null) + { + return new MetadataResult<Movie>(); + } + var movie = new Movie { Name = movieResult.Title ?? movieResult.OriginalTitle, @@ -266,7 +271,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies } } - if (movieResult.Videos?.Results != null) { var trailers = new List<MediaUrl>(); diff --git a/MediaBrowser.sln b/MediaBrowser.sln index d460c0ab0..5a807372d 100644 --- a/MediaBrowser.sln +++ b/MediaBrowser.sln @@ -1,4 +1,4 @@ -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 @@ -68,6 +68,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server.Implementat EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Networking", "Jellyfin.Networking\Jellyfin.Networking.csproj", "{0A3FCC4D-C714-4072-B90F-E374A15F9FF9}" 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}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -182,6 +186,14 @@ Global {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 + {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Release|Any CPU.Build.0 = Release|Any CPU + {B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -193,6 +205,8 @@ Global {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} + {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + {B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE} diff --git a/RSSDP/RSSDP.csproj b/RSSDP/RSSDP.csproj index d0962e82c..c64ee9389 100644 --- a/RSSDP/RSSDP.csproj +++ b/RSSDP/RSSDP.csproj @@ -6,6 +6,7 @@ </PropertyGroup> <ItemGroup> + <ProjectReference Include="..\Jellyfin.Networking\Jellyfin.Networking.csproj" /> <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" /> </ItemGroup> diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs index a4be32e7d..8f1f0fa61 100644 --- a/RSSDP/SsdpCommunicationsServer.cs +++ b/RSSDP/SsdpCommunicationsServer.cs @@ -352,7 +352,7 @@ namespace Rssdp.Infrastructure if (_enableMultiSocketBinding) { - foreach (var address in _networkManager.GetLocalIpAddresses()) + foreach (var address in _networkManager.GetInternalBindAddresses()) { if (address.AddressFamily == AddressFamily.InterNetworkV6) { @@ -362,7 +362,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 90925b9e0..c9e795d56 100644 --- a/RSSDP/SsdpDevicePublisher.cs +++ b/RSSDP/SsdpDevicePublisher.cs @@ -300,17 +300,15 @@ namespace Rssdp.Infrastructure foreach (var device in deviceList) { - if (!_sendOnlyMatchedHost || - _networkManager.IsInSameSubnet(device.ToRootDevice().Address, remoteEndPoint.Address, device.ToRootDevice().SubnetMask)) + var root = device.ToRootDevice(); + var source = new IPNetAddress(root.Address, root.PrefixLength); + var destination = new IPNetAddress(remoteEndPoint.Address, root.PrefixLength); + if (!_sendOnlyMatchedHost || source.NetworkAddress.Equals(destination.NetworkAddress)) { SendDeviceSearchResponses(device, remoteEndPoint, receivedOnlocalIpAddress, cancellationToken); } } } - else - { - // WriteTrace(String.Format("Sending 0 search responses.")); - } }); } diff --git a/RSSDP/SsdpRootDevice.cs b/RSSDP/SsdpRootDevice.cs index 4084b31ca..5ecb1f86f 100644 --- a/RSSDP/SsdpRootDevice.cs +++ b/RSSDP/SsdpRootDevice.cs @@ -45,9 +45,9 @@ namespace Rssdp public IPAddress Address { get; set; } /// <summary> - /// Gets or sets the SubnetMask used to check if the received message from same interface with this device/tree. Required. + /// Gets or sets the prefix length used to check if the received message from same interface with this device/tree. Required. /// </summary> - public IPAddress SubnetMask { get; set; } + public byte PrefixLength { get; set; } /// <summary> /// The base URL to use for all relative url's provided in other properties (and those of child devices). Optional. diff --git a/benches/Jellyfin.Common.Benches/HexDecodeBenches.cs b/benches/Jellyfin.Common.Benches/HexDecodeBenches.cs deleted file mode 100644 index d9a107b69..000000000 --- a/benches/Jellyfin.Common.Benches/HexDecodeBenches.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Globalization; -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Running; -using MediaBrowser.Common; - -namespace Jellyfin.Common.Benches -{ - [MemoryDiagnoser] - public class HexDecodeBenches - { - private string _data; - - [Params(0, 10, 100, 1000, 10000, 1000000)] - public int N { get; set; } - - [GlobalSetup] - public void GlobalSetup() - { - var bytes = new byte[N]; - new Random(42).NextBytes(bytes); - _data = Hex.Encode(bytes); - } - - [Benchmark] - public byte[] Decode() => Hex.Decode(_data); - - [Benchmark] - public byte[] DecodeSubString() => DecodeSubString(_data); - - private static byte[] DecodeSubString(string str) - { - byte[] bytes = new byte[str.Length / 2]; - for (int i = 0; i < str.Length; i += 2) - { - bytes[i / 2] = byte.Parse( - str.Substring(i, 2), - NumberStyles.HexNumber, - CultureInfo.InvariantCulture); - } - - return bytes; - } - } -} diff --git a/benches/Jellyfin.Common.Benches/HexEncodeBenches.cs b/benches/Jellyfin.Common.Benches/HexEncodeBenches.cs deleted file mode 100644 index 7abf93c51..000000000 --- a/benches/Jellyfin.Common.Benches/HexEncodeBenches.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Running; -using MediaBrowser.Common; - -namespace Jellyfin.Common.Benches -{ - [MemoryDiagnoser] - public class HexEncodeBenches - { - private byte[] _data; - - [Params(0, 10, 100, 1000, 10000, 1000000)] - public int N { get; set; } - - [GlobalSetup] - public void GlobalSetup() - { - _data = new byte[N]; - new Random(42).NextBytes(_data); - } - - [Benchmark] - public string HexEncode() => Hex.Encode(_data); - - [Benchmark] - public string BitConverterToString() => BitConverter.ToString(_data); - - [Benchmark] - public string BitConverterToStringWithReplace() => BitConverter.ToString(_data).Replace("-", ""); - } -} diff --git a/benches/Jellyfin.Common.Benches/Jellyfin.Common.Benches.csproj b/benches/Jellyfin.Common.Benches/Jellyfin.Common.Benches.csproj deleted file mode 100644 index c564e86e9..000000000 --- a/benches/Jellyfin.Common.Benches/Jellyfin.Common.Benches.csproj +++ /dev/null @@ -1,16 +0,0 @@ -<Project Sdk="Microsoft.NET.Sdk"> - - <PropertyGroup> - <OutputType>Exe</OutputType> - <TargetFramework>net5.0</TargetFramework> - </PropertyGroup> - - <ItemGroup> - <PackageReference Include="BenchmarkDotNet" Version="0.12.0" /> - </ItemGroup> - - <ItemGroup> - <ProjectReference Include="../../MediaBrowser.Common/MediaBrowser.Common.csproj" /> - </ItemGroup> - -</Project> diff --git a/benches/Jellyfin.Common.Benches/Program.cs b/benches/Jellyfin.Common.Benches/Program.cs deleted file mode 100644 index b218b0dc1..000000000 --- a/benches/Jellyfin.Common.Benches/Program.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using BenchmarkDotNet.Running; - -namespace Jellyfin.Common.Benches -{ - public static class Program - { - public static void Main(string[] args) - { - _ = BenchmarkRunner.Run<HexEncodeBenches>(); - _ = BenchmarkRunner.Run<HexDecodeBenches>(); - } - } -} diff --git a/debian/bin/restart.sh b/debian/bin/restart.sh index 9b64b6d72..34fce0670 100755 --- a/debian/bin/restart.sh +++ b/debian/bin/restart.sh @@ -24,13 +24,13 @@ cmd="$( get_service_command )" echo "Detected service control platform '$cmd'; using it to restart Jellyfin..." case $cmd in 'systemctl') - echo "sleep 2; /usr/bin/sudo $( which systemctl ) restart jellyfin" | at now + echo "sleep 0.5; /usr/bin/sudo $( which systemctl ) start jellyfin" | at now ;; 'service') - echo "sleep 2; /usr/bin/sudo $( which service ) jellyfin restart" | at now + echo "sleep 0.5; /usr/bin/sudo $( which service ) jellyfin start" | at now ;; 'sysv') - echo "sleep 2; /usr/bin/sudo /etc/init.d/jellyfin restart" | at now + echo "sleep 0.5; /usr/bin/sudo /etc/init.d/jellyfin start" | at now ;; esac exit 0 diff --git a/fedora/jellyfin.spec b/fedora/jellyfin.spec index 13305488e..197126ee5 100644 --- a/fedora/jellyfin.spec +++ b/fedora/jellyfin.spec @@ -40,7 +40,7 @@ Jellyfin is a free software media system that puts you in control of managing an Summary: The Free Software Media System Server backend Requires(pre): shadow-utils Requires: ffmpeg -Requires: libcurl, fontconfig, freetype, openssl, glibc libicu +Requires: libcurl, fontconfig, freetype, openssl, glibc, libicu, at %description server The Jellyfin media server backend. diff --git a/fedora/restart.sh b/fedora/restart.sh index 9e53efecd..34fce0670 100755 --- a/fedora/restart.sh +++ b/fedora/restart.sh @@ -24,13 +24,13 @@ cmd="$( get_service_command )" echo "Detected service control platform '$cmd'; using it to restart Jellyfin..." case $cmd in 'systemctl') - echo "sleep 2; /usr/bin/sudo $( which systemctl ) restart jellyfin" | at now + echo "sleep 0.5; /usr/bin/sudo $( which systemctl ) start jellyfin" | at now ;; 'service') - echo "sleep 2; /usr/bin/sudo $( which service ) jellyfin restart" | at now + echo "sleep 0.5; /usr/bin/sudo $( which service ) jellyfin start" | at now ;; 'sysv') - echo "sleep 2; /usr/bin/sudo /etc/init.d/jellyfin restart" | at now + echo "sleep 0.5; /usr/bin/sudo /etc/init.d/jellyfin start" | at now ;; esac exit 0 diff --git a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs index 90c491666..ee20cc573 100644 --- a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs @@ -69,7 +69,7 @@ namespace Jellyfin.Api.Tests.Auth } [Fact] - public async Task HandleAuthenticateAsyncShouldFailOnAuthenticationException() + public async Task HandleAuthenticateAsyncShouldProvideNoResultOnAuthenticationException() { var errorMessage = _fixture.Create<string>(); @@ -81,7 +81,7 @@ namespace Jellyfin.Api.Tests.Auth var authenticateResult = await _sut.AuthenticateAsync(); Assert.False(authenticateResult.Succeeded); - Assert.Equal(errorMessage, authenticateResult.Failure?.Message); + Assert.True(authenticateResult.None); } [Fact] diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj index 14eed30e0..7c552ec06 100644 --- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj +++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj @@ -22,7 +22,7 @@ <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="coverlet.collector" Version="1.3.0" /> - <PackageReference Include="Moq" Version="4.15.1" /> + <PackageReference Include="Moq" Version="4.15.2" /> </ItemGroup> <!-- Code Analyzers --> diff --git a/tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs b/tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs index bd3d35687..54f8eb225 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; @@ -80,7 +78,6 @@ namespace Jellyfin.Api.Tests loggerFactory, commandLineOpts, new ManagedFileSystem(loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths), - new NetworkManager(loggerFactory.CreateLogger<NetworkManager>()), serviceCollection); _disposableComponents.Add(appHost); appHost.Init(); diff --git a/tests/Jellyfin.Common.Tests/HexTests.cs b/tests/Jellyfin.Common.Tests/HexTests.cs deleted file mode 100644 index 5b578d38c..000000000 --- a/tests/Jellyfin.Common.Tests/HexTests.cs +++ /dev/null @@ -1,19 +0,0 @@ -using MediaBrowser.Common; -using Xunit; - -namespace Jellyfin.Common.Tests -{ - public class HexTests - { - [Theory] - [InlineData("")] - [InlineData("00")] - [InlineData("01")] - [InlineData("000102030405060708090a0b0c0d0e0f")] - [InlineData("0123456789abcdef")] - public void RoundTripTest(string data) - { - Assert.Equal(data, Hex.Encode(Hex.Decode(data))); - } - } -} diff --git a/tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs index d9e66d677..3c94db491 100644 --- a/tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs +++ b/tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs @@ -5,28 +5,48 @@ using Xunit; namespace Jellyfin.Common.Tests.Extensions { - public static class JsonGuidConverterTests + public class JsonGuidConverterTests { + private readonly JsonSerializerOptions _options; + + public JsonGuidConverterTests() + { + _options = new JsonSerializerOptions(); + _options.Converters.Add(new JsonGuidConverter()); + } + [Fact] - public static void Deserialize_Valid_Success() + public void Deserialize_Valid_Success() { - var options = new JsonSerializerOptions(); - options.Converters.Add(new JsonGuidConverter()); - Guid value = JsonSerializer.Deserialize<Guid>(@"""a852a27afe324084ae66db579ee3ee18""", options); + Guid value = JsonSerializer.Deserialize<Guid>(@"""a852a27afe324084ae66db579ee3ee18""", _options); Assert.Equal(new Guid("a852a27afe324084ae66db579ee3ee18"), value); + } - value = JsonSerializer.Deserialize<Guid>(@"""e9b2dcaa-529c-426e-9433-5e9981f27f2e""", options); + [Fact] + public void Deserialize_ValidDashed_Success() + { + Guid value = JsonSerializer.Deserialize<Guid>(@"""e9b2dcaa-529c-426e-9433-5e9981f27f2e""", _options); Assert.Equal(new Guid("e9b2dcaa-529c-426e-9433-5e9981f27f2e"), value); } [Fact] - public static void Roundtrip_Valid_Success() + public void Roundtrip_Valid_Success() { - var options = new JsonSerializerOptions(); - options.Converters.Add(new JsonGuidConverter()); Guid guid = new Guid("a852a27afe324084ae66db579ee3ee18"); - string value = JsonSerializer.Serialize(guid, options); - Assert.Equal(guid, JsonSerializer.Deserialize<Guid>(value, options)); + string value = JsonSerializer.Serialize(guid, _options); + Assert.Equal(guid, JsonSerializer.Deserialize<Guid>(value, _options)); + } + + [Fact] + public void Deserialize_Null_EmptyGuid() + { + Assert.Equal(Guid.Empty, JsonSerializer.Deserialize<Guid>("null", _options)); + } + + [Fact] + public void Serialize_EmptyGuid_Null() + { + Assert.Equal("null", JsonSerializer.Serialize(Guid.Empty, _options)); } } } diff --git a/tests/Jellyfin.Common.Tests/PasswordHashTests.cs b/tests/Jellyfin.Common.Tests/PasswordHashTests.cs index 46926f4f8..c4422bd10 100644 --- a/tests/Jellyfin.Common.Tests/PasswordHashTests.cs +++ b/tests/Jellyfin.Common.Tests/PasswordHashTests.cs @@ -1,3 +1,4 @@ +using System; using MediaBrowser.Common; using MediaBrowser.Common.Cryptography; using Xunit; @@ -16,8 +17,8 @@ namespace Jellyfin.Common.Tests { var pass = PasswordHash.Parse(passwordHash); Assert.Equal(id, pass.Id); - Assert.Equal(salt, Hex.Encode(pass.Salt, false)); - Assert.Equal(hash, Hex.Encode(pass.Hash, false)); + Assert.Equal(salt, Convert.ToHexString(pass.Salt)); + Assert.Equal(hash, Convert.ToHexString(pass.Hash)); } [Theory] diff --git a/tests/Jellyfin.Dlna.Tests/GetUuidTests.cs b/tests/Jellyfin.Dlna.Tests/GetUuidTests.cs new file mode 100644 index 000000000..7655e3f7c --- /dev/null +++ b/tests/Jellyfin.Dlna.Tests/GetUuidTests.cs @@ -0,0 +1,17 @@ +using Emby.Dlna.PlayTo; +using Xunit; + +namespace Jellyfin.Dlna.Tests +{ + public static class GetUuidTests + { + [Theory] + [InlineData("uuid:fc4ec57e-b051-11db-88f8-0060085db3f6::urn:schemas-upnp-org:device:WANDevice:1", "fc4ec57e-b051-11db-88f8-0060085db3f6")] + [InlineData("uuid:IGD{8c80f73f-4ba0-45fa-835d-042505d052be}000000000000", "8c80f73f-4ba0-45fa-835d-042505d052be")] + [InlineData("uuid:IGD{8c80f73f-4ba0-45fa-835d-042505d052be}000000000000::urn:schemas-upnp-org:device:InternetGatewayDevice:1", "8c80f73f-4ba0-45fa-835d-042505d052be")] + [InlineData("uuid:00000000-0000-0000-0000-000000000000::upnp:rootdevice", "00000000-0000-0000-0000-000000000000")] + [InlineData("uuid:fc4ec57e-b051-11db-88f8-0060085db3f6", "fc4ec57e-b051-11db-88f8-0060085db3f6")] + public static void GetUuid_Valid_Success(string usn, string uuid) + => Assert.Equal(uuid, PlayToManager.GetUuid(usn)); + } +} diff --git a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj new file mode 100644 index 000000000..f91db6744 --- /dev/null +++ b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj @@ -0,0 +1,33 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net5.0</TargetFramework> + <IsPackable>false</IsPackable> + <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + <Nullable>enable</Nullable> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" /> + <PackageReference Include="xunit" Version="2.4.1" /> + <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> + <PackageReference Include="coverlet.collector" Version="1.3.0" /> + </ItemGroup> + + <!-- Code Analyzers --> + <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> + <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> + <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="../../Emby.Dlna/Emby.Dlna.csproj" /> + </ItemGroup> + + <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> + <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet> + </PropertyGroup> + +</Project> diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj b/tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj new file mode 100644 index 000000000..48b0b4c7d --- /dev/null +++ b/tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj @@ -0,0 +1,39 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> + <PropertyGroup> + <ProjectGuid>{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}</ProjectGuid> + </PropertyGroup> + + <PropertyGroup> + <TargetFramework>net5.0</TargetFramework> + <IsPackable>false</IsPackable> + <Nullable>enable</Nullable> + <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" /> + <PackageReference Include="xunit" Version="2.4.1" /> + <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" /> + <PackageReference Include="coverlet.collector" Version="1.3.0" /> + <PackageReference Include="Moq" Version="4.15.2" /> + </ItemGroup> + + <!-- Code Analyzers--> + <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> + <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> + <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> + </ItemGroup> + <ItemGroup> + <ProjectReference Include="..\..\..\Emby.Server.Implementations\Emby.Server.Implementations.csproj" /> + <ProjectReference Include="..\..\..\MediaBrowser.Common\MediaBrowser.Common.csproj" /> + </ItemGroup> + <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> + <CodeAnalysisRuleSet>../../jellyfin-tests.ruleset</CodeAnalysisRuleSet> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> + <DefineConstants>DEBUG</DefineConstants> + </PropertyGroup> +</Project> diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs new file mode 100644 index 000000000..c350685af --- /dev/null +++ b/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs @@ -0,0 +1,519 @@ +using System; +using System.Net; +using Jellyfin.Networking.Configuration; +using Jellyfin.Networking.Manager; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; +using Moq; +using Microsoft.Extensions.Logging.Abstractions; +using Xunit; +using System.Collections.ObjectModel; + +namespace Jellyfin.Networking.Tests +{ + public class NetworkParseTests + { + /// <summary> + /// Tries to identify the string and return an object of that class. + /// </summary> + /// <param name="addr">String to parse.</param> + /// <param name="result">IPObject to return.</param> + /// <returns>True if the value parsed successfully.</returns> + 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; + } + + private static IConfigurationManager GetMockConfig(NetworkConfiguration conf) + { + var configManager = new Mock<IConfigurationManager> + { + CallBase = true + }; + configManager.Setup(x => x.GetConfiguration(It.IsAny<string>())).Returns(conf); + return (IConfigurationManager)configManager.Object; + } + + /// <summary> + /// Checks the ability to ignore interfaces + /// </summary> + /// <param name="interfaces">Mock network setup, in the format (IP address, interface index, interface name) : .... </param> + /// <param name="lan">LAN addresses.</param> + /// <param name="value">Bind addresses that are excluded.</param> + [Theory] + [InlineData("192.168.1.208/24,-16,eth16:200.200.200.200/24,11,eth11", "192.168.1.0/24;200.200.200.0/24", "[192.168.1.208/24,200.200.200.200/24]")] + [InlineData("192.168.1.208/24,-16,eth16:200.200.200.200/24,11,eth11", "192.168.1.0/24", "[192.168.1.208/24]")] + [InlineData("192.168.1.208/24,-16,vEthernet1:192.168.1.208/24,-16,vEthernet212;200.200.200.200/24,11,eth11", "192.168.1.0/24", "[192.168.1.208/24]")] + public void IgnoreVirtualInterfaces(string interfaces, string lan, string value) + { + var conf = new NetworkConfiguration() + { + EnableIPV6 = true, + EnableIPV4 = true, + LocalNetworkSubnets = lan?.Split(';') ?? throw new ArgumentNullException(nameof(lan)) + }; + + NetworkManager.MockNetworkSettings = interfaces; + using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); + NetworkManager.MockNetworkSettings = string.Empty; + + Assert.Equal(nm.GetInternalBindAddresses().AsString(), value); + } + + /// <summary> + /// Check that the value given is in the network provided. + /// </summary> + /// <param name="network">Network address.</param> + /// <param name="value">Value to check.</param> + [Theory] + [InlineData("192.168.10.0/24, !192.168.10.60/32", "192.168.10.60")] + public void IsInNetwork(string network, string value) + { + if (network == null) + { + throw new ArgumentNullException(nameof(network)); + } + + var conf = new NetworkConfiguration() + { + EnableIPV6 = true, + EnableIPV4 = true, + LocalNetworkSubnets = network.Split(',') + }; + + using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); + + Assert.False(nm.IsInLocalNetwork(value)); + } + + /// <summary> + /// Checks IP address formats. + /// </summary> + /// <param name="address"></param> + [Theory] + [InlineData("127.0.0.1")] + [InlineData("127.0.0.1:123")] + [InlineData("localhost")] + [InlineData("localhost:1345")] + [InlineData("www.google.co.uk")] + [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517")] + [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517/56")] + [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517]:124")] + [InlineData("fe80::7add:12ff:febb:c67b%16")] + [InlineData("[fe80::7add:12ff:febb:c67b%16]:123")] + [InlineData("192.168.1.2/255.255.255.0")] + [InlineData("192.168.1.2/24")] + public void ValidIPStrings(string address) + { + Assert.True(TryParse(address, out _)); + } + + + /// <summary> + /// All should be invalid address strings. + /// </summary> + /// <param name="address">Invalid address strings.</param> + [Theory] + [InlineData("256.128.0.0.0.1")] + [InlineData("127.0.0.1#")] + [InlineData("localhost!")] + [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517:1231")] + [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517:1231]")] + public void InvalidAddressString(string address) + { + Assert.False(TryParse(address, out _)); + } + + + /// <summary> + /// Test collection parsing. + /// </summary> + /// <param name="settings">Collection to parse.</param> + /// <param name="result1">Included addresses from the collection.</param> + /// <param name="result2">Included IP4 addresses from the collection.</param> + /// <param name="result3">Excluded addresses from the collection.</param> + /// <param name="result4">Excluded IP4 addresses from the collection.</param> + /// <param name="result5">Network addresses of the collection.</param> + [Theory] + [InlineData("127.0.0.1#", + "[]", + "[]", + "[]", + "[]", + "[]")] + [InlineData("!127.0.0.1", + "[]", + "[]", + "[127.0.0.1/32]", + "[127.0.0.1/32]", + "[]")] + [InlineData("", + "[]", + "[]", + "[]", + "[]", + "[]")] + [InlineData( + "192.158.1.2/16, localhost, fd23:184f:2029:0:3139:7386:67d7:d517, !10.10.10.10", + "[192.158.1.2/16,127.0.0.1/32,fd23:184f:2029:0:3139:7386:67d7:d517/128]", + "[192.158.1.2/16,127.0.0.1/32]", + "[10.10.10.10/32]", + "[10.10.10.10/32]", + "[192.158.0.0/16,127.0.0.1/32,fd23:184f:2029:0:3139:7386:67d7:d517/128]")] + [InlineData("192.158.1.2/255.255.0.0,192.169.1.2/8", + "[192.158.1.2/16,192.169.1.2/8]", + "[192.158.1.2/16,192.169.1.2/8]", + "[]", + "[]", + "[192.158.0.0/16,192.0.0.0/8]")] + public void TestCollections(string settings, string result1, string result2, string result3, string result4, string result5) + { + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + + var conf = new NetworkConfiguration() + { + EnableIPV6 = true, + EnableIPV4 = true, + }; + + using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); + + // Test included. + Collection<IPObject> nc = nm.CreateIPCollection(settings.Split(","), false); + Assert.Equal(nc.AsString(), result1); + + // Test excluded. + nc = nm.CreateIPCollection(settings.Split(","), true); + Assert.Equal(nc.AsString(), result3); + + conf.EnableIPV6 = false; + nm.UpdateSettings(conf); + + // Test IP4 included. + nc = nm.CreateIPCollection(settings.Split(","), false); + Assert.Equal(nc.AsString(), result2); + + // Test IP4 excluded. + nc = nm.CreateIPCollection(settings.Split(","), true); + Assert.Equal(nc.AsString(), result4); + + conf.EnableIPV6 = true; + nm.UpdateSettings(conf); + + // Test network addresses of collection. + nc = nm.CreateIPCollection(settings.Split(","), false); + nc = nc.AsNetworks(); + Assert.Equal(nc.AsString(), result5); + } + + /// <summary> + /// Union two collections. + /// </summary> + /// <param name="settings">Source.</param> + /// <param name="compare">Destination.</param> + /// <param name="result">Result.</param> + [Theory] + [InlineData("127.0.0.1", "fd23:184f:2029:0:3139:7386:67d7:d517/64,fd23:184f:2029:0:c0f0:8a8a:7605:fffa/128,fe80::3139:7386:67d7:d517%16/64,192.168.1.208/24,::1/128,127.0.0.1/8", "[127.0.0.1/32]")] + [InlineData("127.0.0.1", "127.0.0.1/8", "[127.0.0.1/32]")] + public void UnionCheck(string settings, string compare, string result) + { + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + + if (compare == null) + { + throw new ArgumentNullException(nameof(compare)); + } + + if (result == null) + { + throw new ArgumentNullException(nameof(result)); + } + + + var conf = new NetworkConfiguration() + { + EnableIPV6 = true, + EnableIPV4 = true, + }; + + using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); + + Collection<IPObject> nc1 = nm.CreateIPCollection(settings.Split(","), false); + Collection<IPObject> nc2 = nm.CreateIPCollection(compare.Split(","), false); + + Assert.Equal(nc1.Union(nc2).AsString(), result); + } + + [Theory] + [InlineData("192.168.5.85/24", "192.168.5.1")] + [InlineData("192.168.5.85/24", "192.168.5.254")] + [InlineData("10.128.240.50/30", "10.128.240.48")] + [InlineData("10.128.240.50/30", "10.128.240.49")] + [InlineData("10.128.240.50/30", "10.128.240.50")] + [InlineData("10.128.240.50/30", "10.128.240.51")] + [InlineData("127.0.0.1/8", "127.0.0.1")] + public void IpV4SubnetMaskMatchesValidIpAddress(string netMask, string ipAddress) + { + var ipAddressObj = IPNetAddress.Parse(netMask); + Assert.True(ipAddressObj.Contains(IPAddress.Parse(ipAddress))); + } + + [Theory] + [InlineData("192.168.5.85/24", "192.168.4.254")] + [InlineData("192.168.5.85/24", "191.168.5.254")] + [InlineData("10.128.240.50/30", "10.128.240.47")] + [InlineData("10.128.240.50/30", "10.128.240.52")] + [InlineData("10.128.240.50/30", "10.128.239.50")] + [InlineData("10.128.240.50/30", "10.127.240.51")] + public void IpV4SubnetMaskDoesNotMatchInvalidIpAddress(string netMask, string ipAddress) + { + var ipAddressObj = IPNetAddress.Parse(netMask); + Assert.False(ipAddressObj.Contains(IPAddress.Parse(ipAddress))); + } + + [Theory] + [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0012:0000:0000:0000:0000")] + [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0012:FFFF:FFFF:FFFF:FFFF")] + [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0012:0001:0000:0000:0000")] + [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0012:FFFF:FFFF:FFFF:FFF0")] + [InlineData("2001:db8:abcd:0012::0/128", "2001:0DB8:ABCD:0012:0000:0000:0000:0000")] + public void IpV6SubnetMaskMatchesValidIpAddress(string netMask, string ipAddress) + { + var ipAddressObj = IPNetAddress.Parse(netMask); + Assert.True(ipAddressObj.Contains(IPAddress.Parse(ipAddress))); + } + + [Theory] + [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0011:FFFF:FFFF:FFFF:FFFF")] + [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0013:0000:0000:0000:0000")] + [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0013:0001:0000:0000:0000")] + [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0011:FFFF:FFFF:FFFF:FFF0")] + [InlineData("2001:db8:abcd:0012::0/128", "2001:0DB8:ABCD:0012:0000:0000:0000:0001")] + public void IpV6SubnetMaskDoesNotMatchInvalidIpAddress(string netMask, string ipAddress) + { + var ipAddressObj = IPNetAddress.Parse(netMask); + Assert.False(ipAddressObj.Contains(IPAddress.Parse(ipAddress))); + } + + [Theory] + [InlineData("10.0.0.0/255.0.0.0", "10.10.10.1/32")] + [InlineData("10.0.0.0/8", "10.10.10.1/32")] + [InlineData("10.0.0.0/255.0.0.0", "10.10.10.1")] + + [InlineData("10.10.0.0/255.255.0.0", "10.10.10.1/32")] + [InlineData("10.10.0.0/16", "10.10.10.1/32")] + [InlineData("10.10.0.0/255.255.0.0", "10.10.10.1")] + + [InlineData("10.10.10.0/255.255.255.0", "10.10.10.1/32")] + [InlineData("10.10.10.0/24", "10.10.10.1/32")] + [InlineData("10.10.10.0/255.255.255.0", "10.10.10.1")] + + public void TestSubnetContains(string network, string ip) + { + Assert.True(TryParse(network, out IPObject? networkObj)); + Assert.True(TryParse(ip, out IPObject? ipObj)); + Assert.True(networkObj.Contains(ipObj)); + } + + [Theory] + [InlineData("192.168.1.2/24,10.10.10.1/24,172.168.1.2/24", "172.168.1.2/24", "172.168.1.2/24")] + [InlineData("192.168.1.2/24,10.10.10.1/24,172.168.1.2/24", "172.168.1.2/24, 10.10.10.1", "172.168.1.2/24,10.10.10.1/24")] + [InlineData("192.168.1.2/24,10.10.10.1/24,172.168.1.2/24", "192.168.1.2/255.255.255.0, 10.10.10.1", "192.168.1.2/24,10.10.10.1/24")] + [InlineData("192.168.1.2/24,10.10.10.1/24,172.168.1.2/24", "192.168.1.2/24, 100.10.10.1", "192.168.1.2/24")] + [InlineData("192.168.1.2/24,10.10.10.1/24,172.168.1.2/24", "194.168.1.2/24, 100.10.10.1", "")] + + public void TestCollectionEquality(string source, string dest, string result) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (dest == null) + { + throw new ArgumentNullException(nameof(dest)); + } + + if (result == null) + { + throw new ArgumentNullException(nameof(result)); + } + + var conf = new NetworkConfiguration() + { + EnableIPV6 = true, + EnableIPV4 = true + }; + + using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); + + // Test included, IP6. + Collection<IPObject> ncSource = nm.CreateIPCollection(source.Split(",")); + Collection<IPObject> ncDest = nm.CreateIPCollection(dest.Split(",")); + Collection<IPObject> ncResult = ncSource.Union(ncDest); + Collection<IPObject> resultCollection = nm.CreateIPCollection(result.Split(",")); + Assert.True(ncResult.Compare(resultCollection)); + } + + + [Theory] + [InlineData("10.1.1.1/32", "10.1.1.1")] + [InlineData("192.168.1.254/32", "192.168.1.254/255.255.255.255")] + + public void TestEquals(string source, string dest) + { + Assert.True(IPNetAddress.Parse(source).Equals(IPNetAddress.Parse(dest))); + Assert.True(IPNetAddress.Parse(dest).Equals(IPNetAddress.Parse(source))); + } + + [Theory] + + // Testing bind interfaces. + // On my system eth16 is internal, eth11 external (Windows defines the indexes). + // + // This test is to replicate how DNLA requests work throughout the system. + + // User on internal network, we're bound internal and external - so result is internal. + [InlineData("192.168.1.1", "eth16,eth11", false, "eth16")] + // User on external network, we're bound internal and external - so result is external. + [InlineData("8.8.8.8", "eth16,eth11", false, "eth11")] + // User on internal network, we're bound internal only - so result is internal. + [InlineData("10.10.10.10", "eth16", false, "eth16")] + // User on internal network, no binding specified - so result is the 1st internal. + [InlineData("192.168.1.1", "", false, "eth16")] + // User on external network, internal binding only - so result is the 1st internal. + [InlineData("jellyfin.org", "eth16", false, "eth16")] + // User on external network, no binding - so result is the 1st external. + [InlineData("jellyfin.org", "", false, "eth11")] + // User assumed to be internal, no binding - so result is the 1st internal. + [InlineData("", "", false, "eth16")] + public void TestBindInterfaces(string source, string bindAddresses, bool ipv6enabled, string result) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (bindAddresses == null) + { + throw new ArgumentNullException(nameof(bindAddresses)); + } + + if (result == null) + { + throw new ArgumentNullException(nameof(result)); + } + + var conf = new NetworkConfiguration() + { + LocalNetworkAddresses = bindAddresses.Split(','), + EnableIPV6 = ipv6enabled, + EnableIPV4 = true + }; + + NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16:200.200.200.200/24,11,eth11"; + using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); + NetworkManager.MockNetworkSettings = string.Empty; + + _ = nm.TryParseInterface(result, out Collection<IPObject>? resultObj); + + if (resultObj != null) + { + result = ((IPNetAddress)resultObj[0]).ToString(true); + var intf = nm.GetBindInterface(source, out int? _); + + Assert.Equal(intf, result); + } + } + + [Theory] + + // Testing bind interfaces. These are set for my system so won't work elsewhere. + // On my system eth16 is internal, eth11 external (Windows defines the indexes). + // + // This test is to replicate how subnet bound ServerPublisherUri work throughout the system. + + // User on internal network, we're bound internal and external - so result is internal override. + [InlineData("192.168.1.1", "192.168.1.0/24", "eth16,eth11", false, "192.168.1.0/24=internal.jellyfin", "internal.jellyfin")] + + // User on external network, we're bound internal and external - so result is override. + [InlineData("8.8.8.8", "192.168.1.0/24", "eth16,eth11", false, "0.0.0.0=http://helloworld.com", "http://helloworld.com")] + + // User on internal network, we're bound internal only, but the address isn't in the LAN - so return the override. + [InlineData("10.10.10.10", "192.168.1.0/24", "eth16", false, "0.0.0.0=http://internalButNotDefinedAsLan.com", "http://internalButNotDefinedAsLan.com")] + + // User on internal network, no binding specified - so result is the 1st internal. + [InlineData("192.168.1.1", "192.168.1.0/24", "", false, "0.0.0.0=http://helloworld.com", "eth16")] + + // User on external network, internal binding only - so asumption is a proxy forward, return external override. + [InlineData("jellyfin.org", "192.168.1.0/24", "eth16", false, "0.0.0.0=http://helloworld.com", "http://helloworld.com")] + + // User on external network, no binding - so result is the 1st external which is overriden. + [InlineData("jellyfin.org", "192.168.1.0/24", "", false, "0.0.0.0 = http://helloworld.com", "http://helloworld.com")] + + // User assumed to be internal, no binding - so result is the 1st internal. + [InlineData("", "192.168.1.0/24", "", false, "0.0.0.0=http://helloworld.com", "eth16")] + + // User is internal, no binding - so result is the 1st internal, which is then overridden. + [InlineData("192.168.1.1", "192.168.1.0/24", "", false, "eth16=http://helloworld.com", "http://helloworld.com")] + + public void TestBindInterfaceOverrides(string source, string lan, string bindAddresses, bool ipv6enabled, string publishedServers, string result) + { + if (lan == null) + { + throw new ArgumentNullException(nameof(lan)); + } + + if (bindAddresses == null) + { + throw new ArgumentNullException(nameof(bindAddresses)); + } + + var conf = new NetworkConfiguration() + { + LocalNetworkSubnets = lan.Split(','), + LocalNetworkAddresses = bindAddresses.Split(','), + EnableIPV6 = ipv6enabled, + EnableIPV4 = true, + PublishedServerUriBySubnet = new string[] { publishedServers } + }; + + NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16:200.200.200.200/24,11,eth11"; + using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); + NetworkManager.MockNetworkSettings = string.Empty; + + if (nm.TryParseInterface(result, out Collection<IPObject>? resultObj) && resultObj != null) + { + // Parse out IPAddresses so we can do a string comparison. (Ignore subnet masks). + result = ((IPNetAddress)resultObj[0]).ToString(true); + } + + var intf = nm.GetBindInterface(source, out int? _); + + Assert.Equal(intf, result); + } + } +} diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj index 547f80ed9..fffbc6212 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj +++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj @@ -17,7 +17,7 @@ <PackageReference Include="AutoFixture" Version="4.14.0" /> <PackageReference Include="AutoFixture.AutoMoq" Version="4.14.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" /> - <PackageReference Include="Moq" Version="4.15.1" /> + <PackageReference Include="Moq" Version="4.15.2" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="coverlet.collector" Version="1.3.0" /> |
