From 24d99bdc3e45ea1b94c9d6285de5784ec6565e93 Mon Sep 17 00:00:00 2001 From: h1dden-da3m0n <33120068+h1dden-da3m0n@users.noreply.github.com> Date: Sun, 20 Jun 2021 19:21:14 +0200 Subject: [PATCH 001/858] add auto-bump_version workflow --- .github/workflows/auto-bump_version.yml | 96 +++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 .github/workflows/auto-bump_version.yml diff --git a/.github/workflows/auto-bump_version.yml b/.github/workflows/auto-bump_version.yml new file mode 100644 index 0000000000..67f2e60643 --- /dev/null +++ b/.github/workflows/auto-bump_version.yml @@ -0,0 +1,96 @@ +name: Auto bump_version + +on: + release: + types: + - published + workflow_dispatch: + inputs: + TAG_BRANCH: + required: true + description: release-x.y.z + NEXT_VERSION: + required: true + description: x.y.z + +jobs: + auto_bump_version: + runs-on: ubuntu-latest + if: ${{ github.event_name == 'release' && !contains(github.event.release.tag_name, 'rc') }} + env: + TAG_BRANCH: ${{ github.event.release.target_commitish }} + steps: + - name: Wait for deploy checks to finish + uses: jitterbit/await-check-suites@v1 + with: + ref: ${{ env.TAG_BRANCH }} + intervalSeconds: 60 + timeoutSeconds: 3600 + + - name: Setup YQ + uses: chrisdickinson/setup-yq@latest + with: + yq-version: v4.9.6 + + - name: Checkout Repository + uses: actions/checkout@v2 + with: + ref: ${{ env.TAG_BRANCH }} + + - name: Setup EnvVars + run: |- + CURRENT_VERSION=$(yq e '.version' build.yaml) + CURRENT_MAJOR_MINOR=${CURRENT_VERSION%.*} + CURRENT_PATCH=${CURRENT_VERSION##*.} + echo "CURRENT_VERSION=${CURRENT_VERSION}" >> $GITHUB_ENV + echo "CURRENT_MAJOR_MINOR=${CURRENT_MAJOR_MINOR}" >> $GITHUB_ENV + echo "CURRENT_PATCH=${CURRENT_PATCH}" >> $GITHUB_ENV + echo "NEXT_VERSION=${CURRENT_MAJOR_MINOR}.$(($CURRENT_PATCH + 1))" >> $GITHUB_ENV + + - name: Run bump_version + run: ./bump_version ${{ env.NEXT_VERSION }} + + - name: Commit Changes + run: |- + git config user.name "jellyfin-bot" + git config user.email "team@jellyfin.org" + git checkout ${{ env.TAG_BRANCH }} + git commit -am "Bump version to ${{ env.NEXT_VERSION }}" + git push origin ${{ env.TAG_BRANCH }} + + manual_bump_version: + runs-on: ubuntu-latest + if: ${{ github.event_name == 'workflow_dispatch' }} + env: + TAG_BRANCH: ${{ github.event.inputs.TAG_BRANCH }} + steps: + - name: Setup YQ + uses: chrisdickinson/setup-yq@latest + with: + yq-version: v4.9.6 + + - name: Checkout Repository + uses: actions/checkout@v2 + with: + ref: ${{ env.TAG_BRANCH }} + + - name: Setup EnvVars + run: |- + CURRENT_VERSION=$(yq e '.version' build.yaml) + CURRENT_MAJOR_MINOR=${CURRENT_VERSION%.*} + CURRENT_PATCH=${CURRENT_VERSION##*.} + echo "CURRENT_VERSION=${CURRENT_VERSION}" >> $GITHUB_ENV + echo "CURRENT_MAJOR_MINOR=${CURRENT_MAJOR_MINOR}" >> $GITHUB_ENV + echo "CURRENT_PATCH=${CURRENT_PATCH}" >> $GITHUB_ENV + echo "NEXT_VERSION=${{ github.event.inputs.NEXT_VERSION }}" >> $GITHUB_ENV + + - name: Run bump_version + run: ./bump_version ${{ env.NEXT_VERSION }} + + - name: Commit Changes + run: |- + git config user.name "jellyfin-bot" + git config user.email "team@jellyfin.org" + git checkout ${{ env.TAG_BRANCH }} + git commit -am "Bump version to ${{ env.NEXT_VERSION }}" + git push origin ${{ env.TAG_BRANCH }} From 9923ea6151282df1204089289d59a158b6eaaf67 Mon Sep 17 00:00:00 2001 From: h1dden-da3m0n <33120068+h1dden-da3m0n@users.noreply.github.com> Date: Sun, 27 Jun 2021 13:20:54 +0200 Subject: [PATCH 002/858] change to address review feedback --- .github/workflows/auto-bump_version.yml | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/.github/workflows/auto-bump_version.yml b/.github/workflows/auto-bump_version.yml index 67f2e60643..dc0adb96b0 100644 --- a/.github/workflows/auto-bump_version.yml +++ b/.github/workflows/auto-bump_version.yml @@ -63,27 +63,13 @@ jobs: if: ${{ github.event_name == 'workflow_dispatch' }} env: TAG_BRANCH: ${{ github.event.inputs.TAG_BRANCH }} + NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }} steps: - - name: Setup YQ - uses: chrisdickinson/setup-yq@latest - with: - yq-version: v4.9.6 - - name: Checkout Repository uses: actions/checkout@v2 with: ref: ${{ env.TAG_BRANCH }} - - name: Setup EnvVars - run: |- - CURRENT_VERSION=$(yq e '.version' build.yaml) - CURRENT_MAJOR_MINOR=${CURRENT_VERSION%.*} - CURRENT_PATCH=${CURRENT_VERSION##*.} - echo "CURRENT_VERSION=${CURRENT_VERSION}" >> $GITHUB_ENV - echo "CURRENT_MAJOR_MINOR=${CURRENT_MAJOR_MINOR}" >> $GITHUB_ENV - echo "CURRENT_PATCH=${CURRENT_PATCH}" >> $GITHUB_ENV - echo "NEXT_VERSION=${{ github.event.inputs.NEXT_VERSION }}" >> $GITHUB_ENV - - name: Run bump_version run: ./bump_version ${{ env.NEXT_VERSION }} From 25fc8a1c934706d1a0cf8c424f379e6e708a3283 Mon Sep 17 00:00:00 2001 From: h1dden-da3m0n <33120068+h1dden-da3m0n@users.noreply.github.com> Date: Sun, 12 Sep 2021 21:52:14 +0200 Subject: [PATCH 003/858] ci: unify name to web equivalent workflow see jellyfin/jellyfin-web#2897 for reference --- .../{auto-bump_version.yml => repo-bump-version.yaml} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename .github/workflows/{auto-bump_version.yml => repo-bump-version.yaml} (97%) diff --git a/.github/workflows/auto-bump_version.yml b/.github/workflows/repo-bump-version.yaml similarity index 97% rename from .github/workflows/auto-bump_version.yml rename to .github/workflows/repo-bump-version.yaml index dc0adb96b0..351e24576f 100644 --- a/.github/workflows/auto-bump_version.yml +++ b/.github/workflows/repo-bump-version.yaml @@ -1,4 +1,4 @@ -name: Auto bump_version +name: '🆙 Auto bump_version' on: release: @@ -30,7 +30,7 @@ jobs: - name: Setup YQ uses: chrisdickinson/setup-yq@latest with: - yq-version: v4.9.6 + yq-version: v4.9.8 - name: Checkout Repository uses: actions/checkout@v2 From 066db8ac7fcece0ab3420b6b6c03e420d22c7306 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Tue, 19 Jul 2022 21:28:04 +0200 Subject: [PATCH 004/858] Migrate NetworkManager and Tests to native .NET IP objects --- Emby.Dlna/Main/DlnaEntryPoint.cs | 19 +- .../ApplicationHost.cs | 4 +- .../Configuration/NetworkConfiguration.cs | 10 +- Jellyfin.Networking/Manager/NetworkManager.cs | 1770 ++++++++--------- .../ApiServiceCollectionExtensions.cs | 8 +- .../CreateNetworkConfiguration.cs | 4 +- Jellyfin.Server/Program.cs | 6 +- MediaBrowser.Common/Net/INetworkManager.cs | 128 +- MediaBrowser.Common/Net/IPData.cs | 76 + MediaBrowser.Common/Net/IPHost.cs | 441 ---- MediaBrowser.Common/Net/IPNetAddress.cs | 276 --- MediaBrowser.Common/Net/IPObject.cs | 370 ---- MediaBrowser.Common/Net/NetworkExtensions.cs | 343 ++-- .../IServerApplicationHost.cs | 5 +- RSSDP/SsdpDevicePublisher.cs | 7 +- .../IPNetAddressTests.cs | 49 - ...HostTests.cs => NetworkExtensionsTests.cs} | 10 +- .../NetworkParseTests.cs | 253 +-- 18 files changed, 1116 insertions(+), 2663 deletions(-) create mode 100644 MediaBrowser.Common/Net/IPData.cs delete mode 100644 MediaBrowser.Common/Net/IPHost.cs delete mode 100644 MediaBrowser.Common/Net/IPNetAddress.cs delete mode 100644 MediaBrowser.Common/Net/IPObject.cs delete mode 100644 tests/Jellyfin.Networking.Tests/IPNetAddressTests.cs rename tests/Jellyfin.Networking.Tests/{IPHostTests.cs => NetworkExtensionsTests.cs} (80%) diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index 15021c19d6..01a9def0bf 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -3,15 +3,16 @@ #pragma warning disable CS1591 using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Net; using System.Net.Http; using System.Net.Sockets; using System.Threading.Tasks; using Emby.Dlna.PlayTo; using Emby.Dlna.Ssdp; using Jellyfin.Networking.Configuration; -using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; @@ -285,9 +286,11 @@ namespace Emby.Dlna.Main var udn = CreateUuid(_appHost.SystemId); var descriptorUri = "/dlna/" + udn + "/description.xml"; - var bindAddresses = NetworkManager.CreateCollection( - _networkManager.GetInternalBindAddresses() - .Where(i => i.AddressFamily == AddressFamily.InterNetwork || (i.AddressFamily == AddressFamily.InterNetworkV6 && i.Address.ScopeId != 0))); + var bindAddresses = _networkManager + .GetInternalBindAddresses() + .Where(i => i.Address.AddressFamily == AddressFamily.InterNetwork + || (i.AddressFamily == AddressFamily.InterNetworkV6 && i.Address.ScopeId != 0)) + .ToList(); if (bindAddresses.Count == 0) { @@ -295,7 +298,7 @@ namespace Emby.Dlna.Main bindAddresses = _networkManager.GetLoopbacks(); } - foreach (IPNetAddress address in bindAddresses) + foreach (var address in bindAddresses) { if (address.AddressFamily == AddressFamily.InterNetworkV6) { @@ -304,7 +307,7 @@ namespace Emby.Dlna.Main } // Limit to LAN addresses only - if (!_networkManager.IsInLocalNetwork(address)) + if (!_networkManager.IsInLocalNetwork(address.Address)) { continue; } @@ -313,14 +316,14 @@ namespace Emby.Dlna.Main _logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, address); - var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(address, false) + descriptorUri); + var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(address.Address, false) + descriptorUri); var device = new SsdpRootDevice { CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info. Location = uri.Uri, // Must point to the URL that serves your devices UPnP description document. Address = address.Address, - PrefixLength = address.PrefixLength, + PrefixLength = NetworkExtensions.MaskToCidr(address.Subnet.Prefix), FriendlyName = "Jellyfin", Manufacturer = "Jellyfin", ModelName = "Jellyfin Server", diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 32289625fe..a0ad4a9ab5 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1114,10 +1114,10 @@ namespace Emby.Server.Implementations } /// - public string GetApiUrlForLocalAccess(IPObject hostname = null, bool allowHttps = true) + public string GetApiUrlForLocalAccess(IPAddress ipAddress = null, bool allowHttps = true) { // With an empty source, the port will be null - var smart = NetManager.GetBindInterface(hostname ?? IPHost.None, out _); + var smart = NetManager.GetBindInterface(ipAddress, out _); var scheme = !allowHttps ? Uri.UriSchemeHttp : null; int? port = !allowHttps ? HttpPort : null; return GetLocalApiUrl(smart, scheme, port); diff --git a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs index 61db223d92..0ac55c986e 100644 --- a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs +++ b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs @@ -113,12 +113,12 @@ namespace Jellyfin.Networking.Configuration public string UDPPortRange { get; set; } = string.Empty; /// - /// Gets or sets a value indicating whether gets or sets IPV6 capability. + /// Gets or sets a value indicating whether IPv6 is enabled or not. /// public bool EnableIPV6 { get; set; } /// - /// Gets or sets a value indicating whether gets or sets IPV4 capability. + /// Gets or sets a value indicating whether IPv6 is enabled or not. /// public bool EnableIPV4 { get; set; } = true; @@ -165,12 +165,6 @@ namespace Jellyfin.Networking.Configuration /// public bool EnableMultiSocketBinding { get; } = true; - /// - /// Gets or sets a value indicating whether all IPv6 interfaces should be treated as on the internal network. - /// Depending on the address range implemented ULA ranges might not be used. - /// - public bool TrustAllIP6Interfaces { get; set; } - /// /// Gets or sets the ports that HDHomerun uses. /// diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 4b7b87814c..f51fd85dd5 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Net; @@ -12,30 +11,25 @@ using Jellyfin.Networking.Configuration; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.HttpOverrides; using Microsoft.Extensions.Logging; namespace Jellyfin.Networking.Manager { /// /// Class to take care of network interface management. - /// Note: The normal collection methods and properties will not work with Collection{IPObject}. . /// public class NetworkManager : INetworkManager, IDisposable { - /// - /// Contains the description of the interface along with its index. - /// - private readonly Dictionary _interfaceNames; - /// /// Threading lock for network properties. /// - private readonly object _intLock = new object(); + private readonly object _initLock; /// - /// List of all interface addresses and masks. + /// Dictionary containing interface addresses and their subnets. /// - private readonly Collection _interfaceAddresses; + private readonly List _interfaces; /// /// List of all interface MAC addresses. @@ -49,9 +43,11 @@ namespace Jellyfin.Networking.Manager private readonly object _eventFireLock; /// - /// Holds the bind address overrides. + /// Holds the published server URLs and the IPs to use them on. /// - private readonly Dictionary _publishedServerUrls; + private readonly Dictionary _publishedServerUrls; + + private Collection _remoteAddressFilter; /// /// Used to stop "event-racing conditions". @@ -59,35 +55,25 @@ namespace Jellyfin.Networking.Manager private bool _eventfire; /// - /// Unfiltered user defined LAN subnets. () + /// Unfiltered user defined LAN subnets () /// or internal interface network subnets if undefined by user. /// - private Collection _lanSubnets; + private Collection _lanSubnets; /// /// User defined list of subnets to excluded from the LAN. /// - private Collection _excludedSubnets; + private Collection _excludedSubnets; /// - /// List of interface addresses to bind the WS. + /// List of interfaces to bind to. /// - private Collection _bindAddresses; + private List _bindAddresses; /// /// List of interface addresses to exclude from bind. /// - private Collection _bindExclusions; - - /// - /// Caches list of all internal filtered interface addresses and masks. - /// - private Collection _internalInterfaces; - - /// - /// Flag set when no custom LAN has been defined in the configuration. - /// - private bool _usingPrivateAddresses; + private List _bindExclusions; /// /// True if this object is disposed. @@ -104,12 +90,12 @@ namespace Jellyfin.Networking.Manager { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _configurationManager = configurationManager ?? throw new ArgumentNullException(nameof(configurationManager)); - - _interfaceAddresses = new Collection(); + _initLock = new(); + _interfaces = new List(); _macAddresses = new List(); - _interfaceNames = new Dictionary(); - _publishedServerUrls = new Dictionary(); + _publishedServerUrls = new Dictionary(); _eventFireLock = new object(); + _remoteAddressFilter = new Collection(); UpdateSettings(_configurationManager.GetNetworkConfiguration()); @@ -131,46 +117,24 @@ namespace Jellyfin.Networking.Manager public static string MockNetworkSettings { get; set; } = string.Empty; /// - /// Gets or sets a value indicating whether IP6 is enabled. + /// Gets a value indicating whether IP4 is enabled. /// - public bool IsIP6Enabled { get; set; } + public bool IsIpv4Enabled => _configurationManager.GetNetworkConfiguration().EnableIPV4; /// - /// Gets or sets a value indicating whether IP4 is enabled. + /// Gets a value indicating whether IP6 is enabled. /// - public bool IsIP4Enabled { get; set; } - - /// - public Collection RemoteAddressFilter { get; private set; } + public bool IsIpv6Enabled => _configurationManager.GetNetworkConfiguration().EnableIPV6; /// /// Gets a value indicating whether is all IPv6 interfaces are trusted as internal. /// - public bool TrustAllIP6Interfaces { get; internal set; } + public bool TrustAllIpv6Interfaces { get; private set; } /// /// Gets the Published server override list. /// - public Dictionary PublishedServerUrls => _publishedServerUrls; - - /// - /// Creates a new network collection. - /// - /// Items to assign the collection, or null. - /// The collection created. - public static Collection CreateCollection(IEnumerable? source = null) - { - var result = new Collection(); - if (source != null) - { - foreach (var item in source) - { - result.AddItem(item, false); - } - } - - return result; - } + public Dictionary PublishedServerUrls => _publishedServerUrls; /// public void Dispose() @@ -179,643 +143,6 @@ namespace Jellyfin.Networking.Manager GC.SuppressFinalize(this); } - /// - public IReadOnlyCollection GetMacAddresses() - { - // Populated in construction - so always has values. - return _macAddresses; - } - - /// - public bool IsGatewayInterface(IPObject? addressObj) - { - var address = addressObj?.Address ?? IPAddress.None; - return _internalInterfaces.Any(i => i.Address.Equals(address) && i.Tag < 0); - } - - /// - public bool IsGatewayInterface(IPAddress? addressObj) - { - return _internalInterfaces.Any(i => i.Address.Equals(addressObj ?? IPAddress.None) && i.Tag < 0); - } - - /// - public Collection GetLoopbacks() - { - Collection nc = new Collection(); - if (IsIP4Enabled) - { - nc.AddItem(IPAddress.Loopback); - } - - if (IsIP6Enabled) - { - nc.AddItem(IPAddress.IPv6Loopback); - } - - return nc; - } - - /// - public bool IsExcluded(IPAddress ip) - { - return _excludedSubnets.ContainsAddress(ip); - } - - /// - public bool IsExcluded(EndPoint ip) - { - return ip != null && IsExcluded(((IPEndPoint)ip).Address); - } - - /// - public Collection CreateIPCollection(string[] values, bool negated = false) - { - Collection col = new Collection(); - if (values == null) - { - return col; - } - - for (int a = 0; a < values.Length; a++) - { - string v = values[a].Trim(); - - try - { - if (v.StartsWith('!')) - { - if (negated) - { - AddToCollection(col, v[1..]); - } - } - else if (!negated) - { - AddToCollection(col, v); - } - } - catch (ArgumentException e) - { - _logger.LogWarning(e, "Ignoring LAN value {Value}.", v); - } - } - - return col; - } - - /// - public Collection GetAllBindInterfaces(bool individualInterfaces = false) - { - int count = _bindAddresses.Count; - - if (count == 0) - { - if (_bindExclusions.Count > 0) - { - // Return all the interfaces except the ones specifically excluded. - return _interfaceAddresses.Exclude(_bindExclusions, false); - } - - if (individualInterfaces) - { - return new Collection(_interfaceAddresses); - } - - // No bind address and no exclusions, so listen on all interfaces. - Collection result = new Collection(); - - if (IsIP6Enabled && IsIP4Enabled) - { - // Kestrel source code shows it uses Sockets.DualMode - so this also covers IPAddress.Any - result.AddItem(IPAddress.IPv6Any); - } - else if (IsIP4Enabled) - { - result.AddItem(IPAddress.Any); - } - else if (IsIP6Enabled) - { - // Cannot use IPv6Any as Kestrel will bind to IPv4 addresses. - foreach (var iface in _interfaceAddresses) - { - if (iface.AddressFamily == AddressFamily.InterNetworkV6) - { - result.AddItem(iface.Address); - } - } - } - - return result; - } - - // Remove any excluded bind interfaces. - return _bindAddresses.Exclude(_bindExclusions, false); - } - - /// - public string GetBindInterface(string source, out int? port) - { - if (!string.IsNullOrEmpty(source) && IPHost.TryParse(source, out IPHost host)) - { - return GetBindInterface(host, out port); - } - - return GetBindInterface(IPHost.None, out port); - } - - /// - public string GetBindInterface(IPAddress source, out int? port) - { - return GetBindInterface(new IPNetAddress(source), out port); - } - - /// - public string GetBindInterface(HttpRequest source, out int? port) - { - string result; - - if (source != null && IPHost.TryParse(source.Host.Host, out IPHost host)) - { - result = GetBindInterface(host, out port); - port ??= source.Host.Port; - } - else - { - result = GetBindInterface(IPNetAddress.None, out port); - port ??= source?.Host.Port; - } - - return result; - } - - /// - public string GetBindInterface(IPObject source, out int? port) - { - port = null; - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - // Do we have a source? - bool haveSource = !source.Address.Equals(IPAddress.None); - bool isExternal = false; - - if (haveSource) - { - if (!IsIP6Enabled && source.AddressFamily == AddressFamily.InterNetworkV6) - { - _logger.LogWarning("IPv6 is disabled in Jellyfin, but enabled in the OS. This may affect how the interface is selected."); - } - - if (!IsIP4Enabled && source.AddressFamily == AddressFamily.InterNetwork) - { - _logger.LogWarning("IPv4 is disabled in Jellyfin, but enabled in the OS. This may affect how the interface is selected."); - } - - isExternal = !IsInLocalNetwork(source); - - if (MatchesPublishedServerUrl(source, isExternal, out string res, out port)) - { - _logger.LogInformation("{Source}: Using BindAddress {Address}:{Port}", source, res, port); - return res; - } - } - - _logger.LogDebug("GetBindInterface: Source: {HaveSource}, External: {IsExternal}:", haveSource, isExternal); - - // No preference given, so move on to bind addresses. - if (MatchesBindInterface(source, isExternal, out string result)) - { - return result; - } - - if (isExternal && MatchesExternalInterface(source, out result)) - { - return result; - } - - // Get the first LAN interface address that isn't a loopback. - var interfaces = CreateCollection( - _interfaceAddresses - .Exclude(_bindExclusions, false) - .Where(IsInLocalNetwork) - .OrderBy(p => p.Tag)); - - if (interfaces.Count > 0) - { - if (haveSource) - { - foreach (var intf in interfaces) - { - if (intf.Address.Equals(source.Address)) - { - result = FormatIP6String(intf.Address); - _logger.LogDebug("{Source}: GetBindInterface: Has found matching interface. {Result}", source, result); - return result; - } - } - - // Does the request originate in one of the interface subnets? - // (For systems with multiple internal network cards, and multiple subnets) - foreach (var intf in interfaces) - { - if (intf.Contains(source)) - { - result = FormatIP6String(intf.Address); - _logger.LogDebug("{Source}: GetBindInterface: Has source, matched best internal interface on range. {Result}", source, result); - return result; - } - } - } - - result = FormatIP6String(interfaces.First().Address); - _logger.LogDebug("{Source}: GetBindInterface: Matched first internal interface. {Result}", source, result); - return result; - } - - // There isn't any others, so we'll use the loopback. - result = IsIP6Enabled ? "::1" : "127.0.0.1"; - _logger.LogWarning("{Source}: GetBindInterface: Loopback {Result} returned.", source, result); - return result; - } - - /// - public Collection GetInternalBindAddresses() - { - int count = _bindAddresses.Count; - - if (count == 0) - { - if (_bindExclusions.Count > 0) - { - // Return all the internal interfaces except the ones excluded. - return CreateCollection(_internalInterfaces.Where(p => !_bindExclusions.ContainsAddress(p))); - } - - // No bind address, so return all internal interfaces. - return CreateCollection(_internalInterfaces); - } - - return new Collection(_bindAddresses.Where(a => IsInLocalNetwork(a)).ToArray()); - } - - /// - public bool IsInLocalNetwork(IPObject address) - { - return IsInLocalNetwork(address.Address); - } - - /// - public bool IsInLocalNetwork(string address) - { - return IPHost.TryParse(address, out IPHost ipHost) && IsInLocalNetwork(ipHost); - } - - /// - public bool IsInLocalNetwork(IPAddress address) - { - if (address == null) - { - throw new ArgumentNullException(nameof(address)); - } - - if (address.Equals(IPAddress.None)) - { - return false; - } - - // See conversation at https://github.com/jellyfin/jellyfin/pull/3515. - if (TrustAllIP6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6) - { - return true; - } - - // As private addresses can be redefined by Configuration.LocalNetworkAddresses - return IPAddress.IsLoopback(address) || (_lanSubnets.ContainsAddress(address) && !_excludedSubnets.ContainsAddress(address)); - } - - /// - public bool IsPrivateAddressRange(IPObject address) - { - if (address == null) - { - throw new ArgumentNullException(nameof(address)); - } - - // See conversation at https://github.com/jellyfin/jellyfin/pull/3515. - if (TrustAllIP6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6) - { - return true; - } - else - { - return address.IsPrivateAddressRange(); - } - } - - /// - public bool IsExcludedInterface(IPAddress address) - { - return _bindExclusions.ContainsAddress(address); - } - - /// - public Collection GetFilteredLANSubnets(Collection? filter = null) - { - if (filter == null) - { - return _lanSubnets.Exclude(_excludedSubnets, true).AsNetworks(); - } - - return _lanSubnets.Exclude(filter, true); - } - - /// - public bool IsValidInterfaceAddress(IPAddress address) - { - return _interfaceAddresses.ContainsAddress(address); - } - - /// - public bool TryParseInterface(string token, out Collection? result) - { - result = null; - if (string.IsNullOrEmpty(token)) - { - return false; - } - - if (_interfaceNames != null && _interfaceNames.TryGetValue(token.ToLower(CultureInfo.InvariantCulture), out int index)) - { - result = new Collection(); - - _logger.LogInformation("Interface {Token} used in settings. Using its interface addresses.", token); - - // Replace interface tags with the interface IP's. - foreach (IPNetAddress iface in _interfaceAddresses) - { - if (Math.Abs(iface.Tag) == index - && ((IsIP4Enabled && iface.Address.AddressFamily == AddressFamily.InterNetwork) - || (IsIP6Enabled && iface.Address.AddressFamily == AddressFamily.InterNetworkV6))) - { - result.AddItem(iface, false); - } - } - - return true; - } - - return false; - } - - /// - public bool HasRemoteAccess(IPAddress remoteIp) - { - var config = _configurationManager.GetNetworkConfiguration(); - if (config.EnableRemoteAccess) - { - // 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. - if (RemoteAddressFilter.Count > 0 && !IsInLocalNetwork(remoteIp)) - { - // remoteAddressFilter is a whitelist or blacklist. - return RemoteAddressFilter.ContainsAddress(remoteIp) == !config.IsRemoteIPFilterBlacklist; - } - } - else if (!IsInLocalNetwork(remoteIp)) - { - // Remote not enabled. So everyone should be LAN. - return false; - } - - return true; - } - - /// - /// Reloads all settings and re-initialises the instance. - /// - /// The to use. - public void UpdateSettings(object configuration) - { - NetworkConfiguration config = (NetworkConfiguration)configuration ?? throw new ArgumentNullException(nameof(configuration)); - - IsIP4Enabled = Socket.OSSupportsIPv4 && config.EnableIPV4; - IsIP6Enabled = Socket.OSSupportsIPv6 && config.EnableIPV6; - - if (!IsIP6Enabled && !IsIP4Enabled) - { - _logger.LogError("IPv4 and IPv6 cannot both be disabled."); - IsIP4Enabled = true; - } - - TrustAllIP6Interfaces = config.TrustAllIP6Interfaces; - - if (string.IsNullOrEmpty(MockNetworkSettings)) - { - InitialiseInterfaces(); - } - else // Used in testing only. - { - // Format is ,,: . Set index to -ve to simulate a gateway. - var interfaceList = MockNetworkSettings.Split('|'); - foreach (var details in interfaceList) - { - var parts = details.Split(','); - var address = IPNetAddress.Parse(parts[0]); - var index = int.Parse(parts[1], CultureInfo.InvariantCulture); - address.Tag = index; - _interfaceAddresses.AddItem(address, false); - _interfaceNames[parts[2]] = Math.Abs(index); - } - } - - InitialiseLAN(config); - InitialiseBind(config); - InitialiseRemote(config); - InitialiseOverrides(config); - } - - /// - /// Protected implementation of Dispose pattern. - /// - /// True to dispose the managed state. - protected virtual void Dispose(bool disposing) - { - if (!_disposed) - { - if (disposing) - { - _configurationManager.NamedConfigurationUpdated -= ConfigurationUpdated; - NetworkChange.NetworkAddressChanged -= OnNetworkAddressChanged; - NetworkChange.NetworkAvailabilityChanged -= OnNetworkAvailabilityChanged; - } - - _disposed = true; - } - } - - /// - /// Tries to identify the string and return an object of that class. - /// - /// String to parse. - /// IPObject to return. - /// true if the value parsed successfully, false otherwise. - private static bool TryParse(string addr, out IPObject result) - { - if (!string.IsNullOrEmpty(addr)) - { - // Is it an IP address - if (IPNetAddress.TryParse(addr, out IPNetAddress nw)) - { - result = nw; - return true; - } - - if (IPHost.TryParse(addr, out IPHost h)) - { - result = h; - return true; - } - } - - result = IPNetAddress.None; - return false; - } - - /// - /// Converts an IPAddress into a string. - /// Ipv6 addresses are returned in [ ], with their scope removed. - /// - /// Address to convert. - /// URI safe conversion of the address. - private static string FormatIP6String(IPAddress address) - { - var str = address.ToString(); - if (address.AddressFamily == AddressFamily.InterNetworkV6) - { - int i = str.IndexOf("%", StringComparison.OrdinalIgnoreCase); - if (i != -1) - { - str = str.Substring(0, i); - } - - return $"[{str}]"; - } - - return str; - } - - private void ConfigurationUpdated(object? sender, ConfigurationUpdateEventArgs evt) - { - if (evt.Key.Equals(NetworkConfigurationStore.StoreKey, StringComparison.Ordinal)) - { - UpdateSettings((NetworkConfiguration)evt.NewConfiguration); - } - } - - /// - /// Checks the string to see if it matches any interface names. - /// - /// String to check. - /// Interface index numbers that match. - /// true if an interface name matches the token, False otherwise. - private bool TryGetInterfaces(string token, [NotNullWhen(true)] out List? index) - { - index = null; - - // Is it the name of an interface (windows) eg, Wireless LAN adapter Wireless Network Connection 1. - // Null check required here for automated testing. - if (_interfaceNames != null && token.Length > 1) - { - bool partial = token[^1] == '*'; - if (partial) - { - token = token[..^1]; - } - - foreach ((string interfc, int interfcIndex) in _interfaceNames) - { - if ((!partial && string.Equals(interfc, token, StringComparison.OrdinalIgnoreCase)) - || (partial && interfc.StartsWith(token, true, CultureInfo.InvariantCulture))) - { - index ??= new List(); - index.Add(interfcIndex); - } - } - } - - return index != null; - } - - /// - /// Parses a string and adds it into the collection, replacing any interface references. - /// - /// Collection. - /// String value to parse. - private void AddToCollection(Collection col, string token) - { - // Is it the name of an interface (windows) eg, Wireless LAN adapter Wireless Network Connection 1. - // Null check required here for automated testing. - if (TryGetInterfaces(token, out var indices)) - { - _logger.LogInformation("Interface {Token} used in settings. Using its interface addresses.", token); - - // Replace all the interface tags with the interface IP's. - foreach (IPNetAddress iface in _interfaceAddresses) - { - if (indices.Contains(Math.Abs(iface.Tag)) - && ((IsIP4Enabled && iface.Address.AddressFamily == AddressFamily.InterNetwork) - || (IsIP6Enabled && iface.Address.AddressFamily == AddressFamily.InterNetworkV6))) - { - col.AddItem(iface); - } - } - } - else if (TryParse(token, out IPObject obj)) - { - // 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); - if (!obj.IsIP6()) - { - col.AddItem(obj); - } - } - else if (!IsIP4Enabled) - { - // Remove IP4 addresses from multi-homed IPHosts. - obj.Remove(AddressFamily.InterNetwork); - if (obj.IsIP6()) - { - col.AddItem(obj); - } - } - else - { - col.AddItem(obj); - } - } - else - { - _logger.LogDebug("Invalid or unknown object {Token}.", token); - } - } - /// /// Handler for network change events. /// @@ -838,27 +165,6 @@ namespace Jellyfin.Networking.Manager OnNetworkChanged(); } - /// - /// Async task that waits for 2 seconds before re-initialising the settings, as typically these events fire multiple times in succession. - /// - /// A representing the asynchronous operation. - private async Task OnNetworkChangeAsync() - { - try - { - await Task.Delay(2000).ConfigureAwait(false); - InitialiseInterfaces(); - // Recalculate LAN caches. - InitialiseLAN(_configurationManager.GetNetworkConfiguration()); - - NetworkChanged?.Invoke(this, EventArgs.Empty); - } - finally - { - _eventfire = false; - } - } - /// /// Triggers our event, and re-loads interface information. /// @@ -877,160 +183,23 @@ namespace Jellyfin.Networking.Manager } /// - /// Parses the user defined overrides into the dictionary object. - /// Overrides are the equivalent of localised publishedServerUrl, enabling - /// different addresses to be advertised over different subnets. - /// format is subnet=ipaddress|host|uri - /// when subnet = 0.0.0.0, any external address matches. + /// Async task that waits for 2 seconds before re-initialising the settings, as typically these events fire multiple times in succession. /// - private void InitialiseOverrides(NetworkConfiguration config) + /// A representing the asynchronous operation. + private async Task OnNetworkChangeAsync() { - lock (_intLock) + try { - _publishedServerUrls.Clear(); - string[] overrides = config.PublishedServerUriBySubnet; - if (overrides == null) - { - return; - } + await Task.Delay(2000).ConfigureAwait(false); + InitialiseInterfaces(); + // Recalculate LAN caches. + InitialiseLan(_configurationManager.GetNetworkConfiguration()); - foreach (var entry in overrides) - { - var parts = entry.Split('='); - if (parts.Length != 2) - { - _logger.LogError("Unable to parse bind override: {Entry}", entry); - } - else - { - var replacement = parts[1].Trim(); - if (string.Equals(parts[0], "all", StringComparison.OrdinalIgnoreCase)) - { - _publishedServerUrls[new IPNetAddress(IPAddress.Broadcast)] = replacement; - } - else if (string.Equals(parts[0], "external", StringComparison.OrdinalIgnoreCase)) - { - _publishedServerUrls[new IPNetAddress(IPAddress.Any)] = replacement; - } - else if (TryParseInterface(parts[0], out Collection? addresses) && addresses != null) - { - foreach (IPNetAddress na in addresses) - { - _publishedServerUrls[na] = replacement; - } - } - else if (IPNetAddress.TryParse(parts[0], out IPNetAddress result)) - { - _publishedServerUrls[result] = replacement; - } - else - { - _logger.LogError("Unable to parse bind ip address. {Parts}", parts[1]); - } - } - } + NetworkChanged?.Invoke(this, EventArgs.Empty); } - } - - /// - /// Initialises the network bind addresses. - /// - private void InitialiseBind(NetworkConfiguration config) - { - lock (_intLock) + finally { - string[] lanAddresses = config.LocalNetworkAddresses; - - // Add virtual machine interface names to the list of bind exclusions, so that they are auto-excluded. - if (config.IgnoreVirtualInterfaces) - { - // each virtual interface name must be pre-pended with the exclusion symbol ! - var virtualInterfaceNames = config.VirtualInterfaceNames.Split(',').Select(p => "!" + p).ToArray(); - if (lanAddresses.Length > 0) - { - var newList = new string[lanAddresses.Length + virtualInterfaceNames.Length]; - Array.Copy(lanAddresses, newList, lanAddresses.Length); - Array.Copy(virtualInterfaceNames, 0, newList, lanAddresses.Length, virtualInterfaceNames.Length); - lanAddresses = newList; - } - else - { - lanAddresses = virtualInterfaceNames; - } - } - - // Read and parse bind addresses and exclusions, removing ones that don't exist. - _bindAddresses = CreateIPCollection(lanAddresses).ThatAreContainedInNetworks(_interfaceAddresses); - _bindExclusions = CreateIPCollection(lanAddresses, true).ThatAreContainedInNetworks(_interfaceAddresses); - _logger.LogInformation("Using bind addresses: {0}", _bindAddresses.AsString()); - _logger.LogInformation("Using bind exclusions: {0}", _bindExclusions.AsString()); - } - } - - /// - /// Initialises the remote address values. - /// - private void InitialiseRemote(NetworkConfiguration config) - { - lock (_intLock) - { - RemoteAddressFilter = CreateIPCollection(config.RemoteIPFilter); - } - } - - /// - /// Initialises internal LAN cache settings. - /// - private void InitialiseLAN(NetworkConfiguration config) - { - lock (_intLock) - { - _logger.LogDebug("Refreshing LAN information."); - - // Get configuration options. - string[] subnets = config.LocalNetworkSubnets; - - // Create lists from user settings. - - _lanSubnets = CreateIPCollection(subnets); - _excludedSubnets = CreateIPCollection(subnets, true).AsNetworks(); - - // If no LAN addresses are specified - all private subnets are deemed to be the LAN - _usingPrivateAddresses = _lanSubnets.Count == 0; - - // NOTE: The order of the commands generating the collection in this statement matters. - // Altering the order will cause the collections to be created incorrectly. - if (_usingPrivateAddresses) - { - _logger.LogDebug("Using LAN interface addresses as user provided no LAN details."); - // Internal interfaces must be private and not excluded. - _internalInterfaces = CreateCollection(_interfaceAddresses.Where(i => IsPrivateAddressRange(i) && !_excludedSubnets.ContainsAddress(i))); - - // Subnets are the same as the calculated internal interface. - _lanSubnets = new Collection(); - - if (IsIP6Enabled) - { - _lanSubnets.AddItem(IPNetAddress.Parse("fc00::/7")); // ULA - _lanSubnets.AddItem(IPNetAddress.Parse("fe80::/10")); // Site local - } - - if (IsIP4Enabled) - { - _lanSubnets.AddItem(IPNetAddress.Parse("10.0.0.0/8")); - _lanSubnets.AddItem(IPNetAddress.Parse("172.16.0.0/12")); - _lanSubnets.AddItem(IPNetAddress.Parse("192.168.0.0/16")); - } - } - else - { - // Internal interfaces must be private, not excluded and part of the LocalNetworkSubnet. - _internalInterfaces = CreateCollection(_interfaceAddresses.Where(IsInLocalNetwork)); - } - - _logger.LogInformation("Defined LAN addresses : {0}", _lanSubnets.AsString()); - _logger.LogInformation("Defined LAN exclusions : {0}", _excludedSubnets.AsString()); - _logger.LogInformation("Using LAN addresses: {0}", _lanSubnets.Exclude(_excludedSubnets, true).AsNetworks().AsString()); + _eventfire = false; } } @@ -1040,12 +209,11 @@ namespace Jellyfin.Networking.Manager /// private void InitialiseInterfaces() { - lock (_intLock) + lock (_initLock) { _logger.LogDebug("Refreshing interfaces."); - _interfaceNames.Clear(); - _interfaceAddresses.Clear(); + _interfaces.Clear(); _macAddresses.Clear(); try @@ -1060,136 +228,800 @@ namespace Jellyfin.Networking.Manager IPInterfaceProperties ipProperties = adapter.GetIPProperties(); PhysicalAddress mac = adapter.GetPhysicalAddress(); - // populate mac list - if (adapter.NetworkInterfaceType != NetworkInterfaceType.Loopback && mac != null && mac != PhysicalAddress.None) + // Populate MAC list + if (adapter.NetworkInterfaceType != NetworkInterfaceType.Loopback && PhysicalAddress.None.Equals(mac)) { _macAddresses.Add(mac); } - // populate interface address list + // Populate interface list foreach (UnicastIPAddressInformation info in ipProperties.UnicastAddresses) { - if (IsIP4Enabled && info.Address.AddressFamily == AddressFamily.InterNetwork) + if (IsIpv4Enabled && info.Address.AddressFamily == AddressFamily.InterNetwork) { - IPNetAddress nw = new IPNetAddress(info.Address, IPObject.MaskToCidr(info.IPv4Mask)) - { - // Keep the number of gateways on this interface, along with its index. - Tag = ipProperties.GetIPv4Properties().Index - }; + var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name); + interfaceObject.Index = ipProperties.GetIPv4Properties().Index; + interfaceObject.Name = adapter.Name.ToLower(CultureInfo.InvariantCulture); - int tag = nw.Tag; - if (ipProperties.GatewayAddresses.Count > 0 && !nw.IsLoopback()) - { - // -ve Tags signify the interface has a gateway. - nw.Tag *= -1; - } - - _interfaceAddresses.AddItem(nw, false); - - // Store interface name so we can use the name in Collections. - _interfaceNames[adapter.Description.ToLower(CultureInfo.InvariantCulture)] = tag; - _interfaceNames["eth" + tag.ToString(CultureInfo.InvariantCulture)] = tag; + _interfaces.Add(interfaceObject); } - else if (IsIP6Enabled && info.Address.AddressFamily == AddressFamily.InterNetworkV6) + else if (IsIpv6Enabled && info.Address.AddressFamily == AddressFamily.InterNetworkV6) { - IPNetAddress nw = new IPNetAddress(info.Address, (byte)info.PrefixLength) - { - // Keep the number of gateways on this interface, along with its index. - Tag = ipProperties.GetIPv6Properties().Index - }; + var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name); + interfaceObject.Index = ipProperties.GetIPv6Properties().Index; + interfaceObject.Name = adapter.Name.ToLower(CultureInfo.InvariantCulture); - int tag = nw.Tag; - if (ipProperties.GatewayAddresses.Count > 0 && !nw.IsLoopback()) - { - // -ve Tags signify the interface has a gateway. - nw.Tag *= -1; - } - - _interfaceAddresses.AddItem(nw, false); - - // Store interface name so we can use the name in Collections. - _interfaceNames[adapter.Description.ToLower(CultureInfo.InvariantCulture)] = tag; - _interfaceNames["eth" + tag.ToString(CultureInfo.InvariantCulture)] = tag; + _interfaces.Add(interfaceObject); } } } #pragma warning disable CA1031 // Do not catch general exception types catch (Exception ex) +#pragma warning restore CA1031 // Do not catch general exception types { // Ignore error, and attempt to continue. _logger.LogError(ex, "Error encountered parsing interfaces."); } -#pragma warning restore CA1031 // Do not catch general exception types } } +#pragma warning disable CA1031 // Do not catch general exception types catch (Exception ex) +#pragma warning restore CA1031 // Do not catch general exception types { - _logger.LogError(ex, "Error in InitialiseInterfaces."); + _logger.LogError(ex, "Error obtaining interfaces."); } - // If for some reason we don't have an interface info, resolve our DNS name. - if (_interfaceAddresses.Count == 0) + // If for some reason we don't have an interface info, resolve the DNS name. + if (_interfaces.Count == 0) { _logger.LogError("No interfaces information available. Resolving DNS name."); - IPHost host = new IPHost(Dns.GetHostName()); - foreach (var a in host.GetAddresses()) + var hostName = Dns.GetHostName(); + if (Uri.CheckHostName(hostName).Equals(UriHostNameType.Dns)) { - _interfaceAddresses.AddItem(a); + try + { + IPHostEntry hip = Dns.GetHostEntry(hostName); + foreach (var address in hip.AddressList) + { + _interfaces.Add(new IPData(address, null)); + } + } + catch (SocketException ex) + { + // Log and then ignore socket errors, as the result value will just be an empty array. + _logger.LogWarning("GetHostEntryAsync failed with {Message}.", ex.Message); + } } - if (_interfaceAddresses.Count == 0) + if (_interfaces.Count == 0) { _logger.LogWarning("No interfaces information available. Using loopback."); } } - if (IsIP4Enabled) + if (IsIpv4Enabled && !IsIpv6Enabled) { - _interfaceAddresses.AddItem(IPNetAddress.IP4Loopback); + _interfaces.Add(new IPData(IPAddress.Loopback, new IPNetwork(IPAddress.Loopback, 8), "lo")); } - if (IsIP6Enabled) + if (!IsIpv4Enabled && IsIpv6Enabled) { - _interfaceAddresses.AddItem(IPNetAddress.IP6Loopback); + _interfaces.Add(new IPData(IPAddress.IPv6Loopback, new IPNetwork(IPAddress.IPv6Loopback, 128), "lo")); } - _logger.LogDebug("Discovered {0} interfaces.", _interfaceAddresses.Count); - _logger.LogDebug("Interfaces addresses : {0}", _interfaceAddresses.AsString()); + _logger.LogDebug("Discovered {0} interfaces.", _interfaces.Count); + _logger.LogDebug("Interfaces addresses : {0}", _interfaces.Select(s => s.Address).ToString()); } } /// - /// Attempts to match the source against a user defined bind interface. + /// Initialises internal LAN cache. + /// + private void InitialiseLan(NetworkConfiguration config) + { + lock (_initLock) + { + _logger.LogDebug("Refreshing LAN information."); + + // Get configuration options + string[] subnets = config.LocalNetworkSubnets; + + _ = TryParseSubnets(subnets, out _lanSubnets, false); + _ = TryParseSubnets(subnets, out _excludedSubnets, true); + + if (_lanSubnets.Count == 0) + { + // If no LAN addresses are specified, all private subnets are deemed to be the LAN + _logger.LogDebug("Using LAN interface addresses as user provided no LAN details."); + + if (IsIpv6Enabled) + { + _lanSubnets.Add(new IPNetwork(IPAddress.Parse("fc00::"), 7)); // ULA + _lanSubnets.Add(new IPNetwork(IPAddress.Parse("fe80::"), 10)); // Site local + } + + if (IsIpv4Enabled) + { + _lanSubnets.Add(new IPNetwork(IPAddress.Parse("10.0.0.0"), 8)); + _lanSubnets.Add(new IPNetwork(IPAddress.Parse("172.16.0.0"), 12)); + _lanSubnets.Add(new IPNetwork(IPAddress.Parse("192.168.0.0"), 16)); + } + } + + _logger.LogInformation("Defined LAN addresses : {0}", _lanSubnets.Select(s => s.Prefix + "/" + s.PrefixLength)); + _logger.LogInformation("Defined LAN exclusions : {0}", _excludedSubnets.Select(s => s.Prefix + "/" + s.PrefixLength)); + _logger.LogInformation("Using LAN addresses: {0}", _lanSubnets.Where(s => !_excludedSubnets.Contains(s)).Select(s => s.Prefix + "/" + s.PrefixLength)); + } + } + + /// + /// Initialises the network bind addresses. + /// + private void InitialiseBind(NetworkConfiguration config) + { + lock (_initLock) + { + // Use explicit bind addresses + if (config.LocalNetworkAddresses.Length > 0) + { + _bindAddresses = config.LocalNetworkAddresses.Select(p => IPAddress.TryParse(p, out var address) + ? address + : (_interfaces.Where(x => x.Name.Equals(p, StringComparison.OrdinalIgnoreCase)).Select(x => x.Address).FirstOrDefault() ?? IPAddress.None)).ToList(); + _bindAddresses.RemoveAll(x => x == IPAddress.None); + } + else + { + // Use all addresses from all interfaces + _bindAddresses = _interfaces.Select(x => x.Address).ToList(); + } + + _bindExclusions = new List(); + + // Add all interfaces matching any virtual machine interface prefix to _bindExclusions + if (config.IgnoreVirtualInterfaces) + { + // Remove potentially exisiting * and split config string into prefixes + var virtualInterfacePrefixes = config.VirtualInterfaceNames.Replace("*", string.Empty, StringComparison.OrdinalIgnoreCase).ToLower().Split(','); + + // Check all interfaces for matches against the prefixes and add the interface IPs to _bindExclusions + if (_bindAddresses.Count > 0 && virtualInterfacePrefixes.Length > 0) + { + var localInterfaces = _interfaces.ToList(); + foreach (var virtualInterfacePrefix in virtualInterfacePrefixes) + { + var excludedInterfaceIps = localInterfaces.Where(intf => intf.Name.StartsWith(virtualInterfacePrefix, StringComparison.OrdinalIgnoreCase)) + .Select(intf => intf.Address); + foreach (var interfaceIp in excludedInterfaceIps) + { + _bindExclusions.Add(interfaceIp); + } + } + } + } + + // Remove all excluded addresses from _bindAddresses + _bindAddresses.RemoveAll(x => _bindExclusions.Contains(x)); + + _logger.LogInformation("Using bind addresses: {0}", _bindAddresses); + _logger.LogInformation("Using bind exclusions: {0}", _bindExclusions); + } + } + + /// + /// Initialises the remote address values. + /// + private void InitialiseRemote(NetworkConfiguration config) + { + lock (_initLock) + { + // Parse config values into filter collection + var remoteIPFilter = config.RemoteIPFilter; + if (remoteIPFilter.Any() && !string.IsNullOrWhiteSpace(remoteIPFilter.First())) + { + // Parse all IPs with netmask to a subnet + _ = TryParseSubnets(remoteIPFilter.Where(x => x.Contains("/", StringComparison.OrdinalIgnoreCase)).ToArray(), out _remoteAddressFilter, false); + + // Parse everything else as an IP and construct subnet with a single IP + var ips = remoteIPFilter.Where(x => !x.Contains("/", StringComparison.OrdinalIgnoreCase)); + foreach (var ip in ips) + { + if (IPAddress.TryParse(ip, out var ipp)) + { + _remoteAddressFilter.Add(new IPNetwork(ipp, ipp.AddressFamily == AddressFamily.InterNetwork ? 32 : 128)); + } + } + } + } + } + + /// + /// Parses the user defined overrides into the dictionary object. + /// Overrides are the equivalent of localised publishedServerUrl, enabling + /// different addresses to be advertised over different subnets. + /// format is subnet=ipaddress|host|uri + /// when subnet = 0.0.0.0, any external address matches. + /// + private void InitialiseOverrides(NetworkConfiguration config) + { + lock (_initLock) + { + _publishedServerUrls.Clear(); + string[] overrides = config.PublishedServerUriBySubnet; + + foreach (var entry in overrides) + { + var parts = entry.Split('='); + if (parts.Length != 2) + { + _logger.LogError("Unable to parse bind override: {Entry}", entry); + } + else + { + var replacement = parts[1].Trim(); + var ipParts = parts[0].Split("/"); + if (string.Equals(parts[0], "all", StringComparison.OrdinalIgnoreCase)) + { + _publishedServerUrls[new IPData(IPAddress.Broadcast, null)] = replacement; + } + else if (string.Equals(parts[0], "external", StringComparison.OrdinalIgnoreCase)) + { + _publishedServerUrls[new IPData(IPAddress.Any, new IPNetwork(IPAddress.Any, 0))] = replacement; + _publishedServerUrls[new IPData(IPAddress.IPv6Any, new IPNetwork(IPAddress.IPv6Any, 0))] = replacement; + } + else if (IPAddress.TryParse(ipParts[0], out IPAddress? result)) + { + var data = new IPData(result, null); + if (ipParts.Length > 1 && int.TryParse(ipParts[1], out var netmask)) + { + data.Subnet = new IPNetwork(result, netmask); + } + + _publishedServerUrls[data] = replacement; + } + else if (TryParseInterface(parts[0], out var ifaces)) + { + foreach (var iface in ifaces) + { + _publishedServerUrls[iface] = replacement; + } + } + else + { + _logger.LogError("Unable to parse bind ip address. {Parts}", parts[1]); + } + } + } + } + } + + private void ConfigurationUpdated(object? sender, ConfigurationUpdateEventArgs evt) + { + if (evt.Key.Equals("network", StringComparison.Ordinal)) + { + UpdateSettings((NetworkConfiguration)evt.NewConfiguration); + } + } + + /// + /// Reloads all settings and re-initialises the instance. + /// + /// The to use. + public void UpdateSettings(object configuration) + { + NetworkConfiguration config = (NetworkConfiguration)configuration ?? throw new ArgumentNullException(nameof(configuration)); + + if (string.IsNullOrEmpty(MockNetworkSettings)) + { + InitialiseInterfaces(); + } + else // Used in testing only. + { + // Format is ,,: . Set index to -ve to simulate a gateway. + var interfaceList = MockNetworkSettings.Split('|'); + foreach (var details in interfaceList) + { + var parts = details.Split(','); + var split = parts[0].Split("/"); + var address = IPAddress.Parse(split[0]); + var network = new IPNetwork(address, int.Parse(split[1], CultureInfo.InvariantCulture)); + var index = int.Parse(parts[1], CultureInfo.InvariantCulture); + if (address.AddressFamily == AddressFamily.InterNetwork) + { + _interfaces.Add(new IPData(address, network, parts[2])); + } + else if (address.AddressFamily == AddressFamily.InterNetworkV6) + { + _interfaces.Add(new IPData(address, network, parts[2])); + } + } + } + + InitialiseLan(config); + InitialiseBind(config); + InitialiseRemote(config); + InitialiseOverrides(config); + } + + /// + /// Protected implementation of Dispose pattern. + /// + /// True to dispose the managed state. + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _configurationManager.NamedConfigurationUpdated -= ConfigurationUpdated; + NetworkChange.NetworkAddressChanged -= OnNetworkAddressChanged; + NetworkChange.NetworkAvailabilityChanged -= OnNetworkAvailabilityChanged; + } + + _disposed = true; + } + } + + /// + public bool TryParseInterface(string intf, out Collection result) + { + result = new Collection(); + if (string.IsNullOrEmpty(intf)) + { + return false; + } + + if (_interfaces != null) + { + // Match all interfaces starting with names starting with token + var matchedInterfaces = _interfaces.Where(s => s.Name.Equals(intf.ToLower(CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase)); + if (matchedInterfaces.Any()) + { + _logger.LogInformation("Interface {Token} used in settings. Using its interface addresses.", intf); + + // Use interface IP instead of name + foreach (IPData iface in matchedInterfaces) + { + if ((IsIpv4Enabled && iface.Address.AddressFamily == AddressFamily.InterNetwork) + || (IsIpv6Enabled && iface.Address.AddressFamily == AddressFamily.InterNetworkV6)) + { + result.Add(iface); + } + } + + return true; + } + } + + return false; + } + + /// + /// Try parsing an array of strings into subnets, respecting exclusions. + /// + /// Input string to be parsed. + /// Collection of . + /// Boolean signaling if negated or not negated values should be parsed. + /// True if parsing was successful. + public bool TryParseSubnets(string[] values, out Collection result, bool negated = false) + { + result = new Collection(); + + if (values == null || values.Length == 0) + { + return false; + } + + for (int a = 0; a < values.Length; a++) + { + string[] v = values[a].Trim().Split("/"); + + try + { + var address = IPAddress.None; + if (negated && v[0].StartsWith('!')) + { + _ = IPAddress.TryParse(v[0][1..], out address); + } + else if (!negated) + { + _ = IPAddress.TryParse(v[0][0..], out address); + } + + if (address != IPAddress.None && address != null) + { + if (int.TryParse(v[1], out var netmask)) + { + result.Add(new IPNetwork(address, netmask)); + } + else if (address.AddressFamily == AddressFamily.InterNetwork) + { + result.Add(new IPNetwork(address, 32)); + } + else if (address.AddressFamily == AddressFamily.InterNetworkV6) + { + result.Add(new IPNetwork(address, 128)); + } + } + } + catch (ArgumentException e) + { + _logger.LogWarning(e, "Ignoring LAN value {Value}.", v); + } + } + + if (result.Count > 0) + { + return true; + } + + return false; + } + + /// + public bool HasRemoteAccess(IPAddress remoteIp) + { + var config = _configurationManager.GetNetworkConfiguration(); + if (config.EnableRemoteAccess) + { + // 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. + if (_remoteAddressFilter.Any() && !_lanSubnets.Any(x => x.Contains(remoteIp))) + { + // remoteAddressFilter is a whitelist or blacklist. + var matches = _remoteAddressFilter.Count(remoteNetwork => remoteNetwork.Contains(remoteIp)); + if ((!config.IsRemoteIPFilterBlacklist && matches > 0) + || (config.IsRemoteIPFilterBlacklist && matches == 0)) + { + return true; + } + + return false; + } + } + else if (!_lanSubnets.Where(x => x.Contains(remoteIp)).Any()) + { + // Remote not enabled. So everyone should be LAN. + return false; + } + + return true; + } + + /// + public IReadOnlyCollection GetMacAddresses() + { + // Populated in construction - so always has values. + return _macAddresses; + } + + /// + public List GetLoopbacks() + { + var loopbackNetworks = new List(); + if (IsIpv4Enabled) + { + loopbackNetworks.Add(new IPData(IPAddress.Loopback, new IPNetwork(IPAddress.Loopback, 8), "lo")); + } + + if (IsIpv6Enabled) + { + loopbackNetworks.Add(new IPData(IPAddress.IPv6Loopback, new IPNetwork(IPAddress.IPv6Loopback, 128), "lo")); + } + + return loopbackNetworks; + } + + /// + public List GetAllBindInterfaces(bool individualInterfaces = false) + { + if (_bindAddresses.Count == 0) + { + if (_bindExclusions.Count > 0) + { + foreach (var exclusion in _bindExclusions) + { + // Return all the interfaces except the ones specifically excluded. + _interfaces.RemoveAll(intf => intf.Address == exclusion); + } + + return _interfaces; + } + + // No bind address and no exclusions, so listen on all interfaces. + var result = new List(); + + if (individualInterfaces) + { + foreach (var iface in _interfaces) + { + result.Add(iface); + } + + return result; + } + + if (IsIpv4Enabled && IsIpv6Enabled) + { + // Kestrel source code shows it uses Sockets.DualMode - so this also covers IPAddress.Any by default + result.Add(new IPData(IPAddress.IPv6Any, new IPNetwork(IPAddress.IPv6Any, 0))); + } + else if (IsIpv4Enabled) + { + result.Add(new IPData(IPAddress.Any, new IPNetwork(IPAddress.Any, 0))); + } + else if (IsIpv6Enabled) + { + // Cannot use IPv6Any as Kestrel will bind to IPv4 addresses too. + foreach (var iface in _interfaces) + { + if (iface.AddressFamily == AddressFamily.InterNetworkV6) + { + result.Add(iface); + } + } + } + + return result; + } + + // Remove any excluded bind interfaces. + foreach (var exclusion in _bindExclusions) + { + // Return all the interfaces except the ones specifically excluded. + _bindAddresses.Remove(exclusion); + } + + return _bindAddresses.Select(s => new IPData(s, null)).ToList(); + } + + /// + public string GetBindInterface(string source, out int? port) + { + _ = NetworkExtensions.TryParseHost(source, out var address, IsIpv4Enabled, IsIpv6Enabled); + var result = GetBindInterface(address.FirstOrDefault(), out port); + return result; + } + + /// + public string GetBindInterface(HttpRequest source, out int? port) + { + string result; + _ = NetworkExtensions.TryParseHost(source.Host.Host, out var addresses, IsIpv4Enabled, IsIpv6Enabled); + result = GetBindInterface(addresses.FirstOrDefault(), out port); + port ??= source.Host.Port; + + return result; + } + + /// + public string GetBindInterface(IPAddress? source, out int? port) + { + port = null; + + string result; + + if (source != null) + { + if (IsIpv4Enabled && source.AddressFamily == AddressFamily.InterNetworkV6) + { + _logger.LogWarning("IPv6 is disabled in Jellyfin, but enabled in the OS. This may affect how the interface is selected."); + } + + if (IsIpv6Enabled && source.AddressFamily == AddressFamily.InterNetwork) + { + _logger.LogWarning("IPv4 is disabled in Jellyfin, but enabled in the OS. This may affect how the interface is selected."); + } + + bool isExternal = !_lanSubnets.Any(network => network.Contains(source)); + _logger.LogDebug("GetBindInterface with source. External: {IsExternal}:", isExternal); + + if (MatchesPublishedServerUrl(source, isExternal, out string res, out port)) + { + _logger.LogInformation("{Source}: Using BindAddress {Address}:{Port}", source, res, port); + return res; + } + + // No preference given, so move on to bind addresses. + if (MatchesBindInterface(source, isExternal, out result)) + { + return result; + } + + if (isExternal && MatchesExternalInterface(source, out result)) + { + return result; + } + } + + // Get the first LAN interface address that's not excluded and not a loopback address. + var availableInterfaces = _interfaces.Where(x => !IPAddress.IsLoopback(x.Address)) + .OrderByDescending(x => _bindAddresses.Contains(x.Address)) + .ThenByDescending(x => IsInLocalNetwork(x.Address)) + .ThenBy(x => x.Index); + + if (availableInterfaces.Any()) + { + if (source != null) + { + foreach (var intf in availableInterfaces) + { + if (intf.Address.Equals(source)) + { + result = NetworkExtensions.FormatIpString(intf.Address); + _logger.LogDebug("{Source}: GetBindInterface: Has found matching interface. {Result}", source, result); + return result; + } + } + + // Does the request originate in one of the interface subnets? + // (For systems with multiple internal network cards, and multiple subnets) + foreach (var intf in availableInterfaces) + { + if (intf.Subnet.Contains(source)) + { + result = NetworkExtensions.FormatIpString(intf.Address); + _logger.LogDebug("{Source}: GetBindInterface: Has source, matched best internal interface on range. {Result}", source, result); + return result; + } + } + } + + result = NetworkExtensions.FormatIpString(availableInterfaces.First().Address); + _logger.LogDebug("{Source}: GetBindInterface: Matched first internal interface. {Result}", source, result); + return result; + } + + // There isn't any others, so we'll use the loopback. + result = IsIpv4Enabled && !IsIpv6Enabled ? "127.0.0.1" : "::1"; + _logger.LogWarning("{Source}: GetBindInterface: Loopback {Result} returned.", source, result); + return result; + } + + /// + public List GetInternalBindAddresses() + { + if (_bindAddresses.Count == 0) + { + if (_bindExclusions.Count > 0) + { + // Return all the internal interfaces except the ones excluded. + return _interfaces.Where(p => !_bindExclusions.Contains(p.Address)).ToList(); + } + + // No bind address, so return all internal interfaces. + return _interfaces; + } + + // Select all local bind addresses + return _interfaces.Where(x => _bindAddresses.Contains(x.Address)) + .Where(x => IsInLocalNetwork(x.Address)) + .OrderBy(x => x.Index).ToList(); + } + + /// + public bool IsInLocalNetwork(string address) + { + if (IPAddress.TryParse(address, out var ep)) + { + return IPAddress.IsLoopback(ep) || (_lanSubnets.Any(x => x.Contains(ep)) && !_excludedSubnets.Any(x => x.Contains(ep))); + } + + if (NetworkExtensions.TryParseHost(address, out var addresses, IsIpv4Enabled, IsIpv6Enabled)) + { + bool match = false; + foreach (var ept in addresses) + { + match |= IPAddress.IsLoopback(ept) || (_lanSubnets.Any(x => x.Contains(ept)) && !_excludedSubnets.Any(x => x.Contains(ept))); + } + + return match; + } + + return false; + } + + /// + public bool IsInLocalNetwork(IPAddress address) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + // See conversation at https://github.com/jellyfin/jellyfin/pull/3515. + if (TrustAllIpv6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6) + { + return true; + } + + // As private addresses can be redefined by Configuration.LocalNetworkAddresses + var match = CheckIfLanAndNotExcluded(address); + + return address.Equals(IPAddress.Loopback) || address.Equals(IPAddress.IPv6Loopback) || match; + } + + private IPData? FindInterfaceForIp(IPAddress address, bool localNetwork = false) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + var interfaces = _interfaces; + + if (localNetwork) + { + interfaces = interfaces.Where(x => IsInLocalNetwork(x.Address)).ToList(); + } + + foreach (var intf in _interfaces) + { + if (intf.Subnet.Contains(address)) + { + return intf; + } + } + + return null; + } + + private bool CheckIfLanAndNotExcluded(IPAddress address) + { + bool match = false; + foreach (var lanSubnet in _lanSubnets) + { + match |= lanSubnet.Contains(address); + } + + foreach (var excludedSubnet in _excludedSubnets) + { + match &= !excludedSubnet.Contains(address); + } + + NetworkExtensions.IsIPv6LinkLocal(address); + return match; + } + + /// + /// Attempts to match the source against the published server URL overrides. /// /// IP source address to use. - /// True if the source is in the external subnet. - /// The published server url that matches the source address. + /// True if the source is in an external subnet. + /// The published server URL that matches the source address. /// The resultant port, if one exists. /// true if a match is found, false otherwise. - private bool MatchesPublishedServerUrl(IPObject source, bool isInExternalSubnet, out string bindPreference, out int? port) + private bool MatchesPublishedServerUrl(IPAddress source, bool isInExternalSubnet, out string bindPreference, out int? port) { bindPreference = string.Empty; port = null; + var validPublishedServerUrls = _publishedServerUrls.Where(x => x.Key.Address.Equals(IPAddress.Any)).ToList(); + validPublishedServerUrls.AddRange(_publishedServerUrls.Where(x => x.Key.Address.Equals(IPAddress.IPv6Any))); + validPublishedServerUrls.AddRange(_publishedServerUrls.Where(x => x.Key.Subnet.Contains(source))); + validPublishedServerUrls.Distinct(); + // Check for user override. - foreach (var addr in _publishedServerUrls) + foreach (var data in validPublishedServerUrls) { + // Get address interface + var intf = _interfaces.FirstOrDefault(s => s.Subnet.Contains(data.Key.Address)); + // Remaining. Match anything. - if (addr.Key.Address.Equals(IPAddress.Broadcast)) + if (data.Key.Address.Equals(IPAddress.Broadcast)) { - bindPreference = addr.Value; + bindPreference = data.Value; break; } - else if ((addr.Key.Address.Equals(IPAddress.Any) || addr.Key.Address.Equals(IPAddress.IPv6Any)) && isInExternalSubnet) + else if ((data.Key.Address.Equals(IPAddress.Any) || data.Key.Address.Equals(IPAddress.IPv6Any)) && isInExternalSubnet) { // External. - bindPreference = addr.Value; + bindPreference = data.Value; break; } - else if (addr.Key.Contains(source)) + else if (intf?.Address != null) { // Match ip address. - bindPreference = addr.Value; + bindPreference = data.Value; break; } } @@ -1220,12 +1052,11 @@ namespace Jellyfin.Networking.Manager /// True if the source is in the external subnet. /// The result, if a match is found. /// true if a match is found, false otherwise. - private bool MatchesBindInterface(IPObject source, bool isInExternalSubnet, out string result) + private bool MatchesBindInterface(IPAddress source, bool isInExternalSubnet, out string result) { result = string.Empty; - var addresses = _bindAddresses.Exclude(_bindExclusions, false); - int count = addresses.Count; + int count = _bindAddresses.Count; if (count == 1 && (_bindAddresses[0].Equals(IPAddress.Any) || _bindAddresses[0].Equals(IPAddress.IPv6Any))) { // Ignore IPAny addresses. @@ -1234,24 +1065,25 @@ namespace Jellyfin.Networking.Manager if (count != 0) { - // Check to see if any of the bind interfaces are in the same subnet. - + // Check to see if any of the bind interfaces are in the same subnet as the source. IPAddress? defaultGateway = null; IPAddress? bindAddress = null; if (isInExternalSubnet) { // Find all external bind addresses. Store the default gateway, but check to see if there is a better match first. - foreach (var addr in addresses.OrderBy(p => p.Tag)) + foreach (var addr in _bindAddresses) { if (defaultGateway == null && !IsInLocalNetwork(addr)) { - defaultGateway = addr.Address; + defaultGateway = addr; } - if (bindAddress == null && addr.Contains(source)) + var intf = _interfaces.Where(x => x.Subnet.Contains(addr)).FirstOrDefault(); + + if (bindAddress == null && intf != null && intf.Subnet.Contains(source)) { - bindAddress = addr.Address; + bindAddress = intf.Address; } if (defaultGateway != null && bindAddress != null) @@ -1263,32 +1095,37 @@ namespace Jellyfin.Networking.Manager else { // Look for the best internal address. - bindAddress = addresses - .Where(p => IsInLocalNetwork(p) && (p.Contains(source) || p.Equals(IPAddress.None))) - .OrderBy(p => p.Tag) - .FirstOrDefault()?.Address; + foreach (var bA in _bindAddresses.Where(x => IsInLocalNetwork(x))) + { + var intf = FindInterfaceForIp(source, true); + if (intf != null) + { + bindAddress = intf.Address; + break; + } + } } if (bindAddress != null) { - result = FormatIP6String(bindAddress); - _logger.LogDebug("{Source}: GetBindInterface: Has source, found a match bind interface subnets. {Result}", source, result); + result = NetworkExtensions.FormatIpString(bindAddress); + _logger.LogDebug("{Source}: GetBindInterface: Has source, found a matching bind interface subnet. {Result}", source, result); return true; } if (isInExternalSubnet && defaultGateway != null) { - result = FormatIP6String(defaultGateway); + result = NetworkExtensions.FormatIpString(defaultGateway); _logger.LogDebug("{Source}: GetBindInterface: Using first user defined external interface. {Result}", source, result); return true; } - result = FormatIP6String(addresses[0].Address); + result = NetworkExtensions.FormatIpString(_bindAddresses[0]); _logger.LogDebug("{Source}: GetBindInterface: Selected first user defined interface. {Result}", source, result); if (isInExternalSubnet) { - _logger.LogWarning("{Source}: External request received, however, only an internal interface bind found.", source); + _logger.LogWarning("{Source}: External request received, only an internal interface bind found.", source); } return true; @@ -1303,30 +1140,29 @@ namespace Jellyfin.Networking.Manager /// IP source address to use. /// The result, if a match is found. /// true if a match is found, false otherwise. - private bool MatchesExternalInterface(IPObject source, out string result) + private bool MatchesExternalInterface(IPAddress source, out string result) { result = string.Empty; // Get the first WAN interface address that isn't a loopback. - var extResult = _interfaceAddresses - .Exclude(_bindExclusions, false) - .Where(p => !IsInLocalNetwork(p)) - .OrderBy(p => p.Tag); + var extResult = _interfaces.Where(p => !IsInLocalNetwork(p.Address)); - if (extResult.Any()) + IPAddress? hasResult = null; + // Does the request originate in one of the interface subnets? + // (For systems with multiple internal network cards, and multiple subnets) + foreach (var intf in extResult) { - // Does the request originate in one of the interface subnets? - // (For systems with multiple internal network cards, and multiple subnets) - foreach (var intf in extResult) + hasResult ??= intf.Address; + if (!IsInLocalNetwork(intf.Address) && intf.Subnet.Contains(source)) { - if (!IsInLocalNetwork(intf) && intf.Contains(source)) - { - result = FormatIP6String(intf.Address); - _logger.LogDebug("{Source}: GetBindInterface: Selected best external on interface on range. {Result}", source, result); - return true; - } + result = NetworkExtensions.FormatIpString(intf.Address); + _logger.LogDebug("{Source}: GetBindInterface: Selected best external on interface on range. {Result}", source, result); + return true; } + } - result = FormatIP6String(extResult.First().Address); + if (hasResult != null) + { + result = NetworkExtensions.FormatIpString(hasResult); _logger.LogDebug("{Source}: GetBindInterface: Selected first external interface. {Result}", source, result); return true; } diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 66fa3bc31b..7030b726cd 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -344,13 +344,13 @@ namespace Jellyfin.Server.Extensions { for (var i = 0; i < allowedProxies.Length; i++) { - if (IPNetAddress.TryParse(allowedProxies[i], out var addr)) + if (IPAddress.TryParse(allowedProxies[i], out var addr)) { - AddIpAddress(config, options, addr.Address, addr.PrefixLength); + AddIpAddress(config, options, addr, addr.AddressFamily == AddressFamily.InterNetwork ? 32 : 128); } - else if (IPHost.TryParse(allowedProxies[i], out var host)) + else if (NetworkExtensions.TryParseHost(allowedProxies[i], out var host)) { - foreach (var address in host.GetAddresses()) + foreach (var address in host) { AddIpAddress(config, options, address, address.AddressFamily == AddressFamily.InterNetwork ? 32 : 128); } diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs index 5e601ca847..ceeaa26e62 100644 --- a/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs +++ b/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs @@ -114,9 +114,7 @@ public class CreateNetworkConfiguration : IMigrationRoutine public bool IgnoreVirtualInterfaces { get; set; } = true; - public string VirtualInterfaceNames { get; set; } = "vEthernet*"; - - public bool TrustAllIP6Interfaces { get; set; } + public string VirtualInterfaceNames { get; set; } = "veth*"; public string[] PublishedServerUriBySubnet { get; set; } = Array.Empty(); diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 2bda8d2905..63055a61d4 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -300,7 +300,7 @@ namespace Jellyfin.Server var addresses = appHost.NetManager.GetAllBindInterfaces(); bool flagged = false; - foreach (IPObject netAdd in addresses) + foreach (IPData netAdd in addresses) { _logger.LogInformation("Kestrel listening on {Address}", netAdd.Address == IPAddress.IPv6Any ? "All Addresses" : netAdd); options.Listen(netAdd.Address, appHost.HttpPort); @@ -689,10 +689,10 @@ namespace Jellyfin.Server if (!string.IsNullOrEmpty(socketPerms)) { - #pragma warning disable SA1300 // Entrypoint is case sensitive. +#pragma warning disable SA1300 // Entrypoint is case sensitive. [DllImport("libc")] static extern int chmod(string pathname, int mode); - #pragma warning restore SA1300 +#pragma warning restore SA1300 var exitCode = chmod(socketPath, Convert.ToInt32(socketPerms, 8)); diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs index b939397301..fdf42bdbce 100644 --- a/MediaBrowser.Common/Net/INetworkManager.cs +++ b/MediaBrowser.Common/Net/INetworkManager.cs @@ -18,44 +18,29 @@ namespace MediaBrowser.Common.Net event EventHandler NetworkChanged; /// - /// Gets the published server urls list. + /// Gets a value indicating whether iP6 is enabled. /// - Dictionary PublishedServerUrls { get; } + bool IsIpv6Enabled { get; } /// - /// Gets a value indicating whether is all IPv6 interfaces are trusted as internal. + /// Gets a value indicating whether iP4 is enabled. /// - bool TrustAllIP6Interfaces { get; } - - /// - /// Gets the remote address filter. - /// - Collection RemoteAddressFilter { get; } - - /// - /// Gets or sets a value indicating whether iP6 is enabled. - /// - bool IsIP6Enabled { get; set; } - - /// - /// Gets or sets a value indicating whether iP4 is enabled. - /// - bool IsIP4Enabled { get; set; } + bool IsIpv4Enabled { get; } /// /// Calculates the list of interfaces to use for Kestrel. /// - /// A Collection{IPObject} object containing all the interfaces to bind. + /// A List{IPData} object containing all the interfaces to bind. /// If all the interfaces are specified, and none are excluded, it returns zero items /// to represent any address. /// When false, return or for all interfaces. - Collection GetAllBindInterfaces(bool individualInterfaces = false); + List GetAllBindInterfaces(bool individualInterfaces = false); /// /// Returns a collection containing the loopback interfaces. /// - /// Collection{IPObject}. - Collection GetLoopbacks(); + /// List{IPData}. + List GetLoopbacks(); /// /// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo) @@ -86,22 +71,12 @@ namespace MediaBrowser.Common.Net /// Source of the request. /// Optional port returned, if it's part of an override. /// IP Address to use, or loopback address if all else fails. - string GetBindInterface(IPObject source, out int? port); - - /// - /// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo) - /// If no bind addresses are specified, an internal interface address is selected. - /// (See . - /// - /// Source of the request. - /// Optional port returned, if it's part of an override. - /// IP Address to use, or loopback address if all else fails. string GetBindInterface(HttpRequest source, out int? port); /// /// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo) /// If no bind addresses are specified, an internal interface address is selected. - /// (See . + /// (See . /// /// IP address of the request. /// Optional port returned, if it's part of an override. @@ -111,48 +86,19 @@ namespace MediaBrowser.Common.Net /// /// 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 . /// /// Source of the request. /// Optional port returned, if it's part of an override. /// IP Address to use, or loopback address if all else fails. string GetBindInterface(string source, out int? port); - /// - /// Checks to see if the ip address is specifically excluded in LocalNetworkAddresses. - /// - /// IP address to check. - /// True if it is. - bool IsExcludedInterface(IPAddress address); - /// /// Get a list of all the MAC addresses associated with active interfaces. /// /// List of MAC addresses. IReadOnlyCollection GetMacAddresses(); - /// - /// Checks to see if the IP Address provided matches an interface that has a gateway. - /// - /// IP to check. Can be an IPAddress or an IPObject. - /// Result of the check. - bool IsGatewayInterface(IPObject? addressObj); - - /// - /// Checks to see if the IP Address provided matches an interface that has a gateway. - /// - /// IP to check. Can be an IPAddress or an IPObject. - /// Result of the check. - bool IsGatewayInterface(IPAddress? addressObj); - - /// - /// Returns true if the address is a private address. - /// The configuration option TrustIP6Interfaces overrides this functions behaviour. - /// - /// Address to check. - /// True or False. - bool IsPrivateAddressRange(IPObject address); - /// /// Returns true if the address is part of the user defined LAN. /// The configuration option TrustIP6Interfaces overrides this functions behaviour. @@ -161,14 +107,6 @@ namespace MediaBrowser.Common.Net /// True if endpoint is within the LAN range. bool IsInLocalNetwork(string address); - /// - /// Returns true if the address is part of the user defined LAN. - /// The configuration option TrustIP6Interfaces overrides this functions behaviour. - /// - /// IP to check. - /// True if endpoint is within the LAN range. - bool IsInLocalNetwork(IPObject address); - /// /// Returns true if the address is part of the user defined LAN. /// The configuration option TrustIP6Interfaces overrides this functions behaviour. @@ -178,55 +116,19 @@ namespace MediaBrowser.Common.Net bool IsInLocalNetwork(IPAddress address); /// - /// Attempts to convert the token to an IP address, permitting for interface descriptions and indexes. - /// eg. "eth1", or "TP-LINK Wireless USB Adapter". + /// Attempts to convert the interface name to an IP address. + /// eg. "eth1", or "enp3s5". /// - /// Token to parse. + /// Interface name. /// Resultant object's ip addresses, if successful. /// Success of the operation. - bool TryParseInterface(string token, out Collection? result); - - /// - /// Parses an array of strings into a Collection{IPObject}. - /// - /// Values to parse. - /// When true, only include values beginning with !. When false, ignore ! values. - /// IPCollection object containing the value strings. - Collection CreateIPCollection(string[] values, bool negated = false); + bool TryParseInterface(string intf, out Collection? result); /// /// Returns all the internal Bind interface addresses. /// /// An internal list of interfaces addresses. - Collection GetInternalBindAddresses(); - - /// - /// Checks to see if an IP address is still a valid interface address. - /// - /// IP address to check. - /// True if it is. - bool IsValidInterfaceAddress(IPAddress address); - - /// - /// Returns true if the IP address is in the excluded list. - /// - /// IP to check. - /// True if excluded. - bool IsExcluded(IPAddress ip); - - /// - /// Returns true if the IP address is in the excluded list. - /// - /// IP to check. - /// True if excluded. - bool IsExcluded(EndPoint ip); - - /// - /// Gets the filtered LAN ip addresses. - /// - /// Optional filter for the list. - /// Returns a filtered list of LAN addresses. - Collection GetFilteredLANSubnets(Collection? filter = null); + List GetInternalBindAddresses(); /// /// Checks to see if has access. diff --git a/MediaBrowser.Common/Net/IPData.cs b/MediaBrowser.Common/Net/IPData.cs new file mode 100644 index 0000000000..3c6adc7e86 --- /dev/null +++ b/MediaBrowser.Common/Net/IPData.cs @@ -0,0 +1,76 @@ +using System.Net; +using System.Net.Sockets; +using Microsoft.AspNetCore.HttpOverrides; + +namespace MediaBrowser.Common.Net +{ + /// + /// Base network object class. + /// + public class IPData + { + /// + /// Initializes a new instance of the class. + /// + /// An . + /// The . + public IPData( + IPAddress address, + IPNetwork? subnet) + { + Address = address; + Subnet = subnet ?? (address.AddressFamily == AddressFamily.InterNetwork ? new IPNetwork(address, 32) : new IPNetwork(address, 128)); + Name = string.Empty; + } + + /// + /// Initializes a new instance of the class. + /// + /// An . + /// The . + /// The object's name. + public IPData( + IPAddress address, + IPNetwork? subnet, + string name) + { + Address = address; + Subnet = subnet ?? (address.AddressFamily == AddressFamily.InterNetwork ? new IPNetwork(address, 32) : new IPNetwork(address, 128)); + Name = name; + } + + /// + /// Gets or sets the object's IP address. + /// + public IPAddress Address { get; set; } + + /// + /// Gets or sets the object's IP address. + /// + public IPNetwork Subnet { get; set; } + + /// + /// Gets or sets the interface index. + /// + public int Index { get; set; } + + /// + /// Gets or sets the interface name. + /// + public string Name { get; set; } + + /// + /// Gets the AddressFamily of this object. + /// + public AddressFamily AddressFamily + { + get + { + return Address.Equals(IPAddress.None) + ? (Subnet.Prefix.AddressFamily.Equals(IPAddress.None) + ? AddressFamily.Unspecified : Subnet.Prefix.AddressFamily) + : Address.AddressFamily; + } + } + } +} diff --git a/MediaBrowser.Common/Net/IPHost.cs b/MediaBrowser.Common/Net/IPHost.cs deleted file mode 100644 index 1f125f2b1d..0000000000 --- a/MediaBrowser.Common/Net/IPHost.cs +++ /dev/null @@ -1,441 +0,0 @@ -using System; -using System.Diagnostics; -using System.Linq; -using System.Net; -using System.Net.Sockets; -using System.Text.RegularExpressions; - -namespace MediaBrowser.Common.Net -{ - /// - /// Object that holds a host name. - /// - public class IPHost : IPObject - { - /// - /// Gets or sets timeout value before resolve required, in minutes. - /// - public const int Timeout = 30; - - /// - /// Represents an IPHost that has no value. - /// - public static readonly IPHost None = new IPHost(string.Empty, IPAddress.None); - - /// - /// Time when last resolved in ticks. - /// - private DateTime? _lastResolved = null; - - /// - /// Gets the IP Addresses, attempting to resolve the name, if there are none. - /// - private IPAddress[] _addresses; - - /// - /// Initializes a new instance of the class. - /// - /// Host name to assign. - public IPHost(string name) - { - HostName = name ?? throw new ArgumentNullException(nameof(name)); - _addresses = Array.Empty(); - Resolved = false; - } - - /// - /// Initializes a new instance of the class. - /// - /// Host name to assign. - /// Address to assign. - private IPHost(string name, IPAddress address) - { - HostName = name ?? throw new ArgumentNullException(nameof(name)); - _addresses = new IPAddress[] { address ?? throw new ArgumentNullException(nameof(address)) }; - Resolved = !address.Equals(IPAddress.None); - } - - /// - /// Gets or sets the object's first IP address. - /// - public override IPAddress Address - { - get - { - return ResolveHost() ? this[0] : IPAddress.None; - } - - set - { - // Not implemented, as a host's address is determined by DNS. - throw new NotImplementedException("The address of a host is determined by DNS."); - } - } - - /// - /// Gets or sets the object's first IP's subnet prefix. - /// The setter does nothing, but shouldn't raise an exception. - /// - public override byte PrefixLength - { - get => (byte)(ResolveHost() ? 128 : 32); - - // Not implemented, as a host object can only have a prefix length of 128 (IPv6) or 32 (IPv4) prefix length, - // which is automatically determined by it's IP type. Anything else is meaningless. - set => throw new NotImplementedException(); - } - - /// - /// Gets a value indicating whether the address has a value. - /// - public bool HasAddress => _addresses.Length != 0; - - /// - /// Gets the host name of this object. - /// - public string HostName { get; } - - /// - /// Gets a value indicating whether this host has attempted to be resolved. - /// - public bool Resolved { get; private set; } - - /// - /// Gets or sets the IP Addresses associated with this object. - /// - /// Index of address. - public IPAddress this[int index] - { - get - { - ResolveHost(); - return index >= 0 && index < _addresses.Length ? _addresses[index] : IPAddress.None; - } - } - - /// - /// Attempts to parse the host string. - /// - /// Host name to parse. - /// Object representing the string, if it has successfully been parsed. - /// true if the parsing is successful, false if not. - public static bool TryParse(string host, out IPHost hostObj) - { - if (string.IsNullOrWhiteSpace(host)) - { - hostObj = IPHost.None; - return false; - } - - // See if it's an IPv6 with port address e.g. [::1] or [::1]:120. - int i = host.IndexOf(']', StringComparison.Ordinal); - if (i != -1) - { - return TryParse(host.Remove(i - 1).TrimStart(' ', '['), out hostObj); - } - - if (IPNetAddress.TryParse(host, out var netAddress)) - { - // Host name is an ip address, so fake resolve. - hostObj = new IPHost(host, netAddress.Address); - return true; - } - - // Is it a host, IPv4/6 with/out port? - string[] hosts = host.Split(':'); - - if (hosts.Length <= 2) - { - // This is either a hostname: port, or an IP4:port. - host = hosts[0]; - - if (string.Equals("localhost", host, StringComparison.OrdinalIgnoreCase)) - { - hostObj = new IPHost(host); - return true; - } - - if (IPAddress.TryParse(host, out var netIP)) - { - // Host name is an ip address, so fake resolve. - hostObj = new IPHost(host, netIP); - return true; - } - } - else - { - // Invalid host name, as it cannot contain : - hostObj = new IPHost(string.Empty, IPAddress.None); - return false; - } - - // Use regular expression as CheckHostName isn't RFC5892 compliant. - // Modified from gSkinner's expression at https://stackoverflow.com/questions/11809631/fully-qualified-domain-name-validation - string pattern = @"(?im)^(?!:\/\/)(?=.{1,255}$)((.{1,63}\.){0,127}(?![0-9]*$)[a-z0-9-]+\.?)$"; - - if (Regex.IsMatch(host, pattern)) - { - hostObj = new IPHost(host); - return true; - } - - hostObj = IPHost.None; - return false; - } - - /// - /// Attempts to parse the host string. - /// - /// Host name to parse. - /// Object representing the string, if it has successfully been parsed. - public static IPHost Parse(string host) - { - if (!string.IsNullOrEmpty(host) && IPHost.TryParse(host, out IPHost res)) - { - return res; - } - - throw new InvalidCastException($"Host does not contain a valid value. {host}"); - } - - /// - /// Attempts to parse the host string, ensuring that it resolves only to a specific IP type. - /// - /// Host name to parse. - /// Addressfamily filter. - /// Object representing the string, if it has successfully been parsed. - public static IPHost Parse(string host, AddressFamily family) - { - if (!string.IsNullOrEmpty(host) && IPHost.TryParse(host, out IPHost res)) - { - if (family == AddressFamily.InterNetwork) - { - res.Remove(AddressFamily.InterNetworkV6); - } - else - { - res.Remove(AddressFamily.InterNetwork); - } - - return res; - } - - throw new InvalidCastException($"Host does not contain a valid value. {host}"); - } - - /// - /// Returns the Addresses that this item resolved to. - /// - /// IPAddress Array. - public IPAddress[] GetAddresses() - { - ResolveHost(); - return _addresses; - } - - /// - public override bool Contains(IPAddress address) - { - if (address != null && !Address.Equals(IPAddress.None)) - { - if (address.IsIPv4MappedToIPv6) - { - address = address.MapToIPv4(); - } - - foreach (var addr in GetAddresses()) - { - if (address.Equals(addr)) - { - return true; - } - } - } - - return false; - } - - /// - public override bool Equals(IPObject? other) - { - if (other is IPHost otherObj) - { - // Do we have the name Hostname? - if (string.Equals(otherObj.HostName, HostName, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - if (!ResolveHost() || !otherObj.ResolveHost()) - { - return false; - } - - // Do any of our IP addresses match? - foreach (IPAddress addr in _addresses) - { - foreach (IPAddress otherAddress in otherObj._addresses) - { - if (addr.Equals(otherAddress)) - { - return true; - } - } - } - } - - return false; - } - - /// - public override bool IsIP6() - { - // Returns true if interfaces are only IP6. - if (ResolveHost()) - { - foreach (IPAddress i in _addresses) - { - if (i.AddressFamily != AddressFamily.InterNetworkV6) - { - return false; - } - } - - return true; - } - - return false; - } - - /// - public override string ToString() - { - // StringBuilder not optimum here. - string output = string.Empty; - if (_addresses.Length > 0) - { - bool moreThanOne = _addresses.Length > 1; - if (moreThanOne) - { - output = "["; - } - - foreach (var i in _addresses) - { - if (Address.Equals(IPAddress.None) && Address.AddressFamily == AddressFamily.Unspecified) - { - output += HostName + ","; - } - else if (i.Equals(IPAddress.Any)) - { - output += "Any IP4 Address,"; - } - else if (Address.Equals(IPAddress.IPv6Any)) - { - output += "Any IP6 Address,"; - } - else if (i.Equals(IPAddress.Broadcast)) - { - output += "Any Address,"; - } - else if (i.AddressFamily == AddressFamily.InterNetwork) - { - output += $"{i}/32,"; - } - else - { - output += $"{i}/128,"; - } - } - - output = output[..^1]; - - if (moreThanOne) - { - output += "]"; - } - } - else - { - output = HostName; - } - - return output; - } - - /// - public override void Remove(AddressFamily family) - { - if (ResolveHost()) - { - _addresses = _addresses.Where(p => p.AddressFamily != family).ToArray(); - } - } - - /// - public override bool Contains(IPObject address) - { - // An IPHost cannot contain another IPObject, it can only be equal. - return Equals(address); - } - - /// - protected override IPObject CalculateNetworkAddress() - { - var (address, prefixLength) = NetworkAddressOf(this[0], PrefixLength); - return new IPNetAddress(address, prefixLength); - } - - /// - /// Attempt to resolve the ip address of a host. - /// - /// true if any addresses have been resolved, otherwise false. - private bool ResolveHost() - { - // When was the last time we resolved? - _lastResolved ??= DateTime.UtcNow; - - // If we haven't resolved before, or our timer has run out... - if ((_addresses.Length == 0 && !Resolved) || (DateTime.UtcNow > _lastResolved.Value.AddMinutes(Timeout))) - { - _lastResolved = DateTime.UtcNow; - ResolveHostInternal(); - Resolved = true; - } - - return _addresses.Length > 0; - } - - /// - /// Task that looks up a Host name and returns its IP addresses. - /// - private void ResolveHostInternal() - { - var hostName = HostName; - if (string.IsNullOrEmpty(hostName)) - { - return; - } - - // Resolves the host name - so save a DNS lookup. - if (string.Equals(hostName, "localhost", StringComparison.OrdinalIgnoreCase)) - { - _addresses = new IPAddress[] { IPAddress.Loopback, IPAddress.IPv6Loopback }; - return; - } - - if (Uri.CheckHostName(hostName) == UriHostNameType.Dns) - { - try - { - _addresses = Dns.GetHostEntry(hostName).AddressList; - } - catch (SocketException ex) - { - // Log and then ignore socket errors, as the result value will just be an empty array. - Debug.WriteLine("GetHostAddresses failed with {Message}.", ex.Message); - } - } - } - } -} diff --git a/MediaBrowser.Common/Net/IPNetAddress.cs b/MediaBrowser.Common/Net/IPNetAddress.cs deleted file mode 100644 index f1428d4bef..0000000000 --- a/MediaBrowser.Common/Net/IPNetAddress.cs +++ /dev/null @@ -1,276 +0,0 @@ -using System; -using System.Net; -using System.Net.Sockets; - -namespace MediaBrowser.Common.Net -{ - /// - /// An object that holds and IP address and subnet mask. - /// - public class IPNetAddress : IPObject - { - /// - /// Represents an IPNetAddress that has no value. - /// - public static readonly IPNetAddress None = new IPNetAddress(IPAddress.None); - - /// - /// IPv4 multicast address. - /// - public static readonly IPAddress SSDPMulticastIPv4 = IPAddress.Parse("239.255.255.250"); - - /// - /// IPv6 local link multicast address. - /// - public static readonly IPAddress SSDPMulticastIPv6LinkLocal = IPAddress.Parse("ff02::C"); - - /// - /// IPv6 site local multicast address. - /// - public static readonly IPAddress SSDPMulticastIPv6SiteLocal = IPAddress.Parse("ff05::C"); - - /// - /// IP4Loopback address host. - /// - public static readonly IPNetAddress IP4Loopback = IPNetAddress.Parse("127.0.0.1/8"); - - /// - /// IP6Loopback address host. - /// - public static readonly IPNetAddress IP6Loopback = new IPNetAddress(IPAddress.IPv6Loopback); - - /// - /// Object's IP address. - /// - private IPAddress _address; - - /// - /// Initializes a new instance of the class. - /// - /// Address to assign. - public IPNetAddress(IPAddress address) - { - _address = address ?? throw new ArgumentNullException(nameof(address)); - PrefixLength = (byte)(address.AddressFamily == AddressFamily.InterNetwork ? 32 : 128); - } - - /// - /// Initializes a new instance of the class. - /// - /// IP Address. - /// Mask as a CIDR. - public IPNetAddress(IPAddress address, byte prefixLength) - { - if (address?.IsIPv4MappedToIPv6 ?? throw new ArgumentNullException(nameof(address))) - { - _address = address.MapToIPv4(); - } - else - { - _address = address; - } - - PrefixLength = prefixLength; - } - - /// - /// Gets or sets the object's IP address. - /// - public override IPAddress Address - { - get - { - return _address; - } - - set - { - _address = value ?? IPAddress.None; - } - } - - /// - public override byte PrefixLength { get; set; } - - /// - /// Try to parse the address and subnet strings into an IPNetAddress object. - /// - /// IP address to parse. Can be CIDR or X.X.X.X notation. - /// Resultant object. - /// True if the values parsed successfully. False if not, resulting in the IP being null. - public static bool TryParse(string addr, out IPNetAddress ip) - { - if (!string.IsNullOrEmpty(addr)) - { - addr = addr.Trim(); - - // Try to parse it as is. - if (IPAddress.TryParse(addr, out IPAddress? res)) - { - ip = new IPNetAddress(res); - return true; - } - - // Is it a network? - string[] tokens = addr.Split('/'); - - if (tokens.Length == 2) - { - tokens[0] = tokens[0].TrimEnd(); - tokens[1] = tokens[1].TrimStart(); - - if (IPAddress.TryParse(tokens[0], out res)) - { - // Is the subnet part a cidr? - if (byte.TryParse(tokens[1], out byte cidr)) - { - ip = new IPNetAddress(res, cidr); - return true; - } - - // Is the subnet in x.y.a.b form? - if (IPAddress.TryParse(tokens[1], out IPAddress? mask)) - { - ip = new IPNetAddress(res, MaskToCidr(mask)); - return true; - } - } - } - } - - ip = None; - return false; - } - - /// - /// Parses the string provided, throwing an exception if it is badly formed. - /// - /// String to parse. - /// IPNetAddress object. - public static IPNetAddress Parse(string addr) - { - if (TryParse(addr, out IPNetAddress o)) - { - return o; - } - - throw new ArgumentException("Unable to recognise object :" + addr); - } - - /// - public override bool Contains(IPAddress address) - { - if (address == null) - { - throw new ArgumentNullException(nameof(address)); - } - - if (address.IsIPv4MappedToIPv6) - { - address = address.MapToIPv4(); - } - - var (altAddress, altPrefix) = NetworkAddressOf(address, PrefixLength); - return NetworkAddress.Address.Equals(altAddress) && NetworkAddress.PrefixLength >= altPrefix; - } - - /// - public override bool Contains(IPObject address) - { - if (address is IPHost addressObj && addressObj.HasAddress) - { - foreach (IPAddress addr in addressObj.GetAddresses()) - { - if (Contains(addr)) - { - return true; - } - } - } - else if (address is IPNetAddress netaddrObj) - { - // Have the same network address, but different subnets? - if (NetworkAddress.Address.Equals(netaddrObj.NetworkAddress.Address)) - { - return NetworkAddress.PrefixLength <= netaddrObj.PrefixLength; - } - - var altAddress = NetworkAddressOf(netaddrObj.Address, PrefixLength).Address; - return NetworkAddress.Address.Equals(altAddress); - } - - return false; - } - - /// - public override bool Equals(IPObject? other) - { - if (other is IPNetAddress otherObj && !Address.Equals(IPAddress.None) && !otherObj.Address.Equals(IPAddress.None)) - { - return Address.Equals(otherObj.Address) && - PrefixLength == otherObj.PrefixLength; - } - - return false; - } - - /// - public override bool Equals(IPAddress ip) - { - if (ip != null && !ip.Equals(IPAddress.None) && !Address.Equals(IPAddress.None)) - { - return ip.Equals(Address); - } - - return false; - } - - /// - public override string ToString() - { - return ToString(false); - } - - /// - /// Returns a textual representation of this object. - /// - /// Set to true, if the subnet is to be excluded as part of the address. - /// String representation of this object. - public string ToString(bool shortVersion) - { - if (!Address.Equals(IPAddress.None)) - { - if (Address.Equals(IPAddress.Any)) - { - return "Any IP4 Address"; - } - - if (Address.Equals(IPAddress.IPv6Any)) - { - return "Any IP6 Address"; - } - - if (Address.Equals(IPAddress.Broadcast)) - { - return "Any Address"; - } - - if (shortVersion) - { - return Address.ToString(); - } - - return $"{Address}/{PrefixLength}"; - } - - return string.Empty; - } - - /// - protected override IPObject CalculateNetworkAddress() - { - var (address, prefixLength) = NetworkAddressOf(_address, PrefixLength); - return new IPNetAddress(address, prefixLength); - } - } -} diff --git a/MediaBrowser.Common/Net/IPObject.cs b/MediaBrowser.Common/Net/IPObject.cs deleted file mode 100644 index 3a5187bc3d..0000000000 --- a/MediaBrowser.Common/Net/IPObject.cs +++ /dev/null @@ -1,370 +0,0 @@ -using System; -using System.Net; -using System.Net.Sockets; - -namespace MediaBrowser.Common.Net -{ - /// - /// Base network object class. - /// - public abstract class IPObject : IEquatable - { - /// - /// The network address of this object. - /// - private IPObject? _networkAddress; - - /// - /// Gets or sets a user defined value that is associated with this object. - /// - public int Tag { get; set; } - - /// - /// Gets or sets the object's IP address. - /// - public abstract IPAddress Address { get; set; } - - /// - /// Gets the object's network address. - /// - public IPObject NetworkAddress => _networkAddress ??= CalculateNetworkAddress(); - - /// - /// Gets or sets the object's IP address. - /// - public abstract byte PrefixLength { get; set; } - - /// - /// Gets the AddressFamily of this object. - /// - public AddressFamily AddressFamily - { - get - { - // Keep terms separate as Address performs other functions in inherited objects. - IPAddress address = Address; - return address.Equals(IPAddress.None) ? AddressFamily.Unspecified : address.AddressFamily; - } - } - - /// - /// Returns the network address of an object. - /// - /// IP Address to convert. - /// Subnet prefix. - /// IPAddress. - public static (IPAddress Address, byte PrefixLength) NetworkAddressOf(IPAddress address, byte prefixLength) - { - if (address == null) - { - throw new ArgumentNullException(nameof(address)); - } - - if (address.IsIPv4MappedToIPv6) - { - address = address.MapToIPv4(); - } - - if (IPAddress.IsLoopback(address)) - { - return (address, prefixLength); - } - - // An ip address is just a list of bytes, each one representing a segment on the network. - // This separates the IP address into octets and calculates how many octets will need to be altered or set to zero dependant upon the - // prefix length value. eg. /16 on a 4 octet ip4 address (192.168.2.240) will result in the 2 and the 240 being zeroed out. - // Where there is not an exact boundary (eg /23), mod is used to calculate how many bits of this value are to be kept. - - // GetAddressBytes - Span addressBytes = stackalloc byte[address.AddressFamily == AddressFamily.InterNetwork ? 4 : 16]; - address.TryWriteBytes(addressBytes, out _); - - int div = prefixLength / 8; - int mod = prefixLength % 8; - if (mod != 0) - { - // Prefix length is counted right to left, so subtract 8 so we know how many bits to clear. - mod = 8 - mod; - - // Shift out the bits from the octet that we don't want, by moving right then back left. - addressBytes[div] = (byte)((int)addressBytes[div] >> mod << mod); - // Move on the next byte. - div++; - } - - // Blank out the remaining octets from mod + 1 to the end of the byte array. (192.168.2.240/16 becomes 192.168.0.0) - for (int octet = div; octet < addressBytes.Length; octet++) - { - addressBytes[octet] = 0; - } - - // Return the network address for the prefix. - return (new IPAddress(addressBytes), prefixLength); - } - - /// - /// Tests to see if the ip address is an IP6 address. - /// - /// Value to test. - /// True if it is. - public static bool IsIP6(IPAddress address) - { - if (address == null) - { - throw new ArgumentNullException(nameof(address)); - } - - if (address.IsIPv4MappedToIPv6) - { - address = address.MapToIPv4(); - } - - return !address.Equals(IPAddress.None) && (address.AddressFamily == AddressFamily.InterNetworkV6); - } - - /// - /// Tests to see if the address in the private address range. - /// - /// Object to test. - /// True if it contains a private address. - public static bool IsPrivateAddressRange(IPAddress address) - { - if (address == null) - { - throw new ArgumentNullException(nameof(address)); - } - - if (!address.Equals(IPAddress.None)) - { - if (address.IsIPv4MappedToIPv6) - { - address = address.MapToIPv4(); - } - - if (address.AddressFamily == AddressFamily.InterNetwork) - { - // GetAddressBytes - Span octet = stackalloc byte[4]; - address.TryWriteBytes(octet, out _); - - return (octet[0] == 10) - || (octet[0] == 172 && octet[1] >= 16 && octet[1] <= 31) // RFC1918 - || (octet[0] == 192 && octet[1] == 168) // RFC1918 - || (octet[0] == 127); // RFC1122 - } - else - { - // GetAddressBytes - Span octet = stackalloc byte[16]; - address.TryWriteBytes(octet, out _); - - uint word = (uint)(octet[0] << 8) + octet[1]; - - return (word >= 0xfe80 && word <= 0xfebf) // fe80::/10 :Local link. - || (word >= 0xfc00 && word <= 0xfdff); // fc00::/7 :Unique local address. - } - } - - return false; - } - - /// - /// Returns true if the IPAddress contains an IP6 Local link address. - /// - /// IPAddress object to check. - /// True if it is a local link address. - /// - /// See https://stackoverflow.com/questions/6459928/explain-the-instance-properties-of-system-net-ipaddress - /// it appears that the IPAddress.IsIPv6LinkLocal is out of date. - /// - public static bool IsIPv6LinkLocal(IPAddress address) - { - if (address == null) - { - throw new ArgumentNullException(nameof(address)); - } - - if (address.IsIPv4MappedToIPv6) - { - address = address.MapToIPv4(); - } - - if (address.AddressFamily != AddressFamily.InterNetworkV6) - { - return false; - } - - // GetAddressBytes - Span octet = stackalloc byte[16]; - address.TryWriteBytes(octet, out _); - uint word = (uint)(octet[0] << 8) + octet[1]; - - return word >= 0xfe80 && word <= 0xfebf; // fe80::/10 :Local link. - } - - /// - /// Convert a subnet mask in CIDR notation to a dotted decimal string value. IPv4 only. - /// - /// Subnet mask in CIDR notation. - /// IPv4 or IPv6 family. - /// String value of the subnet mask in dotted decimal notation. - public static IPAddress CidrToMask(byte cidr, AddressFamily family) - { - uint addr = 0xFFFFFFFF << (family == AddressFamily.InterNetwork ? 32 : 128 - cidr); - addr = ((addr & 0xff000000) >> 24) - | ((addr & 0x00ff0000) >> 8) - | ((addr & 0x0000ff00) << 8) - | ((addr & 0x000000ff) << 24); - return new IPAddress(addr); - } - - /// - /// Convert a mask to a CIDR. IPv4 only. - /// https://stackoverflow.com/questions/36954345/get-cidr-from-netmask. - /// - /// Subnet mask. - /// Byte CIDR representing the mask. - public static byte MaskToCidr(IPAddress mask) - { - if (mask == null) - { - throw new ArgumentNullException(nameof(mask)); - } - - byte cidrnet = 0; - if (!mask.Equals(IPAddress.Any)) - { - // GetAddressBytes - Span bytes = stackalloc byte[mask.AddressFamily == AddressFamily.InterNetwork ? 4 : 16]; - mask.TryWriteBytes(bytes, out _); - - var zeroed = false; - for (var i = 0; i < bytes.Length; i++) - { - for (int v = bytes[i]; (v & 0xFF) != 0; v <<= 1) - { - if (zeroed) - { - // Invalid netmask. - return (byte)~cidrnet; - } - - if ((v & 0x80) == 0) - { - zeroed = true; - } - else - { - cidrnet++; - } - } - } - } - - return cidrnet; - } - - /// - /// Tests to see if this object is a Loopback address. - /// - /// True if it is. - public virtual bool IsLoopback() - { - return IPAddress.IsLoopback(Address); - } - - /// - /// Removes all addresses of a specific type from this object. - /// - /// Type of address to remove. - public virtual void Remove(AddressFamily family) - { - // This method only performs a function in the IPHost implementation of IPObject. - } - - /// - /// Tests to see if this object is an IPv6 address. - /// - /// True if it is. - public virtual bool IsIP6() - { - return IsIP6(Address); - } - - /// - /// Returns true if this IP address is in the RFC private address range. - /// - /// True this object has a private address. - public virtual bool IsPrivateAddressRange() - { - return IsPrivateAddressRange(Address); - } - - /// - /// Compares this to the object passed as a parameter. - /// - /// Object to compare to. - /// Equality result. - public virtual bool Equals(IPAddress ip) - { - if (ip != null) - { - if (ip.IsIPv4MappedToIPv6) - { - ip = ip.MapToIPv4(); - } - - return !Address.Equals(IPAddress.None) && Address.Equals(ip); - } - - return false; - } - - /// - /// Compares this to the object passed as a parameter. - /// - /// Object to compare to. - /// Equality result. - public virtual bool Equals(IPObject? other) - { - if (other != null) - { - return !Address.Equals(IPAddress.None) && Address.Equals(other.Address); - } - - return false; - } - - /// - /// Compares the address in this object and the address in the object passed as a parameter. - /// - /// Object's IP address to compare to. - /// Comparison result. - public abstract bool Contains(IPObject address); - - /// - /// Compares the address in this object and the address in the object passed as a parameter. - /// - /// Object's IP address to compare to. - /// Comparison result. - public abstract bool Contains(IPAddress address); - - /// - public override int GetHashCode() - { - return Address.GetHashCode(); - } - - /// - public override bool Equals(object? obj) - { - return Equals(obj as IPObject); - } - - /// - /// Calculates the network address of this object. - /// - /// Returns the network address of this object. - protected abstract IPObject CalculateNetworkAddress(); - } -} diff --git a/MediaBrowser.Common/Net/NetworkExtensions.cs b/MediaBrowser.Common/Net/NetworkExtensions.cs index 264bfacb49..55ec322f43 100644 --- a/MediaBrowser.Common/Net/NetworkExtensions.cs +++ b/MediaBrowser.Common/Net/NetworkExtensions.cs @@ -1,6 +1,9 @@ using System; -using System.Collections.ObjectModel; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Net; +using System.Net.Sockets; +using System.Text.RegularExpressions; namespace MediaBrowser.Common.Net { @@ -10,251 +13,209 @@ namespace MediaBrowser.Common.Net public static class NetworkExtensions { /// - /// Add an address to the collection. + /// Returns true if the IPAddress contains an IP6 Local link address. /// - /// The . - /// Item to add. - public static void AddItem(this Collection source, IPAddress ip) + /// IPAddress object to check. + /// True if it is a local link address. + /// + /// See https://stackoverflow.com/questions/6459928/explain-the-instance-properties-of-system-net-ipaddress + /// it appears that the IPAddress.IsIPv6LinkLocal is out of date. + /// + public static bool IsIPv6LinkLocal(IPAddress address) { - if (!source.ContainsAddress(ip)) + if (address == null) { - source.Add(new IPNetAddress(ip, 32)); + throw new ArgumentNullException(nameof(address)); } - } - /// - /// Adds a network to the collection. - /// - /// The . - /// Item to add. - /// If true the values are treated as subnets. - /// If false items are addresses. - public static void AddItem(this Collection source, IPObject item, bool itemsAreNetworks = true) - { - if (!source.ContainsAddress(item) || !itemsAreNetworks) + if (address.IsIPv4MappedToIPv6) { - source.Add(item); + address = address.MapToIPv4(); } - } - /// - /// Converts this object to a string. - /// - /// The . - /// Returns a string representation of this object. - public static string AsString(this Collection source) - { - return $"[{string.Join(',', source)}]"; - } - - /// - /// Returns true if the collection contains an item with the ip address, - /// or the ip address falls within any of the collection's network ranges. - /// - /// The . - /// The item to look for. - /// True if the collection contains the item. - public static bool ContainsAddress(this Collection source, IPAddress item) - { - if (source.Count == 0) + if (address.AddressFamily != AddressFamily.InterNetworkV6) { return false; } - if (item == null) - { - throw new ArgumentNullException(nameof(item)); - } + // GetAddressBytes + Span octet = stackalloc byte[16]; + address.TryWriteBytes(octet, out _); + uint word = (uint)(octet[0] << 8) + octet[1]; - if (item.IsIPv4MappedToIPv6) - { - item = item.MapToIPv4(); - } - - foreach (var i in source) - { - if (i.Contains(item)) - { - return true; - } - } - - return false; + return word >= 0xfe80 && word <= 0xfebf; // fe80::/10 :Local link. } /// - /// Returns true if the collection contains an item with the ip address, - /// or the ip address falls within any of the collection's network ranges. + /// Convert a subnet mask in CIDR notation to a dotted decimal string value. IPv4 only. /// - /// The . - /// The item to look for. - /// True if the collection contains the item. - public static bool ContainsAddress(this Collection source, IPObject item) + /// Subnet mask in CIDR notation. + /// IPv4 or IPv6 family. + /// String value of the subnet mask in dotted decimal notation. + public static IPAddress CidrToMask(byte cidr, AddressFamily family) { - if (source.Count == 0) - { - return false; - } - - if (item == null) - { - throw new ArgumentNullException(nameof(item)); - } - - foreach (var i in source) - { - if (i.Contains(item)) - { - return true; - } - } - - return false; + uint addr = 0xFFFFFFFF << (family == AddressFamily.InterNetwork ? 32 : 128 - cidr); + addr = ((addr & 0xff000000) >> 24) + | ((addr & 0x00ff0000) >> 8) + | ((addr & 0x0000ff00) << 8) + | ((addr & 0x000000ff) << 24); + return new IPAddress(addr); } /// - /// Compares two Collection{IPObject} objects. The order is ignored. + /// Convert a subnet mask to a CIDR. IPv4 only. + /// https://stackoverflow.com/questions/36954345/get-cidr-from-netmask. /// - /// The . - /// Item to compare to. - /// True if both are equal. - public static bool Compare(this Collection source, Collection dest) + /// Subnet mask. + /// Byte CIDR representing the mask. + public static byte MaskToCidr(IPAddress mask) { - if (dest == null || source.Count != dest.Count) + if (mask == null) { - return false; + throw new ArgumentNullException(nameof(mask)); } - foreach (var sourceItem in source) + byte cidrnet = 0; + if (!mask.Equals(IPAddress.Any)) { - bool found = false; - foreach (var destItem in dest) + // GetAddressBytes + Span bytes = stackalloc byte[mask.AddressFamily == AddressFamily.InterNetwork ? 4 : 16]; + mask.TryWriteBytes(bytes, out _); + + var zeroed = false; + for (var i = 0; i < bytes.Length; i++) { - if (sourceItem.Equals(destItem)) + for (int v = bytes[i]; (v & 0xFF) != 0; v <<= 1) { - found = true; - break; - } - } - - if (!found) - { - return false; - } - } - - return true; - } - - /// - /// Returns a collection containing the subnets of this collection given. - /// - /// The . - /// Collection{IPObject} object containing the subnets. - public static Collection AsNetworks(this Collection source) - { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - Collection res = new Collection(); - - foreach (IPObject i in source) - { - if (i is IPNetAddress nw) - { - // Add the subnet calculated from the interface address/mask. - var na = nw.NetworkAddress; - na.Tag = i.Tag; - res.AddItem(na); - } - else if (i is IPHost ipHost) - { - // Flatten out IPHost and add all its ip addresses. - foreach (var addr in ipHost.GetAddresses()) - { - IPNetAddress host = new IPNetAddress(addr) + if (zeroed) { - Tag = i.Tag - }; + // Invalid netmask. + return (byte)~cidrnet; + } - res.AddItem(host); + if ((v & 0x80) == 0) + { + zeroed = true; + } + else + { + cidrnet++; + } } } } - return res; + return cidrnet; } /// - /// Excludes all the items from this list that are found in excludeList. + /// Converts an IPAddress into a string. + /// Ipv6 addresses are returned in [ ], with their scope removed. /// - /// The . - /// Items to exclude. - /// Collection is a network collection. - /// A new collection, with the items excluded. - public static Collection Exclude(this Collection source, Collection excludeList, bool isNetwork) + /// Address to convert. + /// URI safe conversion of the address. + public static string FormatIpString(IPAddress? address) { - if (source.Count == 0 || excludeList == null) + if (address == null) { - return new Collection(source); + return string.Empty; } - Collection results = new Collection(); - - bool found; - foreach (var outer in source) + var str = address.ToString(); + if (address.AddressFamily == AddressFamily.InterNetworkV6) { - found = false; - - foreach (var inner in excludeList) + int i = str.IndexOf('%', StringComparison.Ordinal); + if (i != -1) { - if (outer.Equals(inner)) + str = str.Substring(0, i); + } + + return $"[{str}]"; + } + + return str; + } + + /// + /// Attempts to parse a host string. + /// + /// Host name to parse. + /// Object representing the string, if it has successfully been parsed. + /// true if IPv4 is enabled. + /// true if IPv6 is enabled. + /// true if the parsing is successful, false if not. + public static bool TryParseHost(string host, [NotNullWhen(true)] out IPAddress[] addresses, bool isIpv4Enabled = true, bool isIpv6Enabled = false) + { + if (string.IsNullOrWhiteSpace(host)) + { + addresses = Array.Empty(); + return false; + } + + host = host.Trim(); + + // See if it's an IPv6 with port address e.g. [::1] or [::1]:120. + if (host[0] == '[') + { + int i = host.IndexOf(']', StringComparison.Ordinal); + if (i != -1) + { + return TryParseHost(host.Remove(i)[1..], out addresses); + } + + addresses = Array.Empty(); + return false; + } + + var hosts = host.Split(':'); + + if (hosts.Length <= 2) + { + // Use regular expression as CheckHostName isn't RFC5892 compliant. + // Modified from gSkinner's expression at https://stackoverflow.com/questions/11809631/fully-qualified-domain-name-validation + string pattern = @"(?im)^(?!:\/\/)(?=.{1,255}$)((.{1,63}\.){0,127}(?![0-9]*$)[a-z0-9-]+\.?)(:(\d){1,5}){0,1}$"; + + // Is hostname or hostname:port + if (Regex.IsMatch(hosts[0], pattern)) + { + try { - found = true; - break; + addresses = Dns.GetHostAddresses(hosts[0]); + return true; + } + catch (SocketException) + { + // Log and then ignore socket errors, as the result value will just be an empty array. + Console.WriteLine("GetHostAddresses failed."); } } - if (!found) + // Is an IP4 or IP4:port + host = hosts[0].Split('/')[0]; + + if (IPAddress.TryParse(host, out var address)) { - results.AddItem(outer, isNetwork); + if (((address.AddressFamily == AddressFamily.InterNetwork) && (!isIpv4Enabled && isIpv6Enabled)) || + ((address.AddressFamily == AddressFamily.InterNetworkV6) && (isIpv4Enabled && !isIpv6Enabled))) + { + addresses = Array.Empty(); + return false; + } + + addresses = new[] { address }; + + // Host name is an ip4 address, so fake resolve. + return true; } } - - return results; - } - - /// - /// Returns all items that co-exist in this object and target. - /// - /// The . - /// Collection to compare with. - /// A collection containing all the matches. - public static Collection ThatAreContainedInNetworks(this Collection source, Collection target) - { - if (source.Count == 0) + else if (hosts.Length <= 9 && IPAddress.TryParse(host.Split('/')[0], out var address)) // 8 octets + port { - return new Collection(); + addresses = new[] { address }; + return true; } - if (target == null) - { - throw new ArgumentNullException(nameof(target)); - } - - Collection nc = new Collection(); - - foreach (IPObject i in source) - { - if (target.ContainsAddress(i)) - { - nc.AddItem(i); - } - } - - return nc; + addresses = Array.Empty(); + return false; } } } diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index 11afdc4aed..45ac5c3a85 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -4,7 +4,6 @@ using System.Net; using MediaBrowser.Common; -using MediaBrowser.Common.Net; using MediaBrowser.Model.System; using Microsoft.AspNetCore.Http; @@ -75,10 +74,10 @@ namespace MediaBrowser.Controller /// /// Gets an URL that can be used to access the API over LAN. /// - /// An optional hostname to use. + /// An optional IP address to use. /// A value indicating whether to allow HTTPS. /// The API URL. - string GetApiUrlForLocalAccess(IPObject hostname = null, bool allowHttps = true); + string GetApiUrlForLocalAccess(IPAddress ipAddress = null, bool allowHttps = true); /// /// Gets a local (LAN) URL that can be used to access the API. diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs index a7767b3c04..37c4128ba4 100644 --- a/RSSDP/SsdpDevicePublisher.cs +++ b/RSSDP/SsdpDevicePublisher.cs @@ -7,6 +7,7 @@ using System.Net; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Net; +using Microsoft.AspNetCore.HttpOverrides; namespace Rssdp.Infrastructure { @@ -297,9 +298,9 @@ namespace Rssdp.Infrastructure foreach (var device in deviceList) { 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)) + var source = new IPData(root.Address, new IPNetwork(root.Address, root.PrefixLength), root.FriendlyName); + var destination = new IPData(remoteEndPoint.Address, new IPNetwork(root.Address, root.PrefixLength)); + if (!_sendOnlyMatchedHost || source.Address.Equals(destination.Address)) { SendDeviceSearchResponses(device, remoteEndPoint, receivedOnlocalIpAddress, cancellationToken); } diff --git a/tests/Jellyfin.Networking.Tests/IPNetAddressTests.cs b/tests/Jellyfin.Networking.Tests/IPNetAddressTests.cs deleted file mode 100644 index aa2dbc57a2..0000000000 --- a/tests/Jellyfin.Networking.Tests/IPNetAddressTests.cs +++ /dev/null @@ -1,49 +0,0 @@ -using FsCheck; -using FsCheck.Xunit; -using MediaBrowser.Common.Net; -using Xunit; - -namespace Jellyfin.Networking.Tests -{ - public static class IPNetAddressTests - { - /// - /// Checks IP address formats. - /// - /// IP Address. - [Theory] - [InlineData("127.0.0.1")] - [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]")] - [InlineData("fe80::7add:12ff:febb:c67b%16")] - [InlineData("[fe80::7add:12ff:febb:c67b%16]:123")] - [InlineData("fe80::7add:12ff:febb:c67b%16:123")] - [InlineData("[fe80::7add:12ff:febb:c67b%16]")] - [InlineData("192.168.1.2/255.255.255.0")] - [InlineData("192.168.1.2/24")] - public static void TryParse_ValidIPStrings_True(string address) - => Assert.True(IPNetAddress.TryParse(address, out _)); - - [Property] - public static Property TryParse_IPv4Address_True(IPv4Address address) - => IPNetAddress.TryParse(address.Item.ToString(), out _).ToProperty(); - - [Property] - public static Property TryParse_IPv6Address_True(IPv6Address address) - => IPNetAddress.TryParse(address.Item.ToString(), out _).ToProperty(); - - /// - /// All should be invalid address strings. - /// - /// Invalid address strings. - [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 static void TryParse_InvalidAddressString_False(string address) - => Assert.False(IPNetAddress.TryParse(address, out _)); - } -} diff --git a/tests/Jellyfin.Networking.Tests/IPHostTests.cs b/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs similarity index 80% rename from tests/Jellyfin.Networking.Tests/IPHostTests.cs rename to tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs index ec3a1300c8..c81fdefe95 100644 --- a/tests/Jellyfin.Networking.Tests/IPHostTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs @@ -5,7 +5,7 @@ using Xunit; namespace Jellyfin.Networking.Tests { - public static class IPHostTests + public static class NetworkExtensionsTests { /// /// Checks IP address formats. @@ -27,15 +27,15 @@ namespace Jellyfin.Networking.Tests [InlineData("192.168.1.2/255.255.255.0")] [InlineData("192.168.1.2/24")] public static void TryParse_ValidHostStrings_True(string address) - => Assert.True(IPHost.TryParse(address, out _)); + => Assert.True(NetworkExtensions.TryParseHost(address, out _, true, true)); [Property] public static Property TryParse_IPv4Address_True(IPv4Address address) - => IPHost.TryParse(address.Item.ToString(), out _).ToProperty(); + => NetworkExtensions.TryParseHost(address.Item.ToString(), out _, true, true).ToProperty(); [Property] public static Property TryParse_IPv6Address_True(IPv6Address address) - => IPHost.TryParse(address.Item.ToString(), out _).ToProperty(); + => NetworkExtensions.TryParseHost(address.Item.ToString(), out _, true, true).ToProperty(); /// /// All should be invalid address strings. @@ -48,6 +48,6 @@ namespace Jellyfin.Networking.Tests [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517:1231")] [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517:1231]")] public static void TryParse_InvalidAddressString_False(string address) - => Assert.False(IPHost.TryParse(address, out _)); + => Assert.False(NetworkExtensions.TryParseHost(address, out _, true, true)); } } diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs index 6b93974373..ce638c9136 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs @@ -1,10 +1,13 @@ using System; using System.Collections.ObjectModel; +using System.Globalization; +using System.Linq; using System.Net; using Jellyfin.Networking.Configuration; using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; +using Microsoft.AspNetCore.HttpOverrides; using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; @@ -46,6 +49,8 @@ namespace Jellyfin.Networking.Tests { EnableIPV6 = true, EnableIPV4 = true, + IgnoreVirtualInterfaces = true, + VirtualInterfaceNames = "veth", LocalNetworkSubnets = lan?.Split(';') ?? throw new ArgumentNullException(nameof(lan)) }; @@ -53,135 +58,7 @@ namespace Jellyfin.Networking.Tests using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); NetworkManager.MockNetworkSettings = string.Empty; - Assert.Equal(nm.GetInternalBindAddresses().AsString(), value); - } - - /// - /// Test collection parsing. - /// - /// Collection to parse. - /// Included addresses from the collection. - /// Included IP4 addresses from the collection. - /// Excluded addresses from the collection. - /// Excluded IP4 addresses from the collection. - /// Network addresses of the collection. - [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,::1/128],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,::1/128,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()); - - // Test included. - Collection 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); - } - - /// - /// Union two collections. - /// - /// Source. - /// Destination. - /// Result. - [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()); - - Collection nc1 = nm.CreateIPCollection(settings.Split(','), false); - Collection nc2 = nm.CreateIPCollection(compare.Split(','), false); - - Assert.Equal(nc1.ThatAreContainedInNetworks(nc2).AsString(), result); + Assert.Equal(value, "[" + String.Join(",", nm.GetInternalBindAddresses().Select(x => x.Address + "/" + x.Subnet.PrefixLength)) + "]"); } [Theory] @@ -194,8 +71,11 @@ namespace Jellyfin.Networking.Tests [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))); + var split = netMask.Split("/"); + var mask = int.Parse(split[1], CultureInfo.InvariantCulture); + var ipa = IPAddress.Parse(split[0]); + var ipn = new IPNetwork(ipa, mask); + Assert.True(ipn.Contains(IPAddress.Parse(ipAddress))); } [Theory] @@ -207,8 +87,11 @@ namespace Jellyfin.Networking.Tests [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))); + var split = netMask.Split("/"); + var mask = int.Parse(split[1], CultureInfo.InvariantCulture); + var ipa = IPAddress.Parse(split[0]); + var ipn = new IPNetwork(ipa, mask); + Assert.False(ipn.Contains(IPAddress.Parse(ipAddress))); } [Theory] @@ -219,8 +102,11 @@ namespace Jellyfin.Networking.Tests [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))); + var split = netMask.Split("/"); + var mask = int.Parse(split[1], CultureInfo.InvariantCulture); + var ipa = IPAddress.Parse(split[0]); + var ipn = new IPNetwork(ipa, mask); + Assert.True(ipn.Contains(IPAddress.Parse(ipAddress))); } [Theory] @@ -231,86 +117,18 @@ namespace Jellyfin.Networking.Tests [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))); + var split = netMask.Split("/"); + var mask = int.Parse(split[1], CultureInfo.InvariantCulture); + var ipa = IPAddress.Parse(split[0]); + var ipn = new IPNetwork(ipa, mask); + Assert.False(ipn.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(IPNetAddress.TryParse(network, out var networkObj)); - Assert.True(IPNetAddress.TryParse(ip, out var 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()); - - // Test included, IP6. - Collection ncSource = nm.CreateIPCollection(source.Split(',')); - Collection ncDest = nm.CreateIPCollection(dest.Split(',')); - Collection ncResult = ncSource.ThatAreContainedInNetworks(ncDest); - Collection 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. + // This test is to replicate how DLNA 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")] @@ -357,14 +175,14 @@ namespace Jellyfin.Networking.Tests using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); NetworkManager.MockNetworkSettings = string.Empty; - _ = nm.TryParseInterface(result, out Collection? resultObj); + _ = nm.TryParseInterface(result, out Collection? resultObj); // Check to see if dns resolution is working. If not, skip test. - _ = IPHost.TryParse(source, out var host); + _ = NetworkExtensions.TryParseHost(source, out var host); - if (resultObj != null && host?.HasAddress == true) + if (resultObj != null && host.Length > 0) { - result = ((IPNetAddress)resultObj[0]).ToString(true); + result = resultObj.First().Address.ToString(); var intf = nm.GetBindInterface(source, out _); Assert.Equal(intf, result); @@ -394,7 +212,7 @@ namespace Jellyfin.Networking.Tests [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")] + [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")] @@ -426,15 +244,15 @@ namespace Jellyfin.Networking.Tests using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); NetworkManager.MockNetworkSettings = string.Empty; - if (nm.TryParseInterface(result, out Collection? resultObj) && resultObj != null) + if (nm.TryParseInterface(result, out Collection? resultObj) && resultObj != null) { // Parse out IPAddresses so we can do a string comparison. (Ignore subnet masks). - result = ((IPNetAddress)resultObj[0]).ToString(true); + result = resultObj.First().Address.ToString(); } var intf = nm.GetBindInterface(source, out int? _); - Assert.Equal(intf, result); + Assert.Equal(result, intf); } [Theory] @@ -461,6 +279,7 @@ namespace Jellyfin.Networking.Tests [InlineData("185.10.10.10", "79.2.3.4", false)] [InlineData("185.10.10.10", "185.10.10.10", true)] [InlineData("", "100.100.100.100", false)] + public void HasRemoteAccess_GivenBlacklist_BlacklistTheIps(string addresses, string remoteIp, bool denied) { // Comma separated list of IP addresses or IP/netmask entries for networks that will be allowed to connect remotely. From 997aa3f1e74d6bbe4f9f928e1162638d75feb12e Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Tue, 19 Jul 2022 21:53:10 +0200 Subject: [PATCH 005/858] Fix build --- Jellyfin.Networking/Manager/NetworkManager.cs | 14 +++++++------- MediaBrowser.Common/Net/INetworkManager.cs | 4 ++-- .../Jellyfin.Networking.Tests/NetworkParseTests.cs | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index f51fd85dd5..d9ef2c6a4c 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -241,7 +241,7 @@ namespace Jellyfin.Networking.Manager { var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name); interfaceObject.Index = ipProperties.GetIPv4Properties().Index; - interfaceObject.Name = adapter.Name.ToLower(CultureInfo.InvariantCulture); + interfaceObject.Name = adapter.Name.ToLowerInvariant(); _interfaces.Add(interfaceObject); } @@ -249,7 +249,7 @@ namespace Jellyfin.Networking.Manager { var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name); interfaceObject.Index = ipProperties.GetIPv6Properties().Index; - interfaceObject.Name = adapter.Name.ToLower(CultureInfo.InvariantCulture); + interfaceObject.Name = adapter.Name.ToLowerInvariant(); _interfaces.Add(interfaceObject); } @@ -381,7 +381,7 @@ namespace Jellyfin.Networking.Manager if (config.IgnoreVirtualInterfaces) { // Remove potentially exisiting * and split config string into prefixes - var virtualInterfacePrefixes = config.VirtualInterfaceNames.Replace("*", string.Empty, StringComparison.OrdinalIgnoreCase).ToLower().Split(','); + var virtualInterfacePrefixes = config.VirtualInterfaceNames.Replace("*", string.Empty, StringComparison.OrdinalIgnoreCase).ToLowerInvariant().Split(','); // Check all interfaces for matches against the prefixes and add the interface IPs to _bindExclusions if (_bindAddresses.Count > 0 && virtualInterfacePrefixes.Length > 0) @@ -419,10 +419,10 @@ namespace Jellyfin.Networking.Manager if (remoteIPFilter.Any() && !string.IsNullOrWhiteSpace(remoteIPFilter.First())) { // Parse all IPs with netmask to a subnet - _ = TryParseSubnets(remoteIPFilter.Where(x => x.Contains("/", StringComparison.OrdinalIgnoreCase)).ToArray(), out _remoteAddressFilter, false); + _ = TryParseSubnets(remoteIPFilter.Where(x => x.Contains('/', StringComparison.OrdinalIgnoreCase)).ToArray(), out _remoteAddressFilter, false); // Parse everything else as an IP and construct subnet with a single IP - var ips = remoteIPFilter.Where(x => !x.Contains("/", StringComparison.OrdinalIgnoreCase)); + var ips = remoteIPFilter.Where(x => !x.Contains('/', StringComparison.OrdinalIgnoreCase)); foreach (var ip in ips) { if (IPAddress.TryParse(ip, out var ipp)) @@ -573,7 +573,7 @@ namespace Jellyfin.Networking.Manager if (_interfaces != null) { // Match all interfaces starting with names starting with token - var matchedInterfaces = _interfaces.Where(s => s.Name.Equals(intf.ToLower(CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase)); + var matchedInterfaces = _interfaces.Where(s => s.Name.Equals(intf.ToLowerInvariant(), StringComparison.OrdinalIgnoreCase)); if (matchedInterfaces.Any()) { _logger.LogInformation("Interface {Token} used in settings. Using its interface addresses.", intf); @@ -998,7 +998,7 @@ namespace Jellyfin.Networking.Manager var validPublishedServerUrls = _publishedServerUrls.Where(x => x.Key.Address.Equals(IPAddress.Any)).ToList(); validPublishedServerUrls.AddRange(_publishedServerUrls.Where(x => x.Key.Address.Equals(IPAddress.IPv6Any))); validPublishedServerUrls.AddRange(_publishedServerUrls.Where(x => x.Key.Subnet.Contains(source))); - validPublishedServerUrls.Distinct(); + validPublishedServerUrls = validPublishedServerUrls.GroupBy(x => x.Key).Select(y => y.First()).ToList(); // Check for user override. foreach (var data in validPublishedServerUrls) diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs index fdf42bdbce..9f48535576 100644 --- a/MediaBrowser.Common/Net/INetworkManager.cs +++ b/MediaBrowser.Common/Net/INetworkManager.cs @@ -18,12 +18,12 @@ namespace MediaBrowser.Common.Net event EventHandler NetworkChanged; /// - /// Gets a value indicating whether iP6 is enabled. + /// Gets a value indicating whether IPv6 is enabled. /// bool IsIpv6Enabled { get; } /// - /// Gets a value indicating whether iP4 is enabled. + /// Gets a value indicating whether IPv4 is enabled. /// bool IsIpv4Enabled { get; } diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs index ce638c9136..d451fbaef1 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs @@ -58,7 +58,7 @@ namespace Jellyfin.Networking.Tests using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); NetworkManager.MockNetworkSettings = string.Empty; - Assert.Equal(value, "[" + String.Join(",", nm.GetInternalBindAddresses().Select(x => x.Address + "/" + x.Subnet.PrefixLength)) + "]"); + Assert.Equal(value, "[" + string.Join(",", nm.GetInternalBindAddresses().Select(x => x.Address + "/" + x.Subnet.PrefixLength)) + "]"); } [Theory] From bdb148316719c32e2aa3a3eba94e161c7824dbac Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Tue, 19 Jul 2022 23:42:32 +0200 Subject: [PATCH 006/858] Add generic IPAddress.Parse tests --- .../NetworkParseTests.cs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs index d451fbaef1..5b9739dd3d 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs @@ -61,6 +61,39 @@ namespace Jellyfin.Networking.Tests Assert.Equal(value, "[" + string.Join(",", nm.GetInternalBindAddresses().Select(x => x.Address + "/" + x.Subnet.PrefixLength)) + "]"); } + /// + /// Checks valid IP address formats. + /// + /// IP Address. + [Theory] + [InlineData("127.0.0.1")] + [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517")] + [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517]")] + [InlineData("fe80::7add:12ff:febb:c67b%16")] + [InlineData("[fe80::7add:12ff:febb:c67b%16]:123")] + [InlineData("fe80::7add:12ff:febb:c67b%16:123")] + [InlineData("[fe80::7add:12ff:febb:c67b%16]")] + [InlineData("192.168.1.2")] + public static void TryParseValidIPStringsTrue(string address) + => Assert.True(IPAddress.TryParse(address, out _)); + + /// + /// Checks invalid IP address formats. + /// + /// IP Address. + [Theory] + [InlineData("192.168.1.2/255.255.255.0")] + [InlineData("192.168.1.2/24")] + [InlineData("127.0.0.1/8")] + [InlineData("127.0.0.1#")] + [InlineData("localhost!")] + [InlineData("256.128.0.0.0.1")] + [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517/56")] + [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517:1231")] + [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517:1231]")] + public static void TryParseInvalidIPStringsFalse(string address) + => Assert.False(IPAddress.TryParse(address, out _)); + [Theory] [InlineData("192.168.5.85/24", "192.168.5.1")] [InlineData("192.168.5.85/24", "192.168.5.254")] From 1c6b6f5d36cb09c0352bebc1ab26b88e4f511921 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 20 Jul 2022 07:42:52 +0200 Subject: [PATCH 007/858] Remove socket wrkaround --- Jellyfin.Server/Program.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 63055a61d4..2bbc2546ed 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -335,13 +335,6 @@ namespace Jellyfin.Server if (startupConfig.UseUnixSocket() && Environment.OSVersion.Platform == PlatformID.Unix) { var socketPath = GetUnixSocketPath(startupConfig, appPaths); - - // Workaround for https://github.com/aspnet/AspNetCore/issues/14134 - if (File.Exists(socketPath)) - { - File.Delete(socketPath); - } - options.ListenUnixSocket(socketPath); _logger.LogInformation("Kestrel listening to unix socket {SocketPath}", socketPath); } From daf33143f6ad30e0d76e038840a1583e4302cd7b Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 20 Jul 2022 09:44:15 +0200 Subject: [PATCH 008/858] Handle forwarded requests not having port set in GetSmartApiUrl --- Emby.Server.Implementations/ApplicationHost.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index a0ad4a9ab5..56f997b3b5 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1080,7 +1080,8 @@ namespace Emby.Server.Implementations if (ConfigurationManager.GetNetworkConfiguration().EnablePublishedServerUriByRequest) { int? requestPort = request.Host.Port; - if ((requestPort == 80 && string.Equals(request.Scheme, "http", StringComparison.OrdinalIgnoreCase)) || (requestPort == 443 && string.Equals(request.Scheme, "https", StringComparison.OrdinalIgnoreCase))) + if (((requestPort == 80 || requestPort == null) && string.Equals(request.Scheme, "http", StringComparison.OrdinalIgnoreCase)) + || ((requestPort == 443 || requestPort == null) && string.Equals(request.Scheme, "https", StringComparison.OrdinalIgnoreCase))) { requestPort = -1; } From 64ffd5fd9526762e27d97e13c59cc6552c97e7bc Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 20 Jul 2022 09:45:57 +0200 Subject: [PATCH 009/858] Move subnet parser to NetworkExtensions --- Jellyfin.Networking/Manager/NetworkManager.cs | 72 ++----------------- MediaBrowser.Common/Net/NetworkExtensions.cs | 58 ++++++++++++++- 2 files changed, 62 insertions(+), 68 deletions(-) diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index d9ef2c6a4c..e9e3e18022 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -326,8 +326,8 @@ namespace Jellyfin.Networking.Manager // Get configuration options string[] subnets = config.LocalNetworkSubnets; - _ = TryParseSubnets(subnets, out _lanSubnets, false); - _ = TryParseSubnets(subnets, out _excludedSubnets, true); + _ = NetworkExtensions.TryParseSubnets(subnets, out _lanSubnets, false); + _ = NetworkExtensions.TryParseSubnets(subnets, out _excludedSubnets, true); if (_lanSubnets.Count == 0) { @@ -419,7 +419,7 @@ namespace Jellyfin.Networking.Manager if (remoteIPFilter.Any() && !string.IsNullOrWhiteSpace(remoteIPFilter.First())) { // Parse all IPs with netmask to a subnet - _ = TryParseSubnets(remoteIPFilter.Where(x => x.Contains('/', StringComparison.OrdinalIgnoreCase)).ToArray(), out _remoteAddressFilter, false); + _ = NetworkExtensions.TryParseSubnets(remoteIPFilter.Where(x => x.Contains('/', StringComparison.OrdinalIgnoreCase)).ToArray(), out _remoteAddressFilter, false); // Parse everything else as an IP and construct subnet with a single IP var ips = remoteIPFilter.Where(x => !x.Contains('/', StringComparison.OrdinalIgnoreCase)); @@ -595,68 +595,6 @@ namespace Jellyfin.Networking.Manager return false; } - /// - /// Try parsing an array of strings into subnets, respecting exclusions. - /// - /// Input string to be parsed. - /// Collection of . - /// Boolean signaling if negated or not negated values should be parsed. - /// True if parsing was successful. - public bool TryParseSubnets(string[] values, out Collection result, bool negated = false) - { - result = new Collection(); - - if (values == null || values.Length == 0) - { - return false; - } - - for (int a = 0; a < values.Length; a++) - { - string[] v = values[a].Trim().Split("/"); - - try - { - var address = IPAddress.None; - if (negated && v[0].StartsWith('!')) - { - _ = IPAddress.TryParse(v[0][1..], out address); - } - else if (!negated) - { - _ = IPAddress.TryParse(v[0][0..], out address); - } - - if (address != IPAddress.None && address != null) - { - if (int.TryParse(v[1], out var netmask)) - { - result.Add(new IPNetwork(address, netmask)); - } - else if (address.AddressFamily == AddressFamily.InterNetwork) - { - result.Add(new IPNetwork(address, 32)); - } - else if (address.AddressFamily == AddressFamily.InterNetworkV6) - { - result.Add(new IPNetwork(address, 128)); - } - } - } - catch (ArgumentException e) - { - _logger.LogWarning(e, "Ignoring LAN value {Value}.", v); - } - } - - if (result.Count > 0) - { - return true; - } - - return false; - } - /// public bool HasRemoteAccess(IPAddress remoteIp) { @@ -802,12 +740,12 @@ namespace Jellyfin.Networking.Manager if (source != null) { - if (IsIpv4Enabled && source.AddressFamily == AddressFamily.InterNetworkV6) + if (IsIpv4Enabled && !IsIpv6Enabled && source.AddressFamily == AddressFamily.InterNetworkV6) { _logger.LogWarning("IPv6 is disabled in Jellyfin, but enabled in the OS. This may affect how the interface is selected."); } - if (IsIpv6Enabled && source.AddressFamily == AddressFamily.InterNetwork) + if (!IsIpv4Enabled && IsIpv6Enabled && source.AddressFamily == AddressFamily.InterNetwork) { _logger.LogWarning("IPv4 is disabled in Jellyfin, but enabled in the OS. This may affect how the interface is selected."); } diff --git a/MediaBrowser.Common/Net/NetworkExtensions.cs b/MediaBrowser.Common/Net/NetworkExtensions.cs index 55ec322f43..4cba1c99f2 100644 --- a/MediaBrowser.Common/Net/NetworkExtensions.cs +++ b/MediaBrowser.Common/Net/NetworkExtensions.cs @@ -1,5 +1,6 @@ +using Microsoft.AspNetCore.HttpOverrides; using System; -using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.Sockets; @@ -136,6 +137,61 @@ namespace MediaBrowser.Common.Net return str; } + /// + /// Try parsing an array of strings into subnets, respecting exclusions. + /// + /// Input string to be parsed. + /// Collection of . + /// Boolean signaling if negated or not negated values should be parsed. + /// True if parsing was successful. + public static bool TryParseSubnets(string[] values, out Collection result, bool negated = false) + { + result = new Collection(); + + if (values == null || values.Length == 0) + { + return false; + } + + for (int a = 0; a < values.Length; a++) + { + string[] v = values[a].Trim().Split("/"); + + var address = IPAddress.None; + if (negated && v[0].StartsWith('!')) + { + _ = IPAddress.TryParse(v[0][1..], out address); + } + else if (!negated) + { + _ = IPAddress.TryParse(v[0][0..], out address); + } + + if (address != IPAddress.None && address != null) + { + if (int.TryParse(v[1], out var netmask)) + { + result.Add(new IPNetwork(address, netmask)); + } + else if (address.AddressFamily == AddressFamily.InterNetwork) + { + result.Add(new IPNetwork(address, 32)); + } + else if (address.AddressFamily == AddressFamily.InterNetworkV6) + { + result.Add(new IPNetwork(address, 128)); + } + } + } + + if (result.Count > 0) + { + return true; + } + + return false; + } + /// /// Attempts to parse a host string. /// From 34d8e531e02c995836546e702e8dc4b02f2206e7 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 20 Jul 2022 09:48:20 +0200 Subject: [PATCH 010/858] Properly handle subnets in KnownProxies --- .../Extensions/ApiServiceCollectionExtensions.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 7030b726cd..5f9f50e315 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -182,7 +182,7 @@ namespace Jellyfin.Server.Extensions } /// - /// Extension method for adding the jellyfin API to the service collection. + /// Extension method for adding the Jellyfin API to the service collection. /// /// The service collection. /// An IEnumerable containing all plugin assemblies with API controllers. @@ -335,7 +335,7 @@ namespace Jellyfin.Server.Extensions } /// - /// Sets up the proxy configuration based on the addresses in . + /// Sets up the proxy configuration based on the addresses/subnets in . /// /// The containing the config settings. /// The string array to parse. @@ -348,6 +348,13 @@ namespace Jellyfin.Server.Extensions { AddIpAddress(config, options, addr, addr.AddressFamily == AddressFamily.InterNetwork ? 32 : 128); } + else if (NetworkExtensions.TryParseSubnets(new[] { allowedProxies[i] }, out var subnets)) + { + for (var j = 0; j < subnets.Count; j++) + { + AddIpAddress(config, options, subnets[j].Prefix, subnets[j].PrefixLength); + } + } else if (NetworkExtensions.TryParseHost(allowedProxies[i], out var host)) { foreach (var address in host) From 748907b9208aa91adc2dcf08672ea8eda7ed7c9c Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 20 Jul 2022 09:50:16 +0200 Subject: [PATCH 011/858] Remove workaround, this only applies to the IPs set by the middleware --- .../Extensions/ApiServiceCollectionExtensions.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 5f9f50e315..5073951067 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -372,14 +372,6 @@ namespace Jellyfin.Server.Extensions return; } - // In order for dual-mode sockets to be used, IP6 has to be enabled in JF and an interface has to have an IP6 address. - if (addr.AddressFamily == AddressFamily.InterNetwork && config.EnableIPV6) - { - // If the server is using dual-mode sockets, IPv4 addresses are supplied in an IPv6 format. - // https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-5.0 . - addr = addr.MapToIPv6(); - } - if (prefixLength == 32) { options.KnownProxies.Add(addr); From 2043a33f815d9a16aa819095e6310620ca4e72a2 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 20 Jul 2022 09:50:41 +0200 Subject: [PATCH 012/858] Small cleanup and logging fix --- Emby.Dlna/Main/DlnaEntryPoint.cs | 2 -- Jellyfin.Server/Program.cs | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index 01a9def0bf..7bc761b71c 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -3,10 +3,8 @@ #pragma warning disable CS1591 using System; -using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Net; using System.Net.Http; using System.Net.Sockets; using System.Threading.Tasks; diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 2bbc2546ed..438994851d 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -302,7 +302,7 @@ namespace Jellyfin.Server bool flagged = false; foreach (IPData netAdd in addresses) { - _logger.LogInformation("Kestrel listening on {Address}", netAdd.Address == IPAddress.IPv6Any ? "All Addresses" : netAdd); + _logger.LogInformation("Kestrel listening on {Address}", netAdd.Address == IPAddress.IPv6Any ? "All Addresses" : netAdd.Address); options.Listen(netAdd.Address, appHost.HttpPort); if (appHost.ListenWithHttps) { From a492082f4e015d6d38368c4ac05d39d236387214 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 20 Jul 2022 11:47:48 +0200 Subject: [PATCH 013/858] Apply review suggestions and fix build --- .../ApiServiceCollectionExtensions.cs | 5 +++++ MediaBrowser.Common/Net/IPData.cs | 17 +++++------------ MediaBrowser.Common/Net/NetworkExtensions.cs | 10 +++++----- .../Jellyfin.Server.Tests/ParseNetworkTests.cs | 8 ++++---- 4 files changed, 19 insertions(+), 21 deletions(-) diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 5073951067..a393b80db5 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -372,6 +372,11 @@ namespace Jellyfin.Server.Extensions return; } + if (addr.IsIPv4MappedToIPv6) + { + addr = addr.MapToIPv4(); + } + if (prefixLength == 32) { options.KnownProxies.Add(addr); diff --git a/MediaBrowser.Common/Net/IPData.cs b/MediaBrowser.Common/Net/IPData.cs index 3c6adc7e86..6901d6ad8a 100644 --- a/MediaBrowser.Common/Net/IPData.cs +++ b/MediaBrowser.Common/Net/IPData.cs @@ -14,13 +14,12 @@ namespace MediaBrowser.Common.Net /// /// An . /// The . - public IPData( - IPAddress address, - IPNetwork? subnet) + /// The object's name. + public IPData(IPAddress address, IPNetwork? subnet, string name) { Address = address; Subnet = subnet ?? (address.AddressFamily == AddressFamily.InterNetwork ? new IPNetwork(address, 32) : new IPNetwork(address, 128)); - Name = string.Empty; + Name = name; } /// @@ -28,15 +27,9 @@ namespace MediaBrowser.Common.Net /// /// An . /// The . - /// The object's name. - public IPData( - IPAddress address, - IPNetwork? subnet, - string name) + public IPData(IPAddress address, IPNetwork? subnet) + : this(address, subnet, string.Empty) { - Address = address; - Subnet = subnet ?? (address.AddressFamily == AddressFamily.InterNetwork ? new IPNetwork(address, 32) : new IPNetwork(address, 128)); - Name = name; } /// diff --git a/MediaBrowser.Common/Net/NetworkExtensions.cs b/MediaBrowser.Common/Net/NetworkExtensions.cs index 4cba1c99f2..ae1e47ccc2 100644 --- a/MediaBrowser.Common/Net/NetworkExtensions.cs +++ b/MediaBrowser.Common/Net/NetworkExtensions.cs @@ -13,6 +13,10 @@ namespace MediaBrowser.Common.Net /// public static class NetworkExtensions { + // Use regular expression as CheckHostName isn't RFC5892 compliant. + // Modified from gSkinner's expression at https://stackoverflow.com/questions/11809631/fully-qualified-domain-name-validation + private static readonly Regex _fqdnRegex = new Regex(@"(?im)^(?!:\/\/)(?=.{1,255}$)((.{1,63}\.){0,127}(?![0-9]*$)[a-z0-9-]+\.?)(:(\d){1,5}){0,1}$"); + /// /// Returns true if the IPAddress contains an IP6 Local link address. /// @@ -227,12 +231,8 @@ namespace MediaBrowser.Common.Net if (hosts.Length <= 2) { - // Use regular expression as CheckHostName isn't RFC5892 compliant. - // Modified from gSkinner's expression at https://stackoverflow.com/questions/11809631/fully-qualified-domain-name-validation - string pattern = @"(?im)^(?!:\/\/)(?=.{1,255}$)((.{1,63}\.){0,127}(?![0-9]*$)[a-z0-9-]+\.?)(:(\d){1,5}){0,1}$"; - // Is hostname or hostname:port - if (Regex.IsMatch(hosts[0], pattern)) + if (_fqdnRegex.IsMatch(hosts[0])) { try { diff --git a/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs index a1bdfa31b8..fc5f5f4c6c 100644 --- a/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs +++ b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs @@ -21,9 +21,9 @@ namespace Jellyfin.Server.Tests data.Add( true, true, - new string[] { "192.168.t", "127.0.0.1", "1234.1232.12.1234" }, - new IPAddress[] { IPAddress.Loopback.MapToIPv6() }, - Array.Empty()); + new string[] { "192.168.t", "127.0.0.1", "::1", "1234.1232.12.1234" }, + new IPAddress[] { IPAddress.Loopback, }, + new IPNetwork[] { new IPNetwork(IPAddress.IPv6Loopback, 128) }); data.Add( true, @@ -64,7 +64,7 @@ namespace Jellyfin.Server.Tests true, true, new string[] { "localhost" }, - new IPAddress[] { IPAddress.Loopback.MapToIPv6() }, + new IPAddress[] { IPAddress.Loopback }, new IPNetwork[] { new IPNetwork(IPAddress.IPv6Loopback, 128) }); return data; } From 2281b8c997dff0fa148bf0f193b37664420aca3e Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 20 Jul 2022 14:29:30 +0200 Subject: [PATCH 014/858] Move away from using Collection, simplify code, add proper ordering --- Emby.Dlna/Main/DlnaEntryPoint.cs | 2 +- .../Configuration/NetworkConfiguration.cs | 4 +- Jellyfin.Networking/Manager/NetworkManager.cs | 54 +++++++++---------- .../CreateNetworkConfiguration.cs | 2 +- MediaBrowser.Common/Net/INetworkManager.cs | 13 +++-- MediaBrowser.Common/Net/NetworkExtensions.cs | 6 +-- .../NetworkParseTests.cs | 8 ++- 7 files changed, 42 insertions(+), 47 deletions(-) diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index 7bc761b71c..90c985a816 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -293,7 +293,7 @@ namespace Emby.Dlna.Main if (bindAddresses.Count == 0) { // No interfaces returned, so use loopback. - bindAddresses = _networkManager.GetLoopbacks(); + bindAddresses = _networkManager.GetLoopbacks().ToList(); } foreach (var address in bindAddresses) diff --git a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs index 0ac55c986e..be8dc738d9 100644 --- a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs +++ b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs @@ -151,9 +151,9 @@ namespace Jellyfin.Networking.Configuration public bool IgnoreVirtualInterfaces { get; set; } = true; /// - /// Gets or sets a value indicating the interfaces that should be ignored. The list can be comma separated. . + /// Gets or sets a value indicating the interface name prefixes that should be ignored. The list can be comma separated and values are case-insensitive. . /// - public string VirtualInterfaceNames { get; set; } = "vEthernet*"; + public string VirtualInterfaceNames { get; set; } = "veth"; /// /// Gets or sets the time (in seconds) between the pings of SSDP gateway monitor. diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index e9e3e18022..757b5994b9 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.Net; @@ -47,7 +46,7 @@ namespace Jellyfin.Networking.Manager /// private readonly Dictionary _publishedServerUrls; - private Collection _remoteAddressFilter; + private List _remoteAddressFilter; /// /// Used to stop "event-racing conditions". @@ -58,12 +57,12 @@ namespace Jellyfin.Networking.Manager /// Unfiltered user defined LAN subnets () /// or internal interface network subnets if undefined by user. /// - private Collection _lanSubnets; + private List _lanSubnets; /// /// User defined list of subnets to excluded from the LAN. /// - private Collection _excludedSubnets; + private List _excludedSubnets; /// /// List of interfaces to bind to. @@ -95,7 +94,7 @@ namespace Jellyfin.Networking.Manager _macAddresses = new List(); _publishedServerUrls = new Dictionary(); _eventFireLock = new object(); - _remoteAddressFilter = new Collection(); + _remoteAddressFilter = new List(); UpdateSettings(_configurationManager.GetNetworkConfiguration()); @@ -225,8 +224,8 @@ namespace Jellyfin.Networking.Manager { try { - IPInterfaceProperties ipProperties = adapter.GetIPProperties(); - PhysicalAddress mac = adapter.GetPhysicalAddress(); + var ipProperties = adapter.GetIPProperties(); + var mac = adapter.GetPhysicalAddress(); // Populate MAC list if (adapter.NetworkInterfaceType != NetworkInterfaceType.Loopback && PhysicalAddress.None.Equals(mac)) @@ -235,7 +234,7 @@ namespace Jellyfin.Networking.Manager } // Populate interface list - foreach (UnicastIPAddressInformation info in ipProperties.UnicastAddresses) + foreach (var info in ipProperties.UnicastAddresses) { if (IsIpv4Enabled && info.Address.AddressFamily == AddressFamily.InterNetwork) { @@ -364,8 +363,8 @@ namespace Jellyfin.Networking.Manager // Use explicit bind addresses if (config.LocalNetworkAddresses.Length > 0) { - _bindAddresses = config.LocalNetworkAddresses.Select(p => IPAddress.TryParse(p, out var address) - ? address + _bindAddresses = config.LocalNetworkAddresses.Select(p => IPAddress.TryParse(p, out var addresses) + ? addresses : (_interfaces.Where(x => x.Name.Equals(p, StringComparison.OrdinalIgnoreCase)).Select(x => x.Address).FirstOrDefault() ?? IPAddress.None)).ToList(); _bindAddresses.RemoveAll(x => x == IPAddress.None); } @@ -525,13 +524,11 @@ namespace Jellyfin.Networking.Manager var address = IPAddress.Parse(split[0]); var network = new IPNetwork(address, int.Parse(split[1], CultureInfo.InvariantCulture)); var index = int.Parse(parts[1], CultureInfo.InvariantCulture); - if (address.AddressFamily == AddressFamily.InterNetwork) + if (address.AddressFamily == AddressFamily.InterNetwork || address.AddressFamily == AddressFamily.InterNetworkV6) { - _interfaces.Add(new IPData(address, network, parts[2])); - } - else if (address.AddressFamily == AddressFamily.InterNetworkV6) - { - _interfaces.Add(new IPData(address, network, parts[2])); + var data = new IPData(address, network, parts[2]); + data.Index = index; + _interfaces.Add(data); } } } @@ -562,9 +559,9 @@ namespace Jellyfin.Networking.Manager } /// - public bool TryParseInterface(string intf, out Collection result) + public bool TryParseInterface(string intf, out List result) { - result = new Collection(); + result = new List(); if (string.IsNullOrEmpty(intf)) { return false; @@ -573,7 +570,7 @@ namespace Jellyfin.Networking.Manager if (_interfaces != null) { // Match all interfaces starting with names starting with token - var matchedInterfaces = _interfaces.Where(s => s.Name.Equals(intf.ToLowerInvariant(), StringComparison.OrdinalIgnoreCase)); + var matchedInterfaces = _interfaces.Where(s => s.Name.Equals(intf.ToLowerInvariant(), StringComparison.OrdinalIgnoreCase)).OrderBy(x => x.Index); if (matchedInterfaces.Any()) { _logger.LogInformation("Interface {Token} used in settings. Using its interface addresses.", intf); @@ -626,14 +623,14 @@ namespace Jellyfin.Networking.Manager } /// - public IReadOnlyCollection GetMacAddresses() + public IReadOnlyList GetMacAddresses() { // Populated in construction - so always has values. return _macAddresses; } /// - public List GetLoopbacks() + public IReadOnlyList GetLoopbacks() { var loopbackNetworks = new List(); if (IsIpv4Enabled) @@ -650,7 +647,7 @@ namespace Jellyfin.Networking.Manager } /// - public List GetAllBindInterfaces(bool individualInterfaces = false) + public IReadOnlyList GetAllBindInterfaces(bool individualInterfaces = false) { if (_bindAddresses.Count == 0) { @@ -816,7 +813,7 @@ namespace Jellyfin.Networking.Manager } /// - public List GetInternalBindAddresses() + public IReadOnlyList GetInternalBindAddresses() { if (_bindAddresses.Count == 0) { @@ -833,7 +830,8 @@ namespace Jellyfin.Networking.Manager // Select all local bind addresses return _interfaces.Where(x => _bindAddresses.Contains(x.Address)) .Where(x => IsInLocalNetwork(x.Address)) - .OrderBy(x => x.Index).ToList(); + .OrderBy(x => x.Index) + .ToList(); } /// @@ -892,7 +890,7 @@ namespace Jellyfin.Networking.Manager interfaces = interfaces.Where(x => IsInLocalNetwork(x.Address)).ToList(); } - foreach (var intf in _interfaces) + foreach (var intf in interfaces) { if (intf.Subnet.Contains(address)) { @@ -942,7 +940,7 @@ namespace Jellyfin.Networking.Manager foreach (var data in validPublishedServerUrls) { // Get address interface - var intf = _interfaces.FirstOrDefault(s => s.Subnet.Contains(data.Key.Address)); + var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(s => s.Subnet.Contains(data.Key.Address)); // Remaining. Match anything. if (data.Key.Address.Equals(IPAddress.Broadcast)) @@ -1017,7 +1015,7 @@ namespace Jellyfin.Networking.Manager defaultGateway = addr; } - var intf = _interfaces.Where(x => x.Subnet.Contains(addr)).FirstOrDefault(); + var intf = _interfaces.Where(x => x.Subnet.Contains(addr)).OrderBy(x => x.Index).FirstOrDefault(); if (bindAddress == null && intf != null && intf.Subnet.Contains(source)) { @@ -1082,7 +1080,7 @@ namespace Jellyfin.Networking.Manager { result = string.Empty; // Get the first WAN interface address that isn't a loopback. - var extResult = _interfaces.Where(p => !IsInLocalNetwork(p.Address)); + var extResult = _interfaces.Where(p => !IsInLocalNetwork(p.Address)).OrderBy(x => x.Index); IPAddress? hasResult = null; // Does the request originate in one of the interface subnets? diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs index ceeaa26e62..b670172814 100644 --- a/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs +++ b/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs @@ -114,7 +114,7 @@ public class CreateNetworkConfiguration : IMigrationRoutine public bool IgnoreVirtualInterfaces { get; set; } = true; - public string VirtualInterfaceNames { get; set; } = "veth*"; + public string VirtualInterfaceNames { get; set; } = "veth"; public string[] PublishedServerUriBySubnet { get; set; } = Array.Empty(); diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs index 9f48535576..bb0e2dcb36 100644 --- a/MediaBrowser.Common/Net/INetworkManager.cs +++ b/MediaBrowser.Common/Net/INetworkManager.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Net; using System.Net.NetworkInformation; using Microsoft.AspNetCore.Http; @@ -34,13 +33,13 @@ namespace MediaBrowser.Common.Net /// If all the interfaces are specified, and none are excluded, it returns zero items /// to represent any address. /// When false, return or for all interfaces. - List GetAllBindInterfaces(bool individualInterfaces = false); + IReadOnlyList GetAllBindInterfaces(bool individualInterfaces = false); /// - /// Returns a collection containing the loopback interfaces. + /// Returns a list containing the loopback interfaces. /// /// List{IPData}. - List GetLoopbacks(); + IReadOnlyList GetLoopbacks(); /// /// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo) @@ -97,7 +96,7 @@ namespace MediaBrowser.Common.Net /// Get a list of all the MAC addresses associated with active interfaces. /// /// List of MAC addresses. - IReadOnlyCollection GetMacAddresses(); + IReadOnlyList GetMacAddresses(); /// /// Returns true if the address is part of the user defined LAN. @@ -122,13 +121,13 @@ namespace MediaBrowser.Common.Net /// Interface name. /// Resultant object's ip addresses, if successful. /// Success of the operation. - bool TryParseInterface(string intf, out Collection? result); + bool TryParseInterface(string intf, out List? result); /// /// Returns all the internal Bind interface addresses. /// /// An internal list of interfaces addresses. - List GetInternalBindAddresses(); + IReadOnlyList GetInternalBindAddresses(); /// /// Checks to see if has access. diff --git a/MediaBrowser.Common/Net/NetworkExtensions.cs b/MediaBrowser.Common/Net/NetworkExtensions.cs index ae1e47ccc2..316c2ebcd1 100644 --- a/MediaBrowser.Common/Net/NetworkExtensions.cs +++ b/MediaBrowser.Common/Net/NetworkExtensions.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.HttpOverrides; using System; -using System.Collections.ObjectModel; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.Sockets; @@ -148,9 +148,9 @@ namespace MediaBrowser.Common.Net /// Collection of . /// Boolean signaling if negated or not negated values should be parsed. /// True if parsing was successful. - public static bool TryParseSubnets(string[] values, out Collection result, bool negated = false) + public static bool TryParseSubnets(string[] values, out List result, bool negated = false) { - result = new Collection(); + result = new List(); if (values == null || values.Length == 0) { diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs index 5b9739dd3d..c45a9c9f54 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs @@ -1,5 +1,5 @@ using System; -using System.Collections.ObjectModel; +using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net; @@ -49,8 +49,6 @@ namespace Jellyfin.Networking.Tests { EnableIPV6 = true, EnableIPV4 = true, - IgnoreVirtualInterfaces = true, - VirtualInterfaceNames = "veth", LocalNetworkSubnets = lan?.Split(';') ?? throw new ArgumentNullException(nameof(lan)) }; @@ -208,7 +206,7 @@ namespace Jellyfin.Networking.Tests using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); NetworkManager.MockNetworkSettings = string.Empty; - _ = nm.TryParseInterface(result, out Collection? resultObj); + _ = nm.TryParseInterface(result, out List? resultObj); // Check to see if dns resolution is working. If not, skip test. _ = NetworkExtensions.TryParseHost(source, out var host); @@ -277,7 +275,7 @@ namespace Jellyfin.Networking.Tests using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); NetworkManager.MockNetworkSettings = string.Empty; - if (nm.TryParseInterface(result, out Collection? resultObj) && resultObj != null) + if (nm.TryParseInterface(result, out List? resultObj) && resultObj != null) { // Parse out IPAddresses so we can do a string comparison. (Ignore subnet masks). result = resultObj.First().Address.ToString(); From 8075cb4e99da4468d9474d1ad2e7668d96cd5224 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 20 Jul 2022 16:14:56 +0200 Subject: [PATCH 015/858] Cleanup and sort NetworkConfiguration --- .../Configuration/NetworkConfiguration.cs | 217 +++++++++--------- 1 file changed, 106 insertions(+), 111 deletions(-) diff --git a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs index be8dc738d9..e9f6d597b4 100644 --- a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs +++ b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs @@ -21,21 +21,6 @@ namespace Jellyfin.Networking.Configuration private string _baseUrl = string.Empty; - /// - /// Gets or sets a value indicating whether the server should force connections over HTTPS. - /// - public bool RequireHttps { get; set; } - - /// - /// Gets or sets the filesystem path of an X.509 certificate to use for SSL. - /// - public string CertificatePath { get; set; } = string.Empty; - - /// - /// Gets or sets the password required to access the X.509 certificate data in the file specified by . - /// - public string CertificatePassword { get; set; } = string.Empty; - /// /// Gets or sets a value used to specify the URL prefix that your Jellyfin instance can be accessed at. /// @@ -69,6 +54,36 @@ namespace Jellyfin.Networking.Configuration } } + /// + /// Gets or sets a value indicating whether to use HTTPS. + /// + /// + /// In order for HTTPS to be used, in addition to setting this to true, valid values must also be + /// provided for and . + /// + public bool EnableHttps { get; set; } + + /// + /// Gets or sets a value indicating whether the server should force connections over HTTPS. + /// + public bool RequireHttps { get; set; } + + /// + /// Gets or sets the filesystem path of an X.509 certificate to use for SSL. + /// + public string CertificatePath { get; set; } = string.Empty; + + /// + /// Gets or sets the password required to access the X.509 certificate data in the file specified by . + /// + public string CertificatePassword { get; set; } = string.Empty; + + /// + /// Gets or sets the HTTPS server port number. + /// + /// The HTTPS server port number. + public int HttpsPortNumber { get; set; } = DefaultHttpsPort; + /// /// Gets or sets the public HTTPS port. /// @@ -81,21 +96,6 @@ namespace Jellyfin.Networking.Configuration /// The HTTP server port number. public int HttpServerPortNumber { get; set; } = DefaultHttpPort; - /// - /// Gets or sets the HTTPS server port number. - /// - /// The HTTPS server port number. - public int HttpsPortNumber { get; set; } = DefaultHttpsPort; - - /// - /// Gets or sets a value indicating whether to use HTTPS. - /// - /// - /// In order for HTTPS to be used, in addition to setting this to true, valid values must also be - /// provided for and . - /// - public bool EnableHttps { get; set; } - /// /// Gets or sets the public mapped port. /// @@ -108,99 +108,30 @@ namespace Jellyfin.Networking.Configuration public bool UPnPCreateHttpPortMap { get; set; } /// - /// Gets or sets the UDPPortRange. + /// Gets or sets a value indicating whether Autodiscovery is enabled. /// - public string UDPPortRange { get; set; } = string.Empty; - - /// - /// Gets or sets a value indicating whether IPv6 is enabled or not. - /// - public bool EnableIPV6 { get; set; } - - /// - /// Gets or sets a value indicating whether IPv6 is enabled or not. - /// - public bool EnableIPV4 { get; set; } = true; - - /// - /// Gets or sets a value indicating whether detailed SSDP logs are sent to the console/log. - /// "Emby.Dlna": "Debug" must be set in logging.default.json for this property to have any effect. - /// - public bool EnableSSDPTracing { get; set; } - - /// - /// Gets or sets the SSDPTracingFilter - /// Gets or sets a value indicating whether an IP address is to be used to filter the detailed ssdp logs that are being sent to the console/log. - /// If the setting "Emby.Dlna": "Debug" msut be set in logging.default.json for this property to work. - /// - public string SSDPTracingFilter { get; set; } = string.Empty; - - /// - /// Gets or sets the number of times SSDP UDP messages are sent. - /// - public int UDPSendCount { get; set; } = 2; - - /// - /// Gets or sets the delay between each groups of SSDP messages (in ms). - /// - public int UDPSendDelay { get; set; } = 100; - - /// - /// Gets or sets a value indicating whether address names that match should be Ignore for the purposes of binding. - /// - public bool IgnoreVirtualInterfaces { get; set; } = true; - - /// - /// Gets or sets a value indicating the interface name prefixes that should be ignored. The list can be comma separated and values are case-insensitive. . - /// - public string VirtualInterfaceNames { get; set; } = "veth"; - - /// - /// Gets or sets the time (in seconds) between the pings of SSDP gateway monitor. - /// - public int GatewayMonitorPeriod { get; set; } = 60; - - /// - /// Gets a value indicating whether multi-socket binding is available. - /// - public bool EnableMultiSocketBinding { get; } = true; - - /// - /// Gets or sets the ports that HDHomerun uses. - /// - public string HDHomerunPortRange { get; set; } = string.Empty; - - /// - /// Gets or sets the PublishedServerUriBySubnet - /// Gets or sets PublishedServerUri to advertise for specific subnets. - /// - public string[] PublishedServerUriBySubnet { get; set; } = Array.Empty(); + public bool AutoDiscovery { get; set; } = true; /// /// Gets or sets a value indicating whether Autodiscovery tracing is enabled. /// public bool AutoDiscoveryTracing { get; set; } - /// - /// Gets or sets a value indicating whether Autodiscovery is enabled. - /// - public bool AutoDiscovery { get; set; } = true; - - /// - /// Gets or sets the filter for remote IP connectivity. Used in conjuntion with . - /// - public string[] RemoteIPFilter { get; set; } = Array.Empty(); - - /// - /// Gets or sets a value indicating whether contains a blacklist or a whitelist. Default is a whitelist. - /// - public bool IsRemoteIPFilterBlacklist { get; set; } - /// /// Gets or sets a value indicating whether to enable automatic port forwarding. /// public bool EnableUPnP { get; set; } + /// + /// Gets or sets a value indicating whether IPv6 is enabled or not. + /// + public bool EnableIPV4 { get; set; } = true; + + /// + /// Gets or sets a value indicating whether IPv6 is enabled or not. + /// + public bool EnableIPV6 { get; set; } + /// /// Gets or sets a value indicating whether access outside of the LAN is permitted. /// @@ -221,9 +152,73 @@ namespace Jellyfin.Networking.Configuration /// public string[] KnownProxies { get; set; } = Array.Empty(); + /// + /// Gets or sets the UDPPortRange. + /// + public string UDPPortRange { get; set; } = string.Empty; + + /// + /// Gets or sets the number of times SSDP UDP messages are sent. + /// + public int UDPSendCount { get; set; } = 2; + + /// + /// Gets or sets the delay between each groups of SSDP messages (in ms). + /// + public int UDPSendDelay { get; set; } = 100; + + /// + /// Gets or sets a value indicating whether address names that match should be Ignore for the purposes of binding. + /// + public bool IgnoreVirtualInterfaces { get; set; } = true; + + /// + /// Gets or sets a value indicating the interface name prefixes that should be ignored. The list can be comma separated and values are case-insensitive. . + /// + public string VirtualInterfaceNames { get; set; } = "veth"; + /// /// Gets or sets a value indicating whether the published server uri is based on information in HTTP requests. /// public bool EnablePublishedServerUriByRequest { get; set; } = false; + + /// + /// Gets or sets the PublishedServerUriBySubnet + /// Gets or sets PublishedServerUri to advertise for specific subnets. + /// + public string[] PublishedServerUriBySubnet { get; set; } = Array.Empty(); + + /// + /// Gets a value indicating whether multi-socket binding is available. + /// + public bool EnableMultiSocketBinding { get; } = true; + + /// + /// Gets or sets the ports that HDHomerun uses. + /// + public string HDHomerunPortRange { get; set; } = string.Empty; + + /// + /// Gets or sets the filter for remote IP connectivity. Used in conjuntion with . + /// + public string[] RemoteIPFilter { get; set; } = Array.Empty(); + + /// + /// Gets or sets a value indicating whether contains a blacklist or a whitelist. Default is a whitelist. + /// + public bool IsRemoteIPFilterBlacklist { get; set; } + + /// + /// Gets or sets a value indicating whether detailed SSDP logs are sent to the console/log. + /// "Emby.Dlna": "Debug" must be set in logging.default.json for this property to have any effect. + /// + public bool EnableSSDPTracing { get; set; } + + /// + /// Gets or sets the SSDPTracingFilter + /// Gets or sets a value indicating whether an IP address is to be used to filter the detailed ssdp logs that are being sent to the console/log. + /// If the setting "Emby.Dlna": "Debug" msut be set in logging.default.json for this property to work. + /// + public string SSDPTracingFilter { get; set; } = string.Empty; } } From 2d3a16ad0f814c4e26a741744b8fc7f3c31890dc Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 20 Jul 2022 21:19:35 +0200 Subject: [PATCH 016/858] Simplify code --- Jellyfin.Networking/Manager/NetworkManager.cs | 267 +++++------------- MediaBrowser.Common/Net/INetworkManager.cs | 2 +- 2 files changed, 72 insertions(+), 197 deletions(-) diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 757b5994b9..50fc974616 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -25,11 +25,6 @@ namespace Jellyfin.Networking.Manager /// private readonly object _initLock; - /// - /// Dictionary containing interface addresses and their subnets. - /// - private readonly List _interfaces; - /// /// List of all interface MAC addresses. /// @@ -53,6 +48,11 @@ namespace Jellyfin.Networking.Manager /// private bool _eventfire; + /// + /// Dictionary containing interface addresses and their subnets. + /// + private List _interfaces; + /// /// Unfiltered user defined LAN subnets () /// or internal interface network subnets if undefined by user. @@ -64,16 +64,6 @@ namespace Jellyfin.Networking.Manager /// private List _excludedSubnets; - /// - /// List of interfaces to bind to. - /// - private List _bindAddresses; - - /// - /// List of interface addresses to exclude from bind. - /// - private List _bindExclusions; - /// /// True if this object is disposed. /// @@ -190,9 +180,10 @@ namespace Jellyfin.Networking.Manager try { await Task.Delay(2000).ConfigureAwait(false); + var networkConfig = _configurationManager.GetNetworkConfiguration(); + InitialiseLan(networkConfig); InitialiseInterfaces(); - // Recalculate LAN caches. - InitialiseLan(_configurationManager.GetNetworkConfiguration()); + EnforceBindRestrictions(networkConfig); NetworkChanged?.Invoke(this, EventArgs.Empty); } @@ -217,7 +208,7 @@ namespace Jellyfin.Networking.Manager try { - IEnumerable nics = NetworkInterface.GetAllNetworkInterfaces() + var nics = NetworkInterface.GetAllNetworkInterfaces() .Where(i => i.SupportsMulticast && i.OperationalStatus == OperationalStatus.Up); foreach (NetworkInterface adapter in nics) @@ -270,33 +261,7 @@ namespace Jellyfin.Networking.Manager _logger.LogError(ex, "Error obtaining interfaces."); } - // If for some reason we don't have an interface info, resolve the DNS name. - if (_interfaces.Count == 0) - { - _logger.LogError("No interfaces information available. Resolving DNS name."); - var hostName = Dns.GetHostName(); - if (Uri.CheckHostName(hostName).Equals(UriHostNameType.Dns)) - { - try - { - IPHostEntry hip = Dns.GetHostEntry(hostName); - foreach (var address in hip.AddressList) - { - _interfaces.Add(new IPData(address, null)); - } - } - catch (SocketException ex) - { - // Log and then ignore socket errors, as the result value will just be an empty array. - _logger.LogWarning("GetHostEntryAsync failed with {Message}.", ex.Message); - } - } - - if (_interfaces.Count == 0) - { - _logger.LogWarning("No interfaces information available. Using loopback."); - } - } + _logger.LogWarning("No interfaces information available. Using loopback."); if (IsIpv4Enabled && !IsIpv6Enabled) { @@ -354,55 +319,46 @@ namespace Jellyfin.Networking.Manager } /// - /// Initialises the network bind addresses. + /// Enforce bind addresses and exclusions on available interfaces. /// - private void InitialiseBind(NetworkConfiguration config) + private void EnforceBindRestrictions(NetworkConfiguration config) { lock (_initLock) { - // Use explicit bind addresses - if (config.LocalNetworkAddresses.Length > 0) + // Respect explicit bind addresses + var localNetworkAddresses = config.LocalNetworkAddresses; + if (localNetworkAddresses.Length > 0 && !string.IsNullOrWhiteSpace(localNetworkAddresses.First())) { - _bindAddresses = config.LocalNetworkAddresses.Select(p => IPAddress.TryParse(p, out var addresses) + var bindAddresses = config.LocalNetworkAddresses.Select(p => IPAddress.TryParse(p, out var addresses) ? addresses - : (_interfaces.Where(x => x.Name.Equals(p, StringComparison.OrdinalIgnoreCase)).Select(x => x.Address).FirstOrDefault() ?? IPAddress.None)).ToList(); - _bindAddresses.RemoveAll(x => x == IPAddress.None); - } - else - { - // Use all addresses from all interfaces - _bindAddresses = _interfaces.Select(x => x.Address).ToList(); + : (_interfaces.Where(x => x.Name.Equals(p, StringComparison.OrdinalIgnoreCase)) + .Select(x => x.Address) + .FirstOrDefault() ?? IPAddress.None)) + .ToList(); + bindAddresses.RemoveAll(x => x == IPAddress.None); + _interfaces = _interfaces.Where(x => bindAddresses.Contains(x.Address)).ToList(); } - _bindExclusions = new List(); - - // Add all interfaces matching any virtual machine interface prefix to _bindExclusions + // Remove all interfaces matching any virtual machine interface prefix if (config.IgnoreVirtualInterfaces) { // Remove potentially exisiting * and split config string into prefixes - var virtualInterfacePrefixes = config.VirtualInterfaceNames.Replace("*", string.Empty, StringComparison.OrdinalIgnoreCase).ToLowerInvariant().Split(','); + var virtualInterfacePrefixes = config.VirtualInterfaceNames + .Replace("*", string.Empty, StringComparison.OrdinalIgnoreCase) + .ToLowerInvariant() + .Split(','); - // Check all interfaces for matches against the prefixes and add the interface IPs to _bindExclusions - if (_bindAddresses.Count > 0 && virtualInterfacePrefixes.Length > 0) + // Check all interfaces for matches against the prefixes and remove them + if (_interfaces.Count > 0 && virtualInterfacePrefixes.Length > 0) { - var localInterfaces = _interfaces.ToList(); foreach (var virtualInterfacePrefix in virtualInterfacePrefixes) { - var excludedInterfaceIps = localInterfaces.Where(intf => intf.Name.StartsWith(virtualInterfacePrefix, StringComparison.OrdinalIgnoreCase)) - .Select(intf => intf.Address); - foreach (var interfaceIp in excludedInterfaceIps) - { - _bindExclusions.Add(interfaceIp); - } + _interfaces.RemoveAll(x => x.Name.StartsWith(virtualInterfacePrefix, StringComparison.OrdinalIgnoreCase)); } } } - // Remove all excluded addresses from _bindAddresses - _bindAddresses.RemoveAll(x => _bindExclusions.Contains(x)); - - _logger.LogInformation("Using bind addresses: {0}", _bindAddresses); - _logger.LogInformation("Using bind exclusions: {0}", _bindExclusions); + _logger.LogInformation("Using bind addresses: {0}", _interfaces.Select(x => x.Address)); } } @@ -477,7 +433,7 @@ namespace Jellyfin.Networking.Manager _publishedServerUrls[data] = replacement; } - else if (TryParseInterface(parts[0], out var ifaces)) + else if (TryParseInterface(ipParts[0], out var ifaces)) { foreach (var iface in ifaces) { @@ -509,6 +465,9 @@ namespace Jellyfin.Networking.Manager { NetworkConfiguration config = (NetworkConfiguration)configuration ?? throw new ArgumentNullException(nameof(configuration)); + InitialiseLan(config); + InitialiseRemote(config); + if (string.IsNullOrEmpty(MockNetworkSettings)) { InitialiseInterfaces(); @@ -533,9 +492,7 @@ namespace Jellyfin.Networking.Manager } } - InitialiseLan(config); - InitialiseBind(config); - InitialiseRemote(config); + EnforceBindRestrictions(config); InitialiseOverrides(config); } @@ -649,19 +606,8 @@ namespace Jellyfin.Networking.Manager /// public IReadOnlyList GetAllBindInterfaces(bool individualInterfaces = false) { - if (_bindAddresses.Count == 0) + if (_interfaces.Count == 0) { - if (_bindExclusions.Count > 0) - { - foreach (var exclusion in _bindExclusions) - { - // Return all the interfaces except the ones specifically excluded. - _interfaces.RemoveAll(intf => intf.Address == exclusion); - } - - return _interfaces; - } - // No bind address and no exclusions, so listen on all interfaces. var result = new List(); @@ -699,14 +645,7 @@ namespace Jellyfin.Networking.Manager return result; } - // Remove any excluded bind interfaces. - foreach (var exclusion in _bindExclusions) - { - // Return all the interfaces except the ones specifically excluded. - _bindAddresses.Remove(exclusion); - } - - return _bindAddresses.Select(s => new IPData(s, null)).ToList(); + return _interfaces; } /// @@ -770,8 +709,7 @@ namespace Jellyfin.Networking.Manager // Get the first LAN interface address that's not excluded and not a loopback address. var availableInterfaces = _interfaces.Where(x => !IPAddress.IsLoopback(x.Address)) - .OrderByDescending(x => _bindAddresses.Contains(x.Address)) - .ThenByDescending(x => IsInLocalNetwork(x.Address)) + .OrderByDescending(x => IsInLocalNetwork(x.Address)) .ThenBy(x => x.Index); if (availableInterfaces.Any()) @@ -815,21 +753,8 @@ namespace Jellyfin.Networking.Manager /// public IReadOnlyList GetInternalBindAddresses() { - if (_bindAddresses.Count == 0) - { - if (_bindExclusions.Count > 0) - { - // Return all the internal interfaces except the ones excluded. - return _interfaces.Where(p => !_bindExclusions.Contains(p.Address)).ToList(); - } - - // No bind address, so return all internal interfaces. - return _interfaces; - } - // Select all local bind addresses - return _interfaces.Where(x => _bindAddresses.Contains(x.Address)) - .Where(x => IsInLocalNetwork(x.Address)) + return _interfaces.Where(x => IsInLocalNetwork(x.Address)) .OrderBy(x => x.Index) .ToList(); } @@ -876,31 +801,6 @@ namespace Jellyfin.Networking.Manager return address.Equals(IPAddress.Loopback) || address.Equals(IPAddress.IPv6Loopback) || match; } - private IPData? FindInterfaceForIp(IPAddress address, bool localNetwork = false) - { - if (address == null) - { - throw new ArgumentNullException(nameof(address)); - } - - var interfaces = _interfaces; - - if (localNetwork) - { - interfaces = interfaces.Where(x => IsInLocalNetwork(x.Address)).ToList(); - } - - foreach (var intf in interfaces) - { - if (intf.Subnet.Contains(address)) - { - return intf; - } - } - - return null; - } - private bool CheckIfLanAndNotExcluded(IPAddress address) { bool match = false; @@ -992,79 +892,54 @@ namespace Jellyfin.Networking.Manager { result = string.Empty; - int count = _bindAddresses.Count; - if (count == 1 && (_bindAddresses[0].Equals(IPAddress.Any) || _bindAddresses[0].Equals(IPAddress.IPv6Any))) + int count = _interfaces.Count(); + if (count == 1 && (_interfaces[0].Equals(IPAddress.Any) || _interfaces[0].Equals(IPAddress.IPv6Any))) { // Ignore IPAny addresses. count = 0; } - if (count != 0) + if (count > 0) { - // Check to see if any of the bind interfaces are in the same subnet as the source. - IPAddress? defaultGateway = null; IPAddress? bindAddress = null; + var externalInterfaces = _interfaces.Where(x => !IsInLocalNetwork(x.Address)) + .OrderBy(x => x.Index) + .ToList(); - if (isInExternalSubnet) + if (isInExternalSubnet && externalInterfaces.Any()) { - // Find all external bind addresses. Store the default gateway, but check to see if there is a better match first. - foreach (var addr in _bindAddresses) + // Check to see if any of the external bind interfaces are in the same subnet as the source. + // If none exists, this will select the first external interface if there is one. + bindAddress = externalInterfaces + .OrderByDescending(x => x.Subnet.Contains(source)) + .ThenBy(x => x.Index) + .Select(x => x.Address) + .FirstOrDefault(); + + if (bindAddress != null) { - if (defaultGateway == null && !IsInLocalNetwork(addr)) - { - defaultGateway = addr; - } - - var intf = _interfaces.Where(x => x.Subnet.Contains(addr)).OrderBy(x => x.Index).FirstOrDefault(); - - if (bindAddress == null && intf != null && intf.Subnet.Contains(source)) - { - bindAddress = intf.Address; - } - - if (defaultGateway != null && bindAddress != null) - { - break; - } + result = NetworkExtensions.FormatIpString(bindAddress); + _logger.LogDebug("{Source}: GetBindInterface: Has source, found a matching external bind interface. {Result}", source, result); + return true; } } else { - // Look for the best internal address. - foreach (var bA in _bindAddresses.Where(x => IsInLocalNetwork(x))) + // Check to see if any of the internal bind interfaces are in the same subnet as the source. + // If none exists, this will select the first internal interface if there is one. + bindAddress = _interfaces.Where(x => IsInLocalNetwork(x.Address)) + .OrderByDescending(x => x.Subnet.Contains(source)) + .ThenBy(x => x.Index) + .Select(x => x.Address) + .FirstOrDefault(); + + if (bindAddress != null) { - var intf = FindInterfaceForIp(source, true); - if (intf != null) - { - bindAddress = intf.Address; - break; - } + result = NetworkExtensions.FormatIpString(bindAddress); + _logger.LogWarning("{Source}: External request received, only an internal interface bind found. {Result}", source, result); + return true; } } - - if (bindAddress != null) - { - result = NetworkExtensions.FormatIpString(bindAddress); - _logger.LogDebug("{Source}: GetBindInterface: Has source, found a matching bind interface subnet. {Result}", source, result); - return true; - } - - if (isInExternalSubnet && defaultGateway != null) - { - result = NetworkExtensions.FormatIpString(defaultGateway); - _logger.LogDebug("{Source}: GetBindInterface: Using first user defined external interface. {Result}", source, result); - return true; - } - - result = NetworkExtensions.FormatIpString(_bindAddresses[0]); - _logger.LogDebug("{Source}: GetBindInterface: Selected first user defined interface. {Result}", source, result); - - if (isInExternalSubnet) - { - _logger.LogWarning("{Source}: External request received, only an internal interface bind found.", source); - } - - return true; } return false; diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs index bb0e2dcb36..21ff982376 100644 --- a/MediaBrowser.Common/Net/INetworkManager.cs +++ b/MediaBrowser.Common/Net/INetworkManager.cs @@ -124,7 +124,7 @@ namespace MediaBrowser.Common.Net bool TryParseInterface(string intf, out List? result); /// - /// Returns all the internal Bind interface addresses. + /// Returns all the internal bind interface addresses. /// /// An internal list of interfaces addresses. IReadOnlyList GetInternalBindAddresses(); From 358642c2d9b0475cd7e4ed18e9507cc87c15935d Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 20 Jul 2022 21:51:12 +0200 Subject: [PATCH 017/858] Fix build, fix loopback binding, exclude unsupported IPs from bind --- Jellyfin.Networking/Manager/NetworkManager.cs | 49 ++++++++++++++----- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 50fc974616..af2429cc28 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -183,7 +183,7 @@ namespace Jellyfin.Networking.Manager var networkConfig = _configurationManager.GetNetworkConfiguration(); InitialiseLan(networkConfig); InitialiseInterfaces(); - EnforceBindRestrictions(networkConfig); + EnforceBindSettings(networkConfig); NetworkChanged?.Invoke(this, EventArgs.Empty); } @@ -261,16 +261,19 @@ namespace Jellyfin.Networking.Manager _logger.LogError(ex, "Error obtaining interfaces."); } - _logger.LogWarning("No interfaces information available. Using loopback."); - - if (IsIpv4Enabled && !IsIpv6Enabled) + if (_interfaces.Count == 0) { - _interfaces.Add(new IPData(IPAddress.Loopback, new IPNetwork(IPAddress.Loopback, 8), "lo")); - } + _logger.LogWarning("No interfaces information available. Using loopback."); - if (!IsIpv4Enabled && IsIpv6Enabled) - { - _interfaces.Add(new IPData(IPAddress.IPv6Loopback, new IPNetwork(IPAddress.IPv6Loopback, 128), "lo")); + if (IsIpv4Enabled && !IsIpv6Enabled) + { + _interfaces.Add(new IPData(IPAddress.Loopback, new IPNetwork(IPAddress.Loopback, 8), "lo")); + } + + if (!IsIpv4Enabled && IsIpv6Enabled) + { + _interfaces.Add(new IPData(IPAddress.IPv6Loopback, new IPNetwork(IPAddress.IPv6Loopback, 128), "lo")); + } } _logger.LogDebug("Discovered {0} interfaces.", _interfaces.Count); @@ -321,7 +324,7 @@ namespace Jellyfin.Networking.Manager /// /// Enforce bind addresses and exclusions on available interfaces. /// - private void EnforceBindRestrictions(NetworkConfiguration config) + private void EnforceBindSettings(NetworkConfiguration config) { lock (_initLock) { @@ -329,7 +332,7 @@ namespace Jellyfin.Networking.Manager var localNetworkAddresses = config.LocalNetworkAddresses; if (localNetworkAddresses.Length > 0 && !string.IsNullOrWhiteSpace(localNetworkAddresses.First())) { - var bindAddresses = config.LocalNetworkAddresses.Select(p => IPAddress.TryParse(p, out var addresses) + var bindAddresses = localNetworkAddresses.Select(p => IPAddress.TryParse(p, out var addresses) ? addresses : (_interfaces.Where(x => x.Name.Equals(p, StringComparison.OrdinalIgnoreCase)) .Select(x => x.Address) @@ -337,6 +340,16 @@ namespace Jellyfin.Networking.Manager .ToList(); bindAddresses.RemoveAll(x => x == IPAddress.None); _interfaces = _interfaces.Where(x => bindAddresses.Contains(x.Address)).ToList(); + + if (bindAddresses.Contains(IPAddress.Loopback)) + { + _interfaces.Add(new IPData(IPAddress.Loopback, new IPNetwork(IPAddress.Loopback, 8), "lo")); + } + + if (bindAddresses.Contains(IPAddress.IPv6Loopback)) + { + _interfaces.Add(new IPData(IPAddress.IPv6Loopback, new IPNetwork(IPAddress.IPv6Loopback, 128), "lo")); + } } // Remove all interfaces matching any virtual machine interface prefix @@ -358,6 +371,16 @@ namespace Jellyfin.Networking.Manager } } + if (!IsIpv4Enabled) + { + _interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetwork); + } + + if (!IsIpv6Enabled) + { + _interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetworkV6); + } + _logger.LogInformation("Using bind addresses: {0}", _interfaces.Select(x => x.Address)); } } @@ -492,7 +515,7 @@ namespace Jellyfin.Networking.Manager } } - EnforceBindRestrictions(config); + EnforceBindSettings(config); InitialiseOverrides(config); } @@ -892,7 +915,7 @@ namespace Jellyfin.Networking.Manager { result = string.Empty; - int count = _interfaces.Count(); + int count = _interfaces.Count; if (count == 1 && (_interfaces[0].Equals(IPAddress.Any) || _interfaces[0].Equals(IPAddress.IPv6Any))) { // Ignore IPAny addresses. From 05458a4a4240d2b15db93de6c9ec25376677b5e1 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Thu, 21 Jul 2022 00:32:51 +0200 Subject: [PATCH 018/858] Fix some ssdp errors --- RSSDP/SsdpCommunicationsServer.cs | 6 ++++-- RSSDP/SsdpDevicePublisher.cs | 4 +--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs index 6e4f5634da..53f872b600 100644 --- a/RSSDP/SsdpCommunicationsServer.cs +++ b/RSSDP/SsdpCommunicationsServer.cs @@ -348,8 +348,6 @@ namespace Rssdp.Infrastructure { var sockets = new List(); - sockets.Add(_SocketFactory.CreateSsdpUdpSocket(IPAddress.Any, _LocalPort)); - if (_enableMultiSocketBinding) { foreach (var address in _networkManager.GetInternalBindAddresses()) @@ -370,6 +368,10 @@ namespace Rssdp.Infrastructure } } } + else + { + sockets.Add(_SocketFactory.CreateSsdpUdpSocket(IPAddress.Any, _LocalPort)); + } foreach (var socket in sockets) { diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs index 37c4128ba4..adaac5fa38 100644 --- a/RSSDP/SsdpDevicePublisher.cs +++ b/RSSDP/SsdpDevicePublisher.cs @@ -298,9 +298,7 @@ namespace Rssdp.Infrastructure foreach (var device in deviceList) { var root = device.ToRootDevice(); - var source = new IPData(root.Address, new IPNetwork(root.Address, root.PrefixLength), root.FriendlyName); - var destination = new IPData(remoteEndPoint.Address, new IPNetwork(root.Address, root.PrefixLength)); - if (!_sendOnlyMatchedHost || source.Address.Equals(destination.Address)) + if (!_sendOnlyMatchedHost || root.Address.Equals(remoteEndPoint.Address)) { SendDeviceSearchResponses(device, remoteEndPoint, receivedOnlocalIpAddress, cancellationToken); } From f6e41269d94e4c3096b136a47d3616cb0c5fe316 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Thu, 21 Jul 2022 09:26:18 +0200 Subject: [PATCH 019/858] Enforce interface bindings on SSDP, add Loopback to LAN if no LAN defined --- Emby.Dlna/Main/DlnaEntryPoint.cs | 7 +- .../Net/SocketFactory.cs | 13 ++-- Jellyfin.Networking/Manager/NetworkManager.cs | 16 ++-- MediaBrowser.Model/Net/ISocketFactory.cs | 4 +- RSSDP/SsdpCommunicationsServer.cs | 75 +++++++++++++------ 5 files changed, 74 insertions(+), 41 deletions(-) diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index 90c985a816..89119d31dc 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -284,6 +284,7 @@ namespace Emby.Dlna.Main var udn = CreateUuid(_appHost.SystemId); var descriptorUri = "/dlna/" + udn + "/description.xml"; + // Only get bind addresses in LAN var bindAddresses = _networkManager .GetInternalBindAddresses() .Where(i => i.Address.AddressFamily == AddressFamily.InterNetwork @@ -304,12 +305,6 @@ namespace Emby.Dlna.Main continue; } - // Limit to LAN addresses only - if (!_networkManager.IsInLocalNetwork(address.Address)) - { - continue; - } - var fullService = "urn:schemas-upnp-org:device:MediaServer:1"; _logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, address); diff --git a/Emby.Server.Implementations/Net/SocketFactory.cs b/Emby.Server.Implementations/Net/SocketFactory.cs index 21795c8f86..a1baf30064 100644 --- a/Emby.Server.Implementations/Net/SocketFactory.cs +++ b/Emby.Server.Implementations/Net/SocketFactory.cs @@ -61,13 +61,18 @@ namespace Emby.Server.Implementations.Net } /// - public ISocket CreateUdpMulticastSocket(IPAddress ipAddress, int multicastTimeToLive, int localPort) + public ISocket CreateUdpMulticastSocket(IPAddress ipAddress, IPAddress bindIpAddress, int multicastTimeToLive, int localPort) { if (ipAddress == null) { throw new ArgumentNullException(nameof(ipAddress)); } + if (bindIpAddress == null) + { + bindIpAddress = IPAddress.Any; + } + if (multicastTimeToLive <= 0) { throw new ArgumentException("multicastTimeToLive cannot be zero or less.", nameof(multicastTimeToLive)); @@ -98,12 +103,10 @@ namespace Emby.Server.Implementations.Net // retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, true); retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, multicastTimeToLive); - var localIp = IPAddress.Any; - - retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(ipAddress, localIp)); + retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(ipAddress, bindIpAddress)); retVal.MulticastLoopback = true; - return new UdpSocket(retVal, localPort, localIp); + return new UdpSocket(retVal, localPort, bindIpAddress); } catch { diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index af2429cc28..b1112626c4 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -298,20 +298,22 @@ namespace Jellyfin.Networking.Manager if (_lanSubnets.Count == 0) { - // If no LAN addresses are specified, all private subnets are deemed to be the LAN + // If no LAN addresses are specified, all private subnets and Loopback are deemed to be the LAN _logger.LogDebug("Using LAN interface addresses as user provided no LAN details."); if (IsIpv6Enabled) { - _lanSubnets.Add(new IPNetwork(IPAddress.Parse("fc00::"), 7)); // ULA - _lanSubnets.Add(new IPNetwork(IPAddress.Parse("fe80::"), 10)); // Site local + _lanSubnets.Add(new IPNetwork(IPAddress.IPv6Loopback, 128)); // RFC 4291 (Loopback) + _lanSubnets.Add(new IPNetwork(IPAddress.Parse("fe80::"), 10)); // RFC 4291 (Site local) + _lanSubnets.Add(new IPNetwork(IPAddress.Parse("fc00::"), 7)); // RFC 4193 (Unique local) } if (IsIpv4Enabled) { - _lanSubnets.Add(new IPNetwork(IPAddress.Parse("10.0.0.0"), 8)); - _lanSubnets.Add(new IPNetwork(IPAddress.Parse("172.16.0.0"), 12)); - _lanSubnets.Add(new IPNetwork(IPAddress.Parse("192.168.0.0"), 16)); + _lanSubnets.Add(new IPNetwork(IPAddress.Loopback, 8)); // RFC 5735 (Loopback) + _lanSubnets.Add(new IPNetwork(IPAddress.Parse("10.0.0.0"), 8)); // RFC 1918 (private) + _lanSubnets.Add(new IPNetwork(IPAddress.Parse("172.16.0.0"), 12)); // RFC 1918 (private) + _lanSubnets.Add(new IPNetwork(IPAddress.Parse("192.168.0.0"), 16)); // RFC 1918 (private) } } @@ -371,11 +373,13 @@ namespace Jellyfin.Networking.Manager } } + // Remove all IPv4 interfaces if IPv4 is disabled if (!IsIpv4Enabled) { _interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetwork); } + // Remove all IPv6 interfaces if IPv6 is disabled if (!IsIpv6Enabled) { _interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetworkV6); diff --git a/MediaBrowser.Model/Net/ISocketFactory.cs b/MediaBrowser.Model/Net/ISocketFactory.cs index a2835b711b..e5cf7adbc0 100644 --- a/MediaBrowser.Model/Net/ISocketFactory.cs +++ b/MediaBrowser.Model/Net/ISocketFactory.cs @@ -1,5 +1,6 @@ #pragma warning disable CS1591 +using System.Collections.Generic; using System.Net; namespace MediaBrowser.Model.Net @@ -23,9 +24,10 @@ namespace MediaBrowser.Model.Net /// Creates a new multicast socket using the specified multicast IP address, multicast time to live and local port. /// /// The multicast IP address to bind to. + /// The bind IP address. /// The multicast time to live value. Actually a maximum number of network hops for UDP packets. /// The local port to bind to. /// A implementation. - ISocket CreateUdpMulticastSocket(IPAddress ipAddress, int multicastTimeToLive, int localPort); + ISocket CreateUdpMulticastSocket(IPAddress ipAddress, IPAddress bindIpAddress, int multicastTimeToLive, int localPort); } } diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs index 53f872b600..e6c0a44039 100644 --- a/RSSDP/SsdpCommunicationsServer.cs +++ b/RSSDP/SsdpCommunicationsServer.cs @@ -33,7 +33,7 @@ namespace Rssdp.Infrastructure */ private object _BroadcastListenSocketSynchroniser = new object(); - private ISocket _BroadcastListenSocket; + private List _BroadcastListenSockets; private object _SendSocketSynchroniser = new object(); private List _sendSockets; @@ -111,24 +111,21 @@ namespace Rssdp.Infrastructure { ThrowIfDisposed(); - if (_BroadcastListenSocket == null) + lock (_BroadcastListenSocketSynchroniser) { - lock (_BroadcastListenSocketSynchroniser) + if (_BroadcastListenSockets == null) { - if (_BroadcastListenSocket == null) + try { - try - { - _BroadcastListenSocket = ListenForBroadcastsAsync(); - } - catch (SocketException ex) - { - _logger.LogError("Failed to bind to port 1900: {Message}. DLNA will be unavailable", ex.Message); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error in BeginListeningForBroadcasts"); - } + _BroadcastListenSockets = ListenForBroadcastsAsync(); + } + catch (SocketException ex) + { + _logger.LogError("Failed to bind to port 1900: {Message}. DLNA will be unavailable", ex.Message); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error in BeginListeningForBroadcasts"); } } } @@ -142,11 +139,15 @@ namespace Rssdp.Infrastructure { lock (_BroadcastListenSocketSynchroniser) { - if (_BroadcastListenSocket != null) + if (_BroadcastListenSockets != null) { _logger.LogInformation("{0} disposing _BroadcastListenSocket", GetType().Name); - _BroadcastListenSocket.Dispose(); - _BroadcastListenSocket = null; + foreach (var socket in _BroadcastListenSockets) + { + socket.Dispose(); + } + + _BroadcastListenSockets = null; } } } @@ -336,12 +337,40 @@ namespace Rssdp.Infrastructure return Task.CompletedTask; } - private ISocket ListenForBroadcastsAsync() + private List ListenForBroadcastsAsync() { - var socket = _SocketFactory.CreateUdpMulticastSocket(IPAddress.Parse(SsdpConstants.MulticastLocalAdminAddress), _MulticastTtl, SsdpConstants.MulticastPort); - _ = ListenToSocketInternal(socket); + var sockets = new List(); + if (_enableMultiSocketBinding) + { + foreach (var address in _networkManager.GetInternalBindAddresses()) + { + if (address.AddressFamily == AddressFamily.InterNetworkV6) + { + // Not support IPv6 right now + continue; + } - return socket; + try + { + sockets.Add(_SocketFactory.CreateUdpMulticastSocket(IPAddress.Parse(SsdpConstants.MulticastLocalAdminAddress), address.Address, _MulticastTtl, SsdpConstants.MulticastPort)); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error in ListenForBroadcastsAsync. IPAddress: {0}", address); + } + } + } + else + { + sockets.Add(_SocketFactory.CreateUdpMulticastSocket(IPAddress.Parse(SsdpConstants.MulticastLocalAdminAddress), IPAddress.Any, _MulticastTtl, SsdpConstants.MulticastPort)); + } + + foreach (var socket in sockets) + { + _ = ListenToSocketInternal(socket); + } + + return sockets; } private List CreateSocketAndListenForResponsesAsync() From b01d169d28cb7cfffa33796dfe7bf8be5570593a Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Thu, 21 Jul 2022 09:42:45 +0200 Subject: [PATCH 020/858] Implement discovery respecting bind addresses --- .../EntryPoints/UdpServerEntryPoint.cs | 20 ++++++++++++++++--- Emby.Server.Implementations/Udp/UdpServer.cs | 4 +++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs index e45baedd7f..9ac2310b4f 100644 --- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs +++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Emby.Server.Implementations.Udp; using Jellyfin.Networking.Configuration; using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.Plugins; using Microsoft.Extensions.Configuration; @@ -29,6 +30,7 @@ namespace Emby.Server.Implementations.EntryPoints private readonly IServerApplicationHost _appHost; private readonly IConfiguration _config; private readonly IConfigurationManager _configurationManager; + private readonly INetworkManager _networkManager; /// /// The UDP server. @@ -44,16 +46,19 @@ namespace Emby.Server.Implementations.EntryPoints /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. + /// Instance of the interface. public UdpServerEntryPoint( ILogger logger, IServerApplicationHost appHost, IConfiguration configuration, - IConfigurationManager configurationManager) + IConfigurationManager configurationManager, + INetworkManager networkManager) { _logger = logger; _appHost = appHost; _config = configuration; _configurationManager = configurationManager; + _networkManager = networkManager; } /// @@ -68,8 +73,17 @@ namespace Emby.Server.Implementations.EntryPoints try { - _udpServer = new UdpServer(_logger, _appHost, _config, PortNumber); - _udpServer.Start(_cancellationTokenSource.Token); + foreach (var bindAddress in _networkManager.GetInternalBindAddresses()) + { + if (bindAddress.AddressFamily == AddressFamily.InterNetworkV6) + { + // Not supporting IPv6 right now + continue; + } + + _udpServer = new UdpServer(_logger, _appHost, _config, bindAddress.Address, PortNumber); + _udpServer.Start(_cancellationTokenSource.Token); + } } catch (SocketException ex) { diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs index 937e792f57..a3bbd6df0c 100644 --- a/Emby.Server.Implementations/Udp/UdpServer.cs +++ b/Emby.Server.Implementations/Udp/UdpServer.cs @@ -37,18 +37,20 @@ namespace Emby.Server.Implementations.Udp /// The logger. /// The application host. /// The configuration manager. + /// The bind address. /// The port. public UdpServer( ILogger logger, IServerApplicationHost appHost, IConfiguration configuration, + IPAddress bindAddress, int port) { _logger = logger; _appHost = appHost; _config = configuration; - _endpoint = new IPEndPoint(IPAddress.Any, port); + _endpoint = new IPEndPoint(bindAddress, port); _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); _udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); From a2857c5a02746d1cfd36d484b61961293de2b889 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Thu, 21 Jul 2022 10:20:20 +0200 Subject: [PATCH 021/858] Check if OS is capable of binding multiple sockets before creating autodiscovery sockets --- .../EntryPoints/UdpServerEntryPoint.cs | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs index 9ac2310b4f..46b66dab3e 100644 --- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs +++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs @@ -31,6 +31,7 @@ namespace Emby.Server.Implementations.EntryPoints private readonly IConfiguration _config; private readonly IConfigurationManager _configurationManager; private readonly INetworkManager _networkManager; + private readonly bool _enableMultiSocketBinding; /// /// The UDP server. @@ -59,6 +60,7 @@ namespace Emby.Server.Implementations.EntryPoints _config = configuration; _configurationManager = configurationManager; _networkManager = networkManager; + _enableMultiSocketBinding = OperatingSystem.IsWindows() || OperatingSystem.IsLinux(); } /// @@ -73,15 +75,23 @@ namespace Emby.Server.Implementations.EntryPoints try { - foreach (var bindAddress in _networkManager.GetInternalBindAddresses()) + if (_enableMultiSocketBinding) { - if (bindAddress.AddressFamily == AddressFamily.InterNetworkV6) + foreach (var bindAddress in _networkManager.GetInternalBindAddresses()) { - // Not supporting IPv6 right now - continue; - } + if (bindAddress.AddressFamily == AddressFamily.InterNetworkV6) + { + // Not supporting IPv6 right now + continue; + } - _udpServer = new UdpServer(_logger, _appHost, _config, bindAddress.Address, PortNumber); + _udpServer = new UdpServer(_logger, _appHost, _config, bindAddress.Address, PortNumber); + _udpServer.Start(_cancellationTokenSource.Token); + } + } + else + { + _udpServer = new UdpServer(_logger, _appHost, _config, System.Net.IPAddress.Any, PortNumber); _udpServer.Start(_cancellationTokenSource.Token); } } From b5c1d6129e394e40cd8a6bf338184edfe58c0ef4 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Thu, 21 Jul 2022 11:43:45 +0200 Subject: [PATCH 022/858] Remove more unused network configuration parameters --- .../Configuration/NetworkConfiguration.cs | 48 ------------------- 1 file changed, 48 deletions(-) diff --git a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs index e9f6d597b4..642af7dda0 100644 --- a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs +++ b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs @@ -102,21 +102,11 @@ namespace Jellyfin.Networking.Configuration /// The public mapped port. public int PublicPort { get; set; } = DefaultHttpPort; - /// - /// Gets or sets a value indicating whether the http port should be mapped as part of UPnP automatic port forwarding. - /// - public bool UPnPCreateHttpPortMap { get; set; } - /// /// Gets or sets a value indicating whether Autodiscovery is enabled. /// public bool AutoDiscovery { get; set; } = true; - /// - /// Gets or sets a value indicating whether Autodiscovery tracing is enabled. - /// - public bool AutoDiscoveryTracing { get; set; } - /// /// Gets or sets a value indicating whether to enable automatic port forwarding. /// @@ -152,21 +142,6 @@ namespace Jellyfin.Networking.Configuration /// public string[] KnownProxies { get; set; } = Array.Empty(); - /// - /// Gets or sets the UDPPortRange. - /// - public string UDPPortRange { get; set; } = string.Empty; - - /// - /// Gets or sets the number of times SSDP UDP messages are sent. - /// - public int UDPSendCount { get; set; } = 2; - - /// - /// Gets or sets the delay between each groups of SSDP messages (in ms). - /// - public int UDPSendDelay { get; set; } = 100; - /// /// Gets or sets a value indicating whether address names that match should be Ignore for the purposes of binding. /// @@ -188,16 +163,6 @@ namespace Jellyfin.Networking.Configuration /// public string[] PublishedServerUriBySubnet { get; set; } = Array.Empty(); - /// - /// Gets a value indicating whether multi-socket binding is available. - /// - public bool EnableMultiSocketBinding { get; } = true; - - /// - /// Gets or sets the ports that HDHomerun uses. - /// - public string HDHomerunPortRange { get; set; } = string.Empty; - /// /// Gets or sets the filter for remote IP connectivity. Used in conjuntion with . /// @@ -207,18 +172,5 @@ namespace Jellyfin.Networking.Configuration /// Gets or sets a value indicating whether contains a blacklist or a whitelist. Default is a whitelist. /// public bool IsRemoteIPFilterBlacklist { get; set; } - - /// - /// Gets or sets a value indicating whether detailed SSDP logs are sent to the console/log. - /// "Emby.Dlna": "Debug" must be set in logging.default.json for this property to have any effect. - /// - public bool EnableSSDPTracing { get; set; } - - /// - /// Gets or sets the SSDPTracingFilter - /// Gets or sets a value indicating whether an IP address is to be used to filter the detailed ssdp logs that are being sent to the console/log. - /// If the setting "Emby.Dlna": "Debug" msut be set in logging.default.json for this property to work. - /// - public string SSDPTracingFilter { get; set; } = string.Empty; } } From cea8e8bbf601b97119c8b0b0a2e666414b53050c Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Thu, 21 Jul 2022 19:17:44 +0200 Subject: [PATCH 023/858] Fix logging output --- Emby.Dlna/Main/DlnaEntryPoint.cs | 2 +- Jellyfin.Networking/Manager/NetworkManager.cs | 33 +++++++++++-------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index 89119d31dc..0aea7855a6 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -307,7 +307,7 @@ namespace Emby.Dlna.Main var fullService = "urn:schemas-upnp-org:device:MediaServer:1"; - _logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, address); + _logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, address.Address); var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(address.Address, false) + descriptorUri); diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index b1112626c4..12edb0e55c 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -933,22 +933,27 @@ namespace Jellyfin.Networking.Manager .OrderBy(x => x.Index) .ToList(); - if (isInExternalSubnet && externalInterfaces.Any()) + if (isInExternalSubnet) { - // Check to see if any of the external bind interfaces are in the same subnet as the source. - // If none exists, this will select the first external interface if there is one. - bindAddress = externalInterfaces - .OrderByDescending(x => x.Subnet.Contains(source)) - .ThenBy(x => x.Index) - .Select(x => x.Address) - .FirstOrDefault(); - - if (bindAddress != null) + if (externalInterfaces.Any()) { - result = NetworkExtensions.FormatIpString(bindAddress); - _logger.LogDebug("{Source}: GetBindInterface: Has source, found a matching external bind interface. {Result}", source, result); - return true; + // Check to see if any of the external bind interfaces are in the same subnet as the source. + // If none exists, this will select the first external interface if there is one. + bindAddress = externalInterfaces + .OrderByDescending(x => x.Subnet.Contains(source)) + .ThenBy(x => x.Index) + .Select(x => x.Address) + .FirstOrDefault(); + + if (bindAddress != null) + { + result = NetworkExtensions.FormatIpString(bindAddress); + _logger.LogDebug("{Source}: GetBindInterface: Has source, found a matching external bind interface. {Result}", source, result); + return true; + } } + + _logger.LogWarning("{Source}: External request received, no external interface bind found, trying internal interfaces.", source); } else { @@ -963,7 +968,7 @@ namespace Jellyfin.Networking.Manager if (bindAddress != null) { result = NetworkExtensions.FormatIpString(bindAddress); - _logger.LogWarning("{Source}: External request received, only an internal interface bind found. {Result}", source, result); + _logger.LogWarning("{Source}: Request received, matching internal interface bind found. {Result}", source, result); return true; } } From ff22f597d219b74ce392e4bbbaf1123e5a269f99 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Thu, 21 Jul 2022 19:38:19 +0200 Subject: [PATCH 024/858] Fix multiple UDP servers for AutoDiscovery --- .../EntryPoints/UdpServerEntryPoint.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs index 46b66dab3e..82c6abb8c8 100644 --- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs +++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; @@ -36,7 +37,7 @@ namespace Emby.Server.Implementations.EntryPoints /// /// The UDP server. /// - private UdpServer? _udpServer; + private List _udpServers; private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); private bool _disposed = false; @@ -60,6 +61,7 @@ namespace Emby.Server.Implementations.EntryPoints _config = configuration; _configurationManager = configurationManager; _networkManager = networkManager; + _udpServers = new List(); _enableMultiSocketBinding = OperatingSystem.IsWindows() || OperatingSystem.IsLinux(); } @@ -85,15 +87,15 @@ namespace Emby.Server.Implementations.EntryPoints continue; } - _udpServer = new UdpServer(_logger, _appHost, _config, bindAddress.Address, PortNumber); - _udpServer.Start(_cancellationTokenSource.Token); + _udpServers.Add(new UdpServer(_logger, _appHost, _config, bindAddress.Address, PortNumber)); } } else { - _udpServer = new UdpServer(_logger, _appHost, _config, System.Net.IPAddress.Any, PortNumber); - _udpServer.Start(_cancellationTokenSource.Token); + _udpServers.Add(new UdpServer(_logger, _appHost, _config, System.Net.IPAddress.Any, PortNumber)); } + + _udpServers.ForEach(u => u.Start(_cancellationTokenSource.Token)); } catch (SocketException ex) { @@ -121,8 +123,8 @@ namespace Emby.Server.Implementations.EntryPoints _cancellationTokenSource.Cancel(); _cancellationTokenSource.Dispose(); - _udpServer?.Dispose(); - _udpServer = null; + _udpServers.ForEach(s => s.Dispose()); + _udpServers.Clear(); _disposed = true; } From 59a86568d9539245dee30cf3a33ef6beb31f4bba Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Thu, 21 Jul 2022 22:09:54 +0200 Subject: [PATCH 025/858] Cleanup and fixes --- Jellyfin.Networking/Manager/NetworkManager.cs | 12 ++++- .../ApiServiceCollectionExtensions.cs | 8 ++-- MediaBrowser.Common/Net/INetworkManager.cs | 44 +++++++++---------- MediaBrowser.Common/Net/IPData.cs | 8 ++-- MediaBrowser.Model/Net/ISocketFactory.cs | 1 - RSSDP/SsdpCommunicationsServer.cs | 6 +-- 6 files changed, 40 insertions(+), 39 deletions(-) diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 12edb0e55c..0f8f1a36a0 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -263,7 +263,7 @@ namespace Jellyfin.Networking.Manager if (_interfaces.Count == 0) { - _logger.LogWarning("No interfaces information available. Using loopback."); + _logger.LogWarning("No interface information available. Using loopback interface(s)."); if (IsIpv4Enabled && !IsIpv6Enabled) { @@ -450,6 +450,14 @@ namespace Jellyfin.Networking.Manager _publishedServerUrls[new IPData(IPAddress.Any, new IPNetwork(IPAddress.Any, 0))] = replacement; _publishedServerUrls[new IPData(IPAddress.IPv6Any, new IPNetwork(IPAddress.IPv6Any, 0))] = replacement; } + else if (string.Equals(parts[0], "internal", StringComparison.OrdinalIgnoreCase)) + { + foreach (var lan in _lanSubnets) + { + var lanPrefix = lan.Prefix; + _publishedServerUrls[new IPData(lanPrefix, new IPNetwork(lanPrefix, lan.PrefixLength))] = replacement; + } + } else if (IPAddress.TryParse(ipParts[0], out IPAddress? result)) { var data = new IPData(result, null); @@ -469,7 +477,7 @@ namespace Jellyfin.Networking.Manager } else { - _logger.LogError("Unable to parse bind ip address. {Parts}", parts[1]); + _logger.LogError("Unable to parse bind override: {Entry}", entry); } } } diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index a393b80db5..25516271c5 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -350,14 +350,14 @@ namespace Jellyfin.Server.Extensions } else if (NetworkExtensions.TryParseSubnets(new[] { allowedProxies[i] }, out var subnets)) { - for (var j = 0; j < subnets.Count; j++) + foreach (var subnet in subnets) { - AddIpAddress(config, options, subnets[j].Prefix, subnets[j].PrefixLength); + AddIpAddress(config, options, subnet.Prefix, subnet.PrefixLength); } } - else if (NetworkExtensions.TryParseHost(allowedProxies[i], out var host)) + else if (NetworkExtensions.TryParseHost(allowedProxies[i], out var addresses)) { - foreach (var address in host) + foreach (var address in addresses) { AddIpAddress(config, options, address, address.AddressFamily == AddressFamily.InterNetwork ? 32 : 128); } diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs index 21ff982376..d462e954a8 100644 --- a/MediaBrowser.Common/Net/INetworkManager.cs +++ b/MediaBrowser.Common/Net/INetworkManager.cs @@ -16,16 +16,16 @@ namespace MediaBrowser.Common.Net /// event EventHandler NetworkChanged; - /// - /// Gets a value indicating whether IPv6 is enabled. - /// - bool IsIpv6Enabled { get; } - /// /// Gets a value indicating whether IPv4 is enabled. /// bool IsIpv4Enabled { get; } + /// + /// Gets a value indicating whether IPv6 is enabled. + /// + bool IsIpv6Enabled { get; } + /// /// Calculates the list of interfaces to use for Kestrel. /// @@ -42,7 +42,7 @@ namespace MediaBrowser.Common.Net IReadOnlyList GetLoopbacks(); /// - /// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo) + /// Retrieves the bind address to use in system URLs. (Server Discovery, PlayTo, LiveTV, SystemInfo) /// If no bind addresses are specified, an internal interface address is selected. /// The priority of selection is as follows:- /// @@ -56,40 +56,40 @@ namespace MediaBrowser.Common.Net /// /// 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. + /// The first public interface that isn't a loopback. + /// The first internal interface that isn't a loopback. /// /// 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. + /// The first private interface that isn't a loopback. /// /// 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. + /// Interfaces that have been specifically excluded from binding are not used in any of the calculations. /// /// Source of the request. /// Optional port returned, if it's part of an override. - /// IP Address to use, or loopback address if all else fails. + /// IP address to use, or loopback address if all else fails. string GetBindInterface(HttpRequest source, out int? port); /// - /// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo) + /// Retrieves the bind address to use in system URLs. (Server Discovery, PlayTo, LiveTV, SystemInfo) /// If no bind addresses are specified, an internal interface address is selected. /// (See . /// /// IP address of the request. /// Optional port returned, if it's part of an override. - /// IP Address to use, or loopback address if all else fails. + /// IP address to use, or loopback address if all else fails. string GetBindInterface(IPAddress source, out int? port); /// - /// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo) + /// Retrieves the bind address to use in system URLs. (Server Discovery, PlayTo, LiveTV, SystemInfo) /// If no bind addresses are specified, an internal interface address is selected. /// (See . /// /// Source of the request. /// Optional port returned, if it's part of an override. - /// IP Address to use, or loopback address if all else fails. + /// IP address to use, or loopback address if all else fails. string GetBindInterface(string source, out int? port); /// @@ -100,7 +100,6 @@ namespace MediaBrowser.Common.Net /// /// Returns true if the address is part of the user defined LAN. - /// The configuration option TrustIP6Interfaces overrides this functions behaviour. /// /// IP to check. /// True if endpoint is within the LAN range. @@ -108,7 +107,6 @@ namespace MediaBrowser.Common.Net /// /// Returns true if the address is part of the user defined LAN. - /// The configuration option TrustIP6Interfaces overrides this functions behaviour. /// /// IP to check. /// True if endpoint is within the LAN range. @@ -119,21 +117,21 @@ namespace MediaBrowser.Common.Net /// eg. "eth1", or "enp3s5". /// /// Interface name. - /// Resultant object's ip addresses, if successful. + /// Resulting object's IP addresses, if successful. /// Success of the operation. bool TryParseInterface(string intf, out List? result); /// - /// Returns all the internal bind interface addresses. + /// Returns all internal (LAN) bind interface addresses. /// - /// An internal list of interfaces addresses. + /// An list of internal (LAN) interfaces addresses. IReadOnlyList GetInternalBindAddresses(); /// - /// Checks to see if has access. + /// Checks if has access to the server. /// - /// IP Address of client. - /// True if has access, otherwise false. + /// IP address of the client. + /// True if it has access, otherwise false. bool HasRemoteAccess(IPAddress remoteIp); } } diff --git a/MediaBrowser.Common/Net/IPData.cs b/MediaBrowser.Common/Net/IPData.cs index 6901d6ad8a..384efe8f68 100644 --- a/MediaBrowser.Common/Net/IPData.cs +++ b/MediaBrowser.Common/Net/IPData.cs @@ -12,9 +12,9 @@ namespace MediaBrowser.Common.Net /// /// Initializes a new instance of the class. /// - /// An . + /// The . /// The . - /// The object's name. + /// The interface name. public IPData(IPAddress address, IPNetwork? subnet, string name) { Address = address; @@ -25,7 +25,7 @@ namespace MediaBrowser.Common.Net /// /// Initializes a new instance of the class. /// - /// An . + /// The . /// The . public IPData(IPAddress address, IPNetwork? subnet) : this(address, subnet, string.Empty) @@ -53,7 +53,7 @@ namespace MediaBrowser.Common.Net public string Name { get; set; } /// - /// Gets the AddressFamily of this object. + /// Gets the AddressFamily of the object. /// public AddressFamily AddressFamily { diff --git a/MediaBrowser.Model/Net/ISocketFactory.cs b/MediaBrowser.Model/Net/ISocketFactory.cs index e5cf7adbc0..f3bc31796d 100644 --- a/MediaBrowser.Model/Net/ISocketFactory.cs +++ b/MediaBrowser.Model/Net/ISocketFactory.cs @@ -1,6 +1,5 @@ #pragma warning disable CS1591 -using System.Collections.Generic; using System.Net; namespace MediaBrowser.Model.Net diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs index e6c0a44039..92c9c83c0f 100644 --- a/RSSDP/SsdpCommunicationsServer.cs +++ b/RSSDP/SsdpCommunicationsServer.cs @@ -142,11 +142,7 @@ namespace Rssdp.Infrastructure if (_BroadcastListenSockets != null) { _logger.LogInformation("{0} disposing _BroadcastListenSocket", GetType().Name); - foreach (var socket in _BroadcastListenSockets) - { - socket.Dispose(); - } - + _BroadcastListenSockets.ForEach(s => s.Dispose()); _BroadcastListenSockets = null; } } From bd9a940fed129d99fe9ffedafec324e795549c90 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Sat, 1 Oct 2022 20:00:35 +0200 Subject: [PATCH 026/858] Declare VirtualInterfaceNames as string array for consistency --- .../Configuration/NetworkConfiguration.cs | 2 +- Jellyfin.Networking/Manager/NetworkManager.cs | 31 ++++++++++++------- .../CreateNetworkConfiguration.cs | 2 +- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs index 642af7dda0..f904198510 100644 --- a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs +++ b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs @@ -150,7 +150,7 @@ namespace Jellyfin.Networking.Configuration /// /// Gets or sets a value indicating the interface name prefixes that should be ignored. The list can be comma separated and values are case-insensitive. . /// - public string VirtualInterfaceNames { get; set; } = "veth"; + public string[] VirtualInterfaceNames { get; set; } = new string[] { "veth" }; /// /// Gets or sets a value indicating whether the published server uri is based on information in HTTP requests. diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 0f8f1a36a0..146340989b 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -359,12 +359,11 @@ namespace Jellyfin.Networking.Manager { // Remove potentially exisiting * and split config string into prefixes var virtualInterfacePrefixes = config.VirtualInterfaceNames - .Replace("*", string.Empty, StringComparison.OrdinalIgnoreCase) - .ToLowerInvariant() - .Split(','); + .Select(i => i.ToLowerInvariant() + .Replace("*", string.Empty, StringComparison.OrdinalIgnoreCase)); // Check all interfaces for matches against the prefixes and remove them - if (_interfaces.Count > 0 && virtualInterfacePrefixes.Length > 0) + if (_interfaces.Count > 0 && virtualInterfacePrefixes.Any()) { foreach (var virtualInterfacePrefix in virtualInterfacePrefixes) { @@ -726,7 +725,15 @@ namespace Jellyfin.Networking.Manager if (MatchesPublishedServerUrl(source, isExternal, out string res, out port)) { - _logger.LogInformation("{Source}: Using BindAddress {Address}:{Port}", source, res, port); + if (port != null) + { + _logger.LogInformation("{Source}: Using BindAddress {Address}:{Port}", source, res, port); + } + else + { + _logger.LogInformation("{Source}: Using BindAddress {Address}", source, res); + } + return res; } @@ -756,7 +763,7 @@ namespace Jellyfin.Networking.Manager if (intf.Address.Equals(source)) { result = NetworkExtensions.FormatIpString(intf.Address); - _logger.LogDebug("{Source}: GetBindInterface: Has found matching interface. {Result}", source, result); + _logger.LogDebug("{Source}: GetBindInterface: Has found matching interface: {Result}", source, result); return result; } } @@ -768,14 +775,14 @@ namespace Jellyfin.Networking.Manager if (intf.Subnet.Contains(source)) { result = NetworkExtensions.FormatIpString(intf.Address); - _logger.LogDebug("{Source}: GetBindInterface: Has source, matched best internal interface on range. {Result}", source, result); + _logger.LogDebug("{Source}: GetBindInterface: Has source, matched best internal interface on range: {Result}", source, result); return result; } } } result = NetworkExtensions.FormatIpString(availableInterfaces.First().Address); - _logger.LogDebug("{Source}: GetBindInterface: Matched first internal interface. {Result}", source, result); + _logger.LogDebug("{Source}: GetBindInterface: Matched first internal interface: {Result}", source, result); return result; } @@ -956,7 +963,7 @@ namespace Jellyfin.Networking.Manager if (bindAddress != null) { result = NetworkExtensions.FormatIpString(bindAddress); - _logger.LogDebug("{Source}: GetBindInterface: Has source, found a matching external bind interface. {Result}", source, result); + _logger.LogDebug("{Source}: GetBindInterface: Has source, found a matching external bind interface: {Result}", source, result); return true; } } @@ -976,7 +983,7 @@ namespace Jellyfin.Networking.Manager if (bindAddress != null) { result = NetworkExtensions.FormatIpString(bindAddress); - _logger.LogWarning("{Source}: Request received, matching internal interface bind found. {Result}", source, result); + _logger.LogWarning("{Source}: Request received, matching internal interface bind found: {Result}", source, result); return true; } } @@ -1006,7 +1013,7 @@ namespace Jellyfin.Networking.Manager if (!IsInLocalNetwork(intf.Address) && intf.Subnet.Contains(source)) { result = NetworkExtensions.FormatIpString(intf.Address); - _logger.LogDebug("{Source}: GetBindInterface: Selected best external on interface on range. {Result}", source, result); + _logger.LogDebug("{Source}: GetBindInterface: Selected best external on interface on range: {Result}", source, result); return true; } } @@ -1014,7 +1021,7 @@ namespace Jellyfin.Networking.Manager if (hasResult != null) { result = NetworkExtensions.FormatIpString(hasResult); - _logger.LogDebug("{Source}: GetBindInterface: Selected first external interface. {Result}", source, result); + _logger.LogDebug("{Source}: GetBindInterface: Selected first external interface: {Result}", source, result); return true; } diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs index b670172814..2c2715526f 100644 --- a/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs +++ b/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs @@ -114,7 +114,7 @@ public class CreateNetworkConfiguration : IMigrationRoutine public bool IgnoreVirtualInterfaces { get; set; } = true; - public string VirtualInterfaceNames { get; set; } = "veth"; + public string[] VirtualInterfaceNames { get; set; } = new string[] { "veth" }; public string[] PublishedServerUriBySubnet { get; set; } = Array.Empty(); From 4aec41752f63594e169511a314f8f86a0dde9c35 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Fri, 14 Oct 2022 10:25:57 +0200 Subject: [PATCH 027/858] Apply review suggestions --- Emby.Dlna/Main/DlnaEntryPoint.cs | 8 +-- Jellyfin.Networking/Manager/NetworkManager.cs | 58 ++++++++++--------- .../ApiServiceCollectionExtensions.cs | 4 +- MediaBrowser.Common/Net/NetworkExtensions.cs | 54 ++++++++++++++++- RSSDP/SsdpCommunicationsServer.cs | 6 +- 5 files changed, 92 insertions(+), 38 deletions(-) diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index 0aea7855a6..7c26262fe1 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -286,10 +286,10 @@ namespace Emby.Dlna.Main // Only get bind addresses in LAN var bindAddresses = _networkManager - .GetInternalBindAddresses() - .Where(i => i.Address.AddressFamily == AddressFamily.InterNetwork - || (i.AddressFamily == AddressFamily.InterNetworkV6 && i.Address.ScopeId != 0)) - .ToList(); + .GetInternalBindAddresses() + .Where(i => i.Address.AddressFamily == AddressFamily.InterNetwork + || (i.AddressFamily == AddressFamily.InterNetworkV6 && i.Address.ScopeId != 0)) + .ToList(); if (bindAddresses.Count == 0) { diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 333afd7484..42b66bbed3 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; +using System.Threading; using System.Threading.Tasks; using Jellyfin.Networking.Configuration; using MediaBrowser.Common.Configuration; @@ -34,7 +35,7 @@ namespace Jellyfin.Networking.Manager private readonly IConfigurationManager _configurationManager; - private readonly object _eventFireLock; + private readonly SemaphoreSlim _networkEvent; /// /// Holds the published server URLs and the IPs to use them on. @@ -86,7 +87,7 @@ namespace Jellyfin.Networking.Manager _interfaces = new List(); _macAddresses = new List(); _publishedServerUrls = new Dictionary(); - _eventFireLock = new object(); + _networkEvent = new SemaphoreSlim(1, 1); _remoteAddressFilter = new List(); UpdateSettings(_configurationManager.GetNetworkConfiguration()); @@ -143,7 +144,7 @@ namespace Jellyfin.Networking.Manager private void OnNetworkAvailabilityChanged(object? sender, NetworkAvailabilityEventArgs e) { _logger.LogDebug("Network availability changed."); - OnNetworkChanged(); + HandleNetworkChange(); } /// @@ -154,35 +155,34 @@ namespace Jellyfin.Networking.Manager private void OnNetworkAddressChanged(object? sender, EventArgs e) { _logger.LogDebug("Network address change detected."); - OnNetworkChanged(); + HandleNetworkChange(); } /// /// Triggers our event, and re-loads interface information. /// - private void OnNetworkChanged() + private void HandleNetworkChange() { - lock (_eventFireLock) + _networkEvent.Wait(); + if (!_eventfire) { - if (!_eventfire) - { - _logger.LogDebug("Network Address Change Event."); - // As network events tend to fire one after the other only fire once every second. - _eventfire = true; - OnNetworkChangeAsync().GetAwaiter().GetResult(); - } + _logger.LogDebug("Network Address Change Event."); + // As network events tend to fire one after the other only fire once every second. + _eventfire = true; + OnNetworkChange(); } + + _networkEvent.Release(); } /// - /// Async task that waits for 2 seconds before re-initialising the settings, as typically these events fire multiple times in succession. + /// Waits for 2 seconds before re-initialising the settings, as typically these events fire multiple times in succession. /// - /// A representing the asynchronous operation. - private async Task OnNetworkChangeAsync() + private void OnNetworkChange() { try { - await Task.Delay(2000).ConfigureAwait(false); + Thread.Sleep(2000); var networkConfig = _configurationManager.GetNetworkConfiguration(); InitialiseLan(networkConfig); InitialiseInterfaces(); @@ -234,7 +234,7 @@ namespace Jellyfin.Networking.Manager { var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name); interfaceObject.Index = ipProperties.GetIPv4Properties().Index; - interfaceObject.Name = adapter.Name.ToLowerInvariant(); + interfaceObject.Name = adapter.Name; _interfaces.Add(interfaceObject); } @@ -242,7 +242,7 @@ namespace Jellyfin.Networking.Manager { var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name); interfaceObject.Index = ipProperties.GetIPv6Properties().Index; - interfaceObject.Name = adapter.Name.ToLowerInvariant(); + interfaceObject.Name = adapter.Name; _interfaces.Add(interfaceObject); } @@ -362,11 +362,10 @@ namespace Jellyfin.Networking.Manager { // Remove potentially exisiting * and split config string into prefixes var virtualInterfacePrefixes = config.VirtualInterfaceNames - .Select(i => i.ToLowerInvariant() - .Replace("*", string.Empty, StringComparison.OrdinalIgnoreCase)); + .Select(i => i.Replace("*", string.Empty, StringComparison.OrdinalIgnoreCase)); // Check all interfaces for matches against the prefixes and remove them - if (_interfaces.Count > 0 && virtualInterfacePrefixes.Any()) + if (_interfaces.Count > 0) { foreach (var virtualInterfacePrefix in virtualInterfacePrefixes) { @@ -548,6 +547,7 @@ namespace Jellyfin.Networking.Manager _configurationManager.NamedConfigurationUpdated -= ConfigurationUpdated; NetworkChange.NetworkAddressChanged -= OnNetworkAddressChanged; NetworkChange.NetworkAvailabilityChanged -= OnNetworkAvailabilityChanged; + _networkEvent.Dispose(); } _disposed = true; @@ -566,7 +566,7 @@ namespace Jellyfin.Networking.Manager if (_interfaces != null) { // Match all interfaces starting with names starting with token - var matchedInterfaces = _interfaces.Where(s => s.Name.Equals(intf.ToLowerInvariant(), StringComparison.OrdinalIgnoreCase)).OrderBy(x => x.Index); + var matchedInterfaces = _interfaces.Where(s => s.Name.Equals(intf, StringComparison.OrdinalIgnoreCase)).OrderBy(x => x.Index); if (matchedInterfaces.Any()) { _logger.LogInformation("Interface {Token} used in settings. Using its interface addresses.", intf); @@ -609,7 +609,7 @@ namespace Jellyfin.Networking.Manager return false; } } - else if (!_lanSubnets.Where(x => x.Contains(remoteIp)).Any()) + else if (!_lanSubnets.Any(x => x.Contains(remoteIp))) { // Remote not enabled. So everyone should be LAN. return false; @@ -875,10 +875,12 @@ namespace Jellyfin.Networking.Manager bindPreference = string.Empty; port = null; - var validPublishedServerUrls = _publishedServerUrls.Where(x => x.Key.Address.Equals(IPAddress.Any)).ToList(); - validPublishedServerUrls.AddRange(_publishedServerUrls.Where(x => x.Key.Address.Equals(IPAddress.IPv6Any))); - validPublishedServerUrls.AddRange(_publishedServerUrls.Where(x => x.Key.Subnet.Contains(source))); - validPublishedServerUrls = validPublishedServerUrls.GroupBy(x => x.Key).Select(y => y.First()).ToList(); + var validPublishedServerUrls = _publishedServerUrls.Where(x => x.Key.Address.Equals(IPAddress.Any) + || x.Key.Address.Equals(IPAddress.IPv6Any) + || x.Key.Subnet.Contains(source)) + .GroupBy(x => x.Key) + .Select(y => y.First()) + .ToList(); // Check for user override. foreach (var data in validPublishedServerUrls) diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 25516271c5..a1adddcbb3 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -348,9 +348,9 @@ namespace Jellyfin.Server.Extensions { AddIpAddress(config, options, addr, addr.AddressFamily == AddressFamily.InterNetwork ? 32 : 128); } - else if (NetworkExtensions.TryParseSubnets(new[] { allowedProxies[i] }, out var subnets)) + else if (NetworkExtensions.TryParseSubnet(allowedProxies[i], out var subnet)) { - foreach (var subnet in subnets) + if (subnet != null) { AddIpAddress(config, options, subnet.Prefix, subnet.PrefixLength); } diff --git a/MediaBrowser.Common/Net/NetworkExtensions.cs b/MediaBrowser.Common/Net/NetworkExtensions.cs index 3d7d58b273..d7632c0deb 100644 --- a/MediaBrowser.Common/Net/NetworkExtensions.cs +++ b/MediaBrowser.Common/Net/NetworkExtensions.cs @@ -138,7 +138,7 @@ namespace MediaBrowser.Common.Net /// /// Try parsing an array of strings into subnets, respecting exclusions. /// - /// Input string to be parsed. + /// Input string array to be parsed. /// Collection of . /// Boolean signaling if negated or not negated values should be parsed. /// True if parsing was successful. @@ -190,6 +190,58 @@ namespace MediaBrowser.Common.Net return false; } + /// + /// Try parsing a string into a subnet, respecting exclusions. + /// + /// Input string to be parsed. + /// An . + /// Boolean signaling if negated or not negated values should be parsed. + /// True if parsing was successful. + public static bool TryParseSubnet(string value, out IPNetwork? result, bool negated = false) + { + result = null; + + if (string.IsNullOrEmpty(value)) + { + return false; + } + + string[] v = value.Trim().Split("/"); + + var address = IPAddress.None; + if (negated && v[0].StartsWith('!')) + { + _ = IPAddress.TryParse(v[0][1..], out address); + } + else if (!negated) + { + _ = IPAddress.TryParse(v[0][0..], out address); + } + + if (address != IPAddress.None && address != null) + { + if (int.TryParse(v[1], out var netmask)) + { + result = new IPNetwork(address, netmask); + } + else if (address.AddressFamily == AddressFamily.InterNetwork) + { + result = new IPNetwork(address, 32); + } + else if (address.AddressFamily == AddressFamily.InterNetworkV6) + { + result = new IPNetwork(address, 128); + } + } + + if (result != null) + { + return true; + } + + return false; + } + /// /// Attempts to parse a host string. /// diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs index 92c9c83c0f..da357546dd 100644 --- a/RSSDP/SsdpCommunicationsServer.cs +++ b/RSSDP/SsdpCommunicationsServer.cs @@ -117,7 +117,7 @@ namespace Rssdp.Infrastructure { try { - _BroadcastListenSockets = ListenForBroadcastsAsync(); + _BroadcastListenSockets = ListenForBroadcasts(); } catch (SocketException ex) { @@ -333,7 +333,7 @@ namespace Rssdp.Infrastructure return Task.CompletedTask; } - private List ListenForBroadcastsAsync() + private List ListenForBroadcasts() { var sockets = new List(); if (_enableMultiSocketBinding) @@ -352,7 +352,7 @@ namespace Rssdp.Infrastructure } catch (Exception ex) { - _logger.LogError(ex, "Error in ListenForBroadcastsAsync. IPAddress: {0}", address); + _logger.LogError(ex, "Error in ListenForBroadcasts. IPAddress: {0}", address); } } } From 87d0158a4af9d8c982736cbae77019fa6dd03126 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Sat, 15 Oct 2022 17:27:37 +0200 Subject: [PATCH 028/858] Fix autodiscovery --- .../EntryPoints/UdpServerEntryPoint.cs | 9 ++- Jellyfin.Networking/Manager/NetworkManager.cs | 59 +++++++++---------- MediaBrowser.Common/Net/NetworkExtensions.cs | 32 +++++++++- 3 files changed, 67 insertions(+), 33 deletions(-) diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs index 82c6abb8c8..01a987b6ab 100644 --- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs +++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs @@ -79,6 +79,10 @@ namespace Emby.Server.Implementations.EntryPoints { if (_enableMultiSocketBinding) { + // Add global broadcast socket + _udpServers.Add(new UdpServer(_logger, _appHost, _config, System.Net.IPAddress.Broadcast, PortNumber)); + + // Add bind address specific broadcast sockets foreach (var bindAddress in _networkManager.GetInternalBindAddresses()) { if (bindAddress.AddressFamily == AddressFamily.InterNetworkV6) @@ -87,7 +91,10 @@ namespace Emby.Server.Implementations.EntryPoints continue; } - _udpServers.Add(new UdpServer(_logger, _appHost, _config, bindAddress.Address, PortNumber)); + var broadcastAddress = NetworkExtensions.GetBroadcastAddress(bindAddress.Subnet); + _logger.LogDebug("Binding UDP server to {Address}", broadcastAddress.ToString()); + + _udpServers.Add(new UdpServer(_logger, _appHost, _config, broadcastAddress, PortNumber)); } } else diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 42b66bbed3..3c347461a7 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -280,7 +280,7 @@ namespace Jellyfin.Networking.Manager } _logger.LogDebug("Discovered {0} interfaces.", _interfaces.Count); - _logger.LogDebug("Interfaces addresses : {0}", _interfaces.Select(s => s.Address).ToString()); + _logger.LogDebug("Interfaces addresses : {0}", _interfaces.Select(s => s.Address.ToString())); } } @@ -726,20 +726,11 @@ namespace Jellyfin.Networking.Manager } bool isExternal = !_lanSubnets.Any(network => network.Contains(source)); - _logger.LogDebug("GetBindInterface with source. External: {IsExternal}:", isExternal); + _logger.LogDebug("GetBindInterface with source {Source}. External: {IsExternal}:", source, isExternal); - if (MatchesPublishedServerUrl(source, isExternal, out string res, out port)) + if (MatchesPublishedServerUrl(source, isExternal, out result)) { - if (port != null) - { - _logger.LogInformation("{Source}: Using BindAddress {Address}:{Port}", source, res, port); - } - else - { - _logger.LogInformation("{Source}: Using BindAddress {Address}", source, res); - } - - return res; + return result; } // No preference given, so move on to bind addresses. @@ -868,41 +859,37 @@ namespace Jellyfin.Networking.Manager /// IP source address to use. /// True if the source is in an external subnet. /// The published server URL that matches the source address. - /// The resultant port, if one exists. /// true if a match is found, false otherwise. - private bool MatchesPublishedServerUrl(IPAddress source, bool isInExternalSubnet, out string bindPreference, out int? port) + private bool MatchesPublishedServerUrl(IPAddress source, bool isInExternalSubnet, out string bindPreference) { bindPreference = string.Empty; - port = null; + int? port = null; var validPublishedServerUrls = _publishedServerUrls.Where(x => x.Key.Address.Equals(IPAddress.Any) || x.Key.Address.Equals(IPAddress.IPv6Any) || x.Key.Subnet.Contains(source)) .GroupBy(x => x.Key) - .Select(y => y.First()) + .Select(x => x.First()) + .OrderBy(x => x.Key.Address.Equals(IPAddress.Any) + || x.Key.Address.Equals(IPAddress.IPv6Any)) .ToList(); // Check for user override. foreach (var data in validPublishedServerUrls) { - // Get address interface - var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(s => s.Subnet.Contains(data.Key.Address)); + // Get address interface. + var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Key.Subnet.Contains(x.Address)); - // Remaining. Match anything. - if (data.Key.Address.Equals(IPAddress.Broadcast)) - { - bindPreference = data.Value; - break; - } - else if ((data.Key.Address.Equals(IPAddress.Any) || data.Key.Address.Equals(IPAddress.IPv6Any)) && isInExternalSubnet) + if (isInExternalSubnet && (data.Key.Address.Equals(IPAddress.Any) || data.Key.Address.Equals(IPAddress.IPv6Any))) { // External. bindPreference = data.Value; break; } - else if (intf?.Address != null) + + if (intf?.Address != null) { - // Match ip address. + // Match IP address. bindPreference = data.Value; break; } @@ -910,6 +897,7 @@ namespace Jellyfin.Networking.Manager if (string.IsNullOrEmpty(bindPreference)) { + _logger.LogInformation("{Source}: No matching bind address override found", source); return false; } @@ -924,6 +912,15 @@ namespace Jellyfin.Networking.Manager } } + if (port != null) + { + _logger.LogInformation("{Source}: Matching bind address override found {Address}:{Port}", source, bindPreference, port); + } + else + { + _logger.LogInformation("{Source}: Matching bind address override found {Address}", source, bindPreference); + } + return true; } @@ -967,12 +964,12 @@ namespace Jellyfin.Networking.Manager if (bindAddress != null) { result = NetworkExtensions.FormatIpString(bindAddress); - _logger.LogDebug("{Source}: GetBindInterface: Has source, found a matching external bind interface: {Result}", source, result); + _logger.LogDebug("{Source}: External request received, matching external bind interface found: {Result}", source, result); return true; } } - _logger.LogWarning("{Source}: External request received, no external interface bind found, trying internal interfaces.", source); + _logger.LogWarning("{Source}: External request received, no matching external bind interface found, trying internal interfaces.", source); } else { @@ -987,7 +984,7 @@ namespace Jellyfin.Networking.Manager if (bindAddress != null) { result = NetworkExtensions.FormatIpString(bindAddress); - _logger.LogWarning("{Source}: Request received, matching internal interface bind found: {Result}", source, result); + _logger.LogDebug("{Source}: Internal request received, matching internal bind interface found: {Result}", source, result); return true; } } diff --git a/MediaBrowser.Common/Net/NetworkExtensions.cs b/MediaBrowser.Common/Net/NetworkExtensions.cs index d7632c0deb..1c2b65346c 100644 --- a/MediaBrowser.Common/Net/NetworkExtensions.cs +++ b/MediaBrowser.Common/Net/NetworkExtensions.cs @@ -56,7 +56,23 @@ namespace MediaBrowser.Common.Net /// String value of the subnet mask in dotted decimal notation. public static IPAddress CidrToMask(byte cidr, AddressFamily family) { - uint addr = 0xFFFFFFFF << (family == AddressFamily.InterNetwork ? 32 : 128 - cidr); + uint addr = 0xFFFFFFFF << ((family == AddressFamily.InterNetwork ? 32 : 128) - cidr); + addr = ((addr & 0xff000000) >> 24) + | ((addr & 0x00ff0000) >> 8) + | ((addr & 0x0000ff00) << 8) + | ((addr & 0x000000ff) << 24); + return new IPAddress(addr); + } + + /// + /// Convert a subnet mask in CIDR notation to a dotted decimal string value. IPv4 only. + /// + /// Subnet mask in CIDR notation. + /// IPv4 or IPv6 family. + /// String value of the subnet mask in dotted decimal notation. + public static IPAddress CidrToMask(int cidr, AddressFamily family) + { + uint addr = 0xFFFFFFFF << ((family == AddressFamily.InterNetwork ? 32 : 128) - cidr); addr = ((addr & 0xff000000) >> 24) | ((addr & 0x00ff0000) >> 8) | ((addr & 0x0000ff00) << 8) @@ -319,5 +335,19 @@ namespace MediaBrowser.Common.Net addresses = Array.Empty(); return false; } + + /// + /// Gets the broadcast address for a . + /// + /// The . + /// The broadcast address. + public static IPAddress GetBroadcastAddress(IPNetwork network) + { + uint ipAddress = BitConverter.ToUInt32(network.Prefix.GetAddressBytes(), 0); + uint ipMaskV4 = BitConverter.ToUInt32(CidrToMask(network.PrefixLength, AddressFamily.InterNetwork).GetAddressBytes(), 0); + uint broadCastIpAddress = ipAddress | ~ipMaskV4; + + return new IPAddress(BitConverter.GetBytes(broadCastIpAddress)); + } } } From 26d79a5ce3639700131d0359c60c096cd0fbb093 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Sat, 15 Oct 2022 20:57:02 +0200 Subject: [PATCH 029/858] Properly name some bind address functions, cleanup logging --- .../ApplicationHost.cs | 4 +- .../EntryPoints/UdpServerEntryPoint.cs | 2 +- Jellyfin.Networking/Manager/NetworkManager.cs | 47 +++++++++---------- MediaBrowser.Common/Net/INetworkManager.cs | 6 +-- 4 files changed, 29 insertions(+), 30 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 2ca5e6ded8..7004af3d43 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1069,7 +1069,7 @@ namespace Emby.Server.Implementations return PublishedServerUrl.Trim('/'); } - string smart = NetManager.GetBindInterface(remoteAddr, out var port); + string smart = NetManager.GetBindAddress(remoteAddr, out var port); return GetLocalApiUrl(smart.Trim('/'), null, port); } @@ -1118,7 +1118,7 @@ namespace Emby.Server.Implementations public string GetApiUrlForLocalAccess(IPAddress ipAddress = null, bool allowHttps = true) { // With an empty source, the port will be null - var smart = NetManager.GetBindInterface(ipAddress, out _); + var smart = NetManager.GetBindAddress(ipAddress, out _); var scheme = !allowHttps ? Uri.UriSchemeHttp : null; int? port = !allowHttps ? HttpPort : null; return GetLocalApiUrl(smart, scheme, port); diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs index 01a987b6ab..c2ffaf1bdd 100644 --- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs +++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs @@ -92,7 +92,7 @@ namespace Emby.Server.Implementations.EntryPoints } var broadcastAddress = NetworkExtensions.GetBroadcastAddress(bindAddress.Subnet); - _logger.LogDebug("Binding UDP server to {Address}", broadcastAddress.ToString()); + _logger.LogDebug("Binding UDP server to {Address} on port {PortNumber}", broadcastAddress.ToString(), PortNumber); _udpServers.Add(new UdpServer(_logger, _appHost, _config, broadcastAddress, PortNumber)); } diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 3c347461a7..7e9fb4df71 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -6,7 +6,6 @@ using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; using System.Threading; -using System.Threading.Tasks; using Jellyfin.Networking.Configuration; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; @@ -280,7 +279,7 @@ namespace Jellyfin.Networking.Manager } _logger.LogDebug("Discovered {0} interfaces.", _interfaces.Count); - _logger.LogDebug("Interfaces addresses : {0}", _interfaces.Select(s => s.Address.ToString())); + _logger.LogDebug("Interfaces addresses: {0}", _interfaces.OrderByDescending(s => s.AddressFamily == AddressFamily.InterNetwork).Select(s => s.Address.ToString())); } } @@ -320,8 +319,8 @@ namespace Jellyfin.Networking.Manager } } - _logger.LogInformation("Defined LAN addresses : {0}", _lanSubnets.Select(s => s.Prefix + "/" + s.PrefixLength)); - _logger.LogInformation("Defined LAN exclusions : {0}", _excludedSubnets.Select(s => s.Prefix + "/" + s.PrefixLength)); + _logger.LogInformation("Defined LAN addresses: {0}", _lanSubnets.Select(s => s.Prefix + "/" + s.PrefixLength)); + _logger.LogInformation("Defined LAN exclusions: {0}", _excludedSubnets.Select(s => s.Prefix + "/" + s.PrefixLength)); _logger.LogInformation("Using LAN addresses: {0}", _lanSubnets.Where(s => !_excludedSubnets.Contains(s)).Select(s => s.Prefix + "/" + s.PrefixLength)); } } @@ -386,7 +385,7 @@ namespace Jellyfin.Networking.Manager _interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetworkV6); } - _logger.LogInformation("Using bind addresses: {0}", _interfaces.Select(x => x.Address)); + _logger.LogInformation("Using bind addresses: {0}", _interfaces.OrderByDescending(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => x.Address)); } } @@ -691,7 +690,7 @@ namespace Jellyfin.Networking.Manager public string GetBindInterface(string source, out int? port) { _ = NetworkExtensions.TryParseHost(source, out var address, IsIpv4Enabled, IsIpv6Enabled); - var result = GetBindInterface(address.FirstOrDefault(), out port); + var result = GetBindAddress(address.FirstOrDefault(), out port); return result; } @@ -700,14 +699,14 @@ namespace Jellyfin.Networking.Manager { string result; _ = NetworkExtensions.TryParseHost(source.Host.Host, out var addresses, IsIpv4Enabled, IsIpv6Enabled); - result = GetBindInterface(addresses.FirstOrDefault(), out port); + result = GetBindAddress(addresses.FirstOrDefault(), out port); port ??= source.Host.Port; return result; } /// - public string GetBindInterface(IPAddress? source, out int? port) + public string GetBindAddress(IPAddress? source, out int? port) { port = null; @@ -726,7 +725,7 @@ namespace Jellyfin.Networking.Manager } bool isExternal = !_lanSubnets.Any(network => network.Contains(source)); - _logger.LogDebug("GetBindInterface with source {Source}. External: {IsExternal}:", source, isExternal); + _logger.LogDebug("Trying to get bind address for source {Source} - External: {IsExternal}", source, isExternal); if (MatchesPublishedServerUrl(source, isExternal, out result)) { @@ -759,7 +758,7 @@ namespace Jellyfin.Networking.Manager if (intf.Address.Equals(source)) { result = NetworkExtensions.FormatIpString(intf.Address); - _logger.LogDebug("{Source}: GetBindInterface: Has found matching interface: {Result}", source, result); + _logger.LogDebug("{Source}: Found matching interface to use as bind address: {Result}", source, result); return result; } } @@ -771,20 +770,20 @@ namespace Jellyfin.Networking.Manager if (intf.Subnet.Contains(source)) { result = NetworkExtensions.FormatIpString(intf.Address); - _logger.LogDebug("{Source}: GetBindInterface: Has source, matched best internal interface on range: {Result}", source, result); + _logger.LogDebug("{Source}: Found internal interface with matching subnet, using it as bind address: {Result}", source, result); return result; } } } result = NetworkExtensions.FormatIpString(availableInterfaces.First().Address); - _logger.LogDebug("{Source}: GetBindInterface: Matched first internal interface: {Result}", source, result); + _logger.LogDebug("{Source}: Using first internal interface as bind address: {Result}", source, result); return result; } // There isn't any others, so we'll use the loopback. result = IsIpv4Enabled && !IsIpv6Enabled ? "127.0.0.1" : "::1"; - _logger.LogWarning("{Source}: GetBindInterface: Loopback {Result} returned.", source, result); + _logger.LogWarning("{Source}: Only loopback {Result} returned, using that as bind address.", source, result); return result; } @@ -897,7 +896,7 @@ namespace Jellyfin.Networking.Manager if (string.IsNullOrEmpty(bindPreference)) { - _logger.LogInformation("{Source}: No matching bind address override found", source); + _logger.LogDebug("{Source}: No matching bind address override found.", source); return false; } @@ -914,18 +913,18 @@ namespace Jellyfin.Networking.Manager if (port != null) { - _logger.LogInformation("{Source}: Matching bind address override found {Address}:{Port}", source, bindPreference, port); + _logger.LogDebug("{Source}: Matching bind address override found: {Address}:{Port}", source, bindPreference, port); } else { - _logger.LogInformation("{Source}: Matching bind address override found {Address}", source, bindPreference); + _logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference); } return true; } /// - /// Attempts to match the source against a user defined bind interface. + /// Attempts to match the source against the user defined bind interfaces. /// /// IP source address to use. /// True if the source is in the external subnet. @@ -964,12 +963,12 @@ namespace Jellyfin.Networking.Manager if (bindAddress != null) { result = NetworkExtensions.FormatIpString(bindAddress); - _logger.LogDebug("{Source}: External request received, matching external bind interface found: {Result}", source, result); + _logger.LogDebug("{Source}: External request received, matching external bind address found: {Result}", source, result); return true; } } - _logger.LogWarning("{Source}: External request received, no matching external bind interface found, trying internal interfaces.", source); + _logger.LogWarning("{Source}: External request received, no matching external bind address found, trying internal addresses.", source); } else { @@ -984,7 +983,7 @@ namespace Jellyfin.Networking.Manager if (bindAddress != null) { result = NetworkExtensions.FormatIpString(bindAddress); - _logger.LogDebug("{Source}: Internal request received, matching internal bind interface found: {Result}", source, result); + _logger.LogDebug("{Source}: Internal request received, matching internal bind address found: {Result}", source, result); return true; } } @@ -994,7 +993,7 @@ namespace Jellyfin.Networking.Manager } /// - /// Attempts to match the source against an external interface. + /// Attempts to match the source against external interfaces. /// /// IP source address to use. /// The result, if a match is found. @@ -1014,7 +1013,7 @@ namespace Jellyfin.Networking.Manager if (!IsInLocalNetwork(intf.Address) && intf.Subnet.Contains(source)) { result = NetworkExtensions.FormatIpString(intf.Address); - _logger.LogDebug("{Source}: GetBindInterface: Selected best external on interface on range: {Result}", source, result); + _logger.LogDebug("{Source}: Found external interface with matching subnet, using it as bind address: {Result}", source, result); return true; } } @@ -1022,11 +1021,11 @@ namespace Jellyfin.Networking.Manager if (hasResult != null) { result = NetworkExtensions.FormatIpString(hasResult); - _logger.LogDebug("{Source}: GetBindInterface: Selected first external interface: {Result}", source, result); + _logger.LogDebug("{Source}: Using first external interface as bind address: {Result}", source, result); return true; } - _logger.LogDebug("{Source}: External request received, but no WAN interface found. Need to route through internal network.", source); + _logger.LogWarning("{Source}: External request received, but no external interface found. Need to route through internal network.", source); return false; } } diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs index d462e954a8..f0f16af78f 100644 --- a/MediaBrowser.Common/Net/INetworkManager.cs +++ b/MediaBrowser.Common/Net/INetworkManager.cs @@ -75,17 +75,17 @@ namespace MediaBrowser.Common.Net /// /// Retrieves the bind address to use in system URLs. (Server Discovery, PlayTo, LiveTV, SystemInfo) /// If no bind addresses are specified, an internal interface address is selected. - /// (See . + /// (See . /// /// IP address of the request. /// Optional port returned, if it's part of an override. /// IP address to use, or loopback address if all else fails. - string GetBindInterface(IPAddress source, out int? port); + string GetBindAddress(IPAddress source, out int? port); /// /// Retrieves the bind address to use in system URLs. (Server Discovery, PlayTo, LiveTV, SystemInfo) /// If no bind addresses are specified, an internal interface address is selected. - /// (See . + /// (See . /// /// Source of the request. /// Optional port returned, if it's part of an override. From f6d6f0367bf62435dfaf7d122415d31977f889aa Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Mon, 17 Oct 2022 15:38:42 +0200 Subject: [PATCH 030/858] Properly handle IPs with subnetmasks --- Jellyfin.Networking/Manager/NetworkManager.cs | 54 +++++++-------- .../ApiServiceCollectionExtensions.cs | 2 +- MediaBrowser.Common/Net/NetworkExtensions.cs | 26 +++++--- .../NetworkParseTests.cs | 65 ++++++++++--------- 4 files changed, 81 insertions(+), 66 deletions(-) diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 7e9fb4df71..4a32423e5e 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -295,8 +295,8 @@ namespace Jellyfin.Networking.Manager // Get configuration options string[] subnets = config.LocalNetworkSubnets; - _ = NetworkExtensions.TryParseSubnets(subnets, out _lanSubnets, false); - _ = NetworkExtensions.TryParseSubnets(subnets, out _excludedSubnets, true); + _ = NetworkExtensions.TryParseToSubnets(subnets, out _lanSubnets, false); + _ = NetworkExtensions.TryParseToSubnets(subnets, out _excludedSubnets, true); if (_lanSubnets.Count == 0) { @@ -336,8 +336,8 @@ namespace Jellyfin.Networking.Manager var localNetworkAddresses = config.LocalNetworkAddresses; if (localNetworkAddresses.Length > 0 && !string.IsNullOrWhiteSpace(localNetworkAddresses.First())) { - var bindAddresses = localNetworkAddresses.Select(p => IPAddress.TryParse(p, out var addresses) - ? addresses + var bindAddresses = localNetworkAddresses.Select(p => NetworkExtensions.TryParseToSubnet(p, out var network) + ? network.Prefix : (_interfaces.Where(x => x.Name.Equals(p, StringComparison.OrdinalIgnoreCase)) .Select(x => x.Address) .FirstOrDefault() ?? IPAddress.None)) @@ -401,7 +401,7 @@ namespace Jellyfin.Networking.Manager if (remoteIPFilter.Any() && !string.IsNullOrWhiteSpace(remoteIPFilter.First())) { // Parse all IPs with netmask to a subnet - _ = NetworkExtensions.TryParseSubnets(remoteIPFilter.Where(x => x.Contains('/', StringComparison.OrdinalIgnoreCase)).ToArray(), out _remoteAddressFilter, false); + _ = NetworkExtensions.TryParseToSubnets(remoteIPFilter.Where(x => x.Contains('/', StringComparison.OrdinalIgnoreCase)).ToArray(), out _remoteAddressFilter, false); // Parse everything else as an IP and construct subnet with a single IP var ips = remoteIPFilter.Where(x => !x.Contains('/', StringComparison.OrdinalIgnoreCase)); @@ -440,17 +440,17 @@ namespace Jellyfin.Networking.Manager else { var replacement = parts[1].Trim(); - var ipParts = parts[0].Split("/"); - if (string.Equals(parts[0], "all", StringComparison.OrdinalIgnoreCase)) + var identifier = parts[0]; + if (string.Equals(identifier, "all", StringComparison.OrdinalIgnoreCase)) { _publishedServerUrls[new IPData(IPAddress.Broadcast, null)] = replacement; } - else if (string.Equals(parts[0], "external", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(identifier, "external", StringComparison.OrdinalIgnoreCase)) { _publishedServerUrls[new IPData(IPAddress.Any, new IPNetwork(IPAddress.Any, 0))] = replacement; _publishedServerUrls[new IPData(IPAddress.IPv6Any, new IPNetwork(IPAddress.IPv6Any, 0))] = replacement; } - else if (string.Equals(parts[0], "internal", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(identifier, "internal", StringComparison.OrdinalIgnoreCase)) { foreach (var lan in _lanSubnets) { @@ -458,17 +458,12 @@ namespace Jellyfin.Networking.Manager _publishedServerUrls[new IPData(lanPrefix, new IPNetwork(lanPrefix, lan.PrefixLength))] = replacement; } } - else if (IPAddress.TryParse(ipParts[0], out IPAddress? result)) + else if (NetworkExtensions.TryParseToSubnet(identifier, out var result) && result != null) { - var data = new IPData(result, null); - if (ipParts.Length > 1 && int.TryParse(ipParts[1], out var netmask)) - { - data.Subnet = new IPNetwork(result, netmask); - } - + var data = new IPData(result.Prefix, result); _publishedServerUrls[data] = replacement; } - else if (TryParseInterface(ipParts[0], out var ifaces)) + else if (TryParseInterface(identifier, out var ifaces)) { foreach (var iface in ifaces) { @@ -516,15 +511,20 @@ namespace Jellyfin.Networking.Manager foreach (var details in interfaceList) { var parts = details.Split(','); - var split = parts[0].Split("/"); - var address = IPAddress.Parse(split[0]); - var network = new IPNetwork(address, int.Parse(split[1], CultureInfo.InvariantCulture)); - var index = int.Parse(parts[1], CultureInfo.InvariantCulture); - if (address.AddressFamily == AddressFamily.InterNetwork || address.AddressFamily == AddressFamily.InterNetworkV6) + if (NetworkExtensions.TryParseToSubnet(parts[0], out var subnet)) { - var data = new IPData(address, network, parts[2]); - data.Index = index; - _interfaces.Add(data); + var address = subnet.Prefix; + var index = int.Parse(parts[1], CultureInfo.InvariantCulture); + if (address.AddressFamily == AddressFamily.InterNetwork || address.AddressFamily == AddressFamily.InterNetworkV6) + { + var data = new IPData(address, subnet, parts[2]); + data.Index = index; + _interfaces.Add(data); + } + } + else + { + _logger.LogWarning("Could not parse mock interface settings: {Part}", details); } } } @@ -799,9 +799,9 @@ namespace Jellyfin.Networking.Manager /// public bool IsInLocalNetwork(string address) { - if (IPAddress.TryParse(address, out var ep)) + if (NetworkExtensions.TryParseToSubnet(address, out var subnet)) { - return IPAddress.IsLoopback(ep) || (_lanSubnets.Any(x => x.Contains(ep)) && !_excludedSubnets.Any(x => x.Contains(ep))); + return IPAddress.IsLoopback(subnet.Prefix) || (_lanSubnets.Any(x => x.Contains(subnet.Prefix)) && !_excludedSubnets.Any(x => x.Contains(subnet.Prefix))); } if (NetworkExtensions.TryParseHost(address, out var addresses, IsIpv4Enabled, IsIpv6Enabled)) diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index a1adddcbb3..439147bfd9 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -348,7 +348,7 @@ namespace Jellyfin.Server.Extensions { AddIpAddress(config, options, addr, addr.AddressFamily == AddressFamily.InterNetwork ? 32 : 128); } - else if (NetworkExtensions.TryParseSubnet(allowedProxies[i], out var subnet)) + else if (NetworkExtensions.TryParseToSubnet(allowedProxies[i], out var subnet)) { if (subnet != null) { diff --git a/MediaBrowser.Common/Net/NetworkExtensions.cs b/MediaBrowser.Common/Net/NetworkExtensions.cs index 1c2b65346c..e37a5dc6bc 100644 --- a/MediaBrowser.Common/Net/NetworkExtensions.cs +++ b/MediaBrowser.Common/Net/NetworkExtensions.cs @@ -152,13 +152,14 @@ namespace MediaBrowser.Common.Net } /// - /// Try parsing an array of strings into subnets, respecting exclusions. + /// Try parsing an array of strings into objects, respecting exclusions. + /// Elements without a subnet mask will be represented as with a single IP. /// /// Input string array to be parsed. /// Collection of . /// Boolean signaling if negated or not negated values should be parsed. /// True if parsing was successful. - public static bool TryParseSubnets(string[] values, out List result, bool negated = false) + public static bool TryParseToSubnets(string[] values, out List result, bool negated = false) { result = new List(); @@ -183,10 +184,14 @@ namespace MediaBrowser.Common.Net if (address != IPAddress.None && address != null) { - if (int.TryParse(v[1], out var netmask)) + if (v.Length > 1 && int.TryParse(v[1], out var netmask)) { result.Add(new IPNetwork(address, netmask)); } + else if (v.Length > 1 && IPAddress.TryParse(v[1], out var netmaskAddress)) + { + result.Add(new IPNetwork(address, NetworkExtensions.MaskToCidr(netmaskAddress))); + } else if (address.AddressFamily == AddressFamily.InterNetwork) { result.Add(new IPNetwork(address, 32)); @@ -207,15 +212,16 @@ namespace MediaBrowser.Common.Net } /// - /// Try parsing a string into a subnet, respecting exclusions. + /// Try parsing a string into an , respecting exclusions. + /// Inputs without a subnet mask will be represented as with a single IP. /// /// Input string to be parsed. /// An . /// Boolean signaling if negated or not negated values should be parsed. /// True if parsing was successful. - public static bool TryParseSubnet(string value, out IPNetwork? result, bool negated = false) + public static bool TryParseToSubnet(string value, out IPNetwork result, bool negated = false) { - result = null; + result = new IPNetwork(IPAddress.None, 32); if (string.IsNullOrEmpty(value)) { @@ -236,10 +242,14 @@ namespace MediaBrowser.Common.Net if (address != IPAddress.None && address != null) { - if (int.TryParse(v[1], out var netmask)) + if (v.Length > 1 && int.TryParse(v[1], out var netmask)) { result = new IPNetwork(address, netmask); } + else if (v.Length > 1 && IPAddress.TryParse(v[1], out var netmaskAddress)) + { + result = new IPNetwork(address, NetworkExtensions.MaskToCidr(netmaskAddress)); + } else if (address.AddressFamily == AddressFamily.InterNetwork) { result = new IPNetwork(address, 32); @@ -250,7 +260,7 @@ namespace MediaBrowser.Common.Net } } - if (result != null) + if (!result.Prefix.Equals(IPAddress.None)) { return true; } diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs index d492b64393..86b2ab21a3 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs @@ -1,13 +1,11 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Net; using Jellyfin.Networking.Configuration; using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; -using Microsoft.AspNetCore.HttpOverrides; using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; @@ -37,6 +35,8 @@ namespace Jellyfin.Networking.Tests [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]")] // eth16 only [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]")] + // eth16 only without mask + [InlineData("192.168.1.208,-16,eth16|200.200.200.200,11,eth11", "192.168.1.0/24", "[192.168.1.208/32]")] // All interfaces excluded. (including loopbacks) [InlineData("192.168.1.208/24,-16,vEthernet1|192.168.2.208/24,-16,vEthernet212|200.200.200.200/24,11,eth11", "192.168.1.0/24", "[]")] // vEthernet1 and vEthernet212 should be excluded. @@ -65,66 +65,79 @@ namespace Jellyfin.Networking.Tests /// IP Address. [Theory] [InlineData("127.0.0.1")] + [InlineData("127.0.0.1/8")] + [InlineData("192.168.1.2")] + [InlineData("192.168.1.2/24")] + [InlineData("192.168.1.2/255.255.255.0")] [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517")] [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517]")] [InlineData("fe80::7add:12ff:febb:c67b%16")] [InlineData("[fe80::7add:12ff:febb:c67b%16]:123")] [InlineData("fe80::7add:12ff:febb:c67b%16:123")] [InlineData("[fe80::7add:12ff:febb:c67b%16]")] - [InlineData("192.168.1.2")] + [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517/56")] public static void TryParseValidIPStringsTrue(string address) - => Assert.True(IPAddress.TryParse(address, out _)); + => Assert.True(NetworkExtensions.TryParseToSubnet(address, out _)); /// /// Checks invalid IP address formats. /// /// IP Address. [Theory] - [InlineData("192.168.1.2/255.255.255.0")] - [InlineData("192.168.1.2/24")] - [InlineData("127.0.0.1/8")] [InlineData("127.0.0.1#")] [InlineData("localhost!")] [InlineData("256.128.0.0.0.1")] - [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517/56")] [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517:1231")] [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517:1231]")] public static void TryParseInvalidIPStringsFalse(string address) - => Assert.False(IPAddress.TryParse(address, out _)); + => Assert.False(NetworkExtensions.TryParseToSubnet(address, out _)); + /// + /// Checks if IPv4 address is within a defined subnet. + /// + /// Network mask. + /// IP Address. [Theory] [InlineData("192.168.5.85/24", "192.168.5.1")] [InlineData("192.168.5.85/24", "192.168.5.254")] + [InlineData("192.168.5.85/255.255.255.0", "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("10.128.240.50/255.255.255.252", "10.128.240.51")] [InlineData("127.0.0.1/8", "127.0.0.1")] public void IpV4SubnetMaskMatchesValidIpAddress(string netMask, string ipAddress) { - var split = netMask.Split("/"); - var mask = int.Parse(split[1], CultureInfo.InvariantCulture); - var ipa = IPAddress.Parse(split[0]); - var ipn = new IPNetwork(ipa, mask); - Assert.True(ipn.Contains(IPAddress.Parse(ipAddress))); + var ipa = IPAddress.Parse(ipAddress); + Assert.True(NetworkExtensions.TryParseToSubnet(netMask, out var subnet) && subnet.Contains(IPAddress.Parse(ipAddress))); } + /// + /// Checks if IPv4 address is not within a defined subnet. + /// + /// Network mask. + /// IP Address. [Theory] [InlineData("192.168.5.85/24", "192.168.4.254")] [InlineData("192.168.5.85/24", "191.168.5.254")] + [InlineData("192.168.5.85/255.255.255.252", "192.168.4.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")] + [InlineData("10.128.240.50/255.255.255.252", "10.127.240.51")] public void IpV4SubnetMaskDoesNotMatchInvalidIpAddress(string netMask, string ipAddress) { - var split = netMask.Split("/"); - var mask = int.Parse(split[1], CultureInfo.InvariantCulture); - var ipa = IPAddress.Parse(split[0]); - var ipn = new IPNetwork(ipa, mask); - Assert.False(ipn.Contains(IPAddress.Parse(ipAddress))); + var ipa = IPAddress.Parse(ipAddress); + Assert.False(NetworkExtensions.TryParseToSubnet(netMask, out var subnet) && subnet.Contains(IPAddress.Parse(ipAddress))); } + /// + /// Checks if IPv6 address is within a defined subnet. + /// + /// Network mask. + /// IP Address. [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")] @@ -133,11 +146,7 @@ namespace Jellyfin.Networking.Tests [InlineData("2001:db8:abcd:0012::0/128", "2001:0DB8:ABCD:0012:0000:0000:0000:0000")] public void IpV6SubnetMaskMatchesValidIpAddress(string netMask, string ipAddress) { - var split = netMask.Split("/"); - var mask = int.Parse(split[1], CultureInfo.InvariantCulture); - var ipa = IPAddress.Parse(split[0]); - var ipn = new IPNetwork(ipa, mask); - Assert.True(ipn.Contains(IPAddress.Parse(ipAddress))); + Assert.True(NetworkExtensions.TryParseToSubnet(netMask, out var subnet) && subnet.Contains(IPAddress.Parse(ipAddress))); } [Theory] @@ -148,11 +157,7 @@ namespace Jellyfin.Networking.Tests [InlineData("2001:db8:abcd:0012::0/128", "2001:0DB8:ABCD:0012:0000:0000:0000:0001")] public void IpV6SubnetMaskDoesNotMatchInvalidIpAddress(string netMask, string ipAddress) { - var split = netMask.Split("/"); - var mask = int.Parse(split[1], CultureInfo.InvariantCulture); - var ipa = IPAddress.Parse(split[0]); - var ipn = new IPNetwork(ipa, mask); - Assert.False(ipn.Contains(IPAddress.Parse(ipAddress))); + Assert.False(NetworkExtensions.TryParseToSubnet(netMask, out var subnet) && subnet.Contains(IPAddress.Parse(ipAddress))); } [Theory] @@ -262,7 +267,7 @@ namespace Jellyfin.Networking.Tests if (nm.TryParseInterface(result, out List? resultObj) && resultObj != null) { - // Parse out IPAddresses so we can do a string comparison. (Ignore subnet masks). + // Parse out IPAddresses so we can do a string comparison (ignore subnet masks). result = resultObj.First().Address.ToString(); } From c9a387943f05cbbd11c5b92d900ff850055ed4f3 Mon Sep 17 00:00:00 2001 From: Neuheit <38368299+Neuheit@users.noreply.github.com> Date: Thu, 20 Oct 2022 16:17:56 -0400 Subject: [PATCH 031/858] Add IPv4 fallback from IPv6 failure. Co-authored-by: BaronGreenback --- Emby.Dlna/Eventing/DlnaEventManager.cs | 2 +- .../HappyEyeballs/HttpClientExtension.cs | 117 ++++++++++++++++++ Jellyfin.Networking/Manager/NetworkManager.cs | 17 ++- Jellyfin.Server/Startup.cs | 20 ++- MediaBrowser.Common/Net/NamedClient.cs | 9 +- 5 files changed, 158 insertions(+), 7 deletions(-) create mode 100644 Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs diff --git a/Emby.Dlna/Eventing/DlnaEventManager.cs b/Emby.Dlna/Eventing/DlnaEventManager.cs index d17e238715..d2c0017ec6 100644 --- a/Emby.Dlna/Eventing/DlnaEventManager.cs +++ b/Emby.Dlna/Eventing/DlnaEventManager.cs @@ -165,7 +165,7 @@ namespace Emby.Dlna.Eventing try { - using var response = await _httpClientFactory.CreateClient(NamedClient.Default) + using var response = await _httpClientFactory.CreateClient(NamedClient.DirectIp) .SendAsync(options, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); } catch (OperationCanceledException) diff --git a/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs b/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs new file mode 100644 index 0000000000..12b42cc6e1 --- /dev/null +++ b/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs @@ -0,0 +1,117 @@ +using System.Diagnostics.Metrics; +using System.IO; +using System.Net.Http; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +/* + Copyright (c) 2021 ppy Pty Ltd . + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +namespace Jellyfin.Networking.HappyEyeballs +{ + /// + /// Defines the class. + /// + /// Implementation taken from https://github.com/ppy/osu-framework/pull/4191 . + /// + public static class HttpClientExtension + { + private const int ConnectionEstablishTimeout = 2000; + + /// + /// Gets a value indicating whether the initial IPv6 check has been performed (to determine whether v6 is available or not). + /// + private static bool _hasResolvedIPv6Availability; + + /// + /// Gets or sets a value indicating whether IPv6 should be preferred. Value may change based on runtime failures. + /// + public static bool? UseIPv6 { get; set; } = null; + + /// + /// Implements the httpclient callback method. + /// + /// The instance. + /// The instance. + /// The http steam. + public static async ValueTask OnConnect(SocketsHttpConnectionContext context, CancellationToken cancellationToken) + { + // Until .NET supports an implementation of Happy Eyeballs (https://tools.ietf.org/html/rfc8305#section-2), + // let's make IPv4 fallback work in a simple way. This issue is being tracked at https://github.com/dotnet/runtime/issues/26177 + // and expected to be fixed in .NET 6. + if (UseIPv6 == true) + { + try + { + var localToken = cancellationToken; + + if (!_hasResolvedIPv6Availability) + { + // to make things move fast, use a very low timeout for the initial ipv6 attempt. + using var quickFailCts = new CancellationTokenSource(ConnectionEstablishTimeout); + using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, quickFailCts.Token); + + localToken = linkedTokenSource.Token; + } + + return await AttemptConnection(AddressFamily.InterNetworkV6, context, localToken).ConfigureAwait(false); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch +#pragma warning restore CA1031 // Do not catch general exception types + { + // very naively fallback to ipv4 permanently for this execution based on the response of the first connection attempt. + // Network manager will reset this value in the case of physical network changes / interruptions. + UseIPv6 = false; + } + finally + { + _hasResolvedIPv6Availability = true; + } + } + + // fallback to IPv4. + return await AttemptConnection(AddressFamily.InterNetwork, context, cancellationToken).ConfigureAwait(false); + } + + private static async ValueTask AttemptConnection(AddressFamily addressFamily, SocketsHttpConnectionContext context, CancellationToken cancellationToken) + { + // The following socket constructor will create a dual-mode socket on systems where IPV6 is available. + var socket = new Socket(addressFamily, SocketType.Stream, ProtocolType.Tcp) + { + // Turn off Nagle's algorithm since it degrades performance in most HttpClient scenarios. + NoDelay = true + }; + + try + { + await socket.ConnectAsync(context.DnsEndPoint, cancellationToken).ConfigureAwait(false); + // The stream should take the ownership of the underlying socket, + // closing it when it's disposed. + return new NetworkStream(socket, ownsSocket: true); + } + catch + { + socket.Dispose(); + throw; + } + } + } +} diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 9e06cdfe71..631e9cdb8d 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -594,6 +594,7 @@ namespace Jellyfin.Networking.Manager IsIP4Enabled = Socket.OSSupportsIPv4 && config.EnableIPV4; IsIP6Enabled = Socket.OSSupportsIPv6 && config.EnableIPV6; + HappyEyeballs.HttpClientExtension.UseIPv6 = IsIP6Enabled; if (!IsIP6Enabled && !IsIP4Enabled) { @@ -838,9 +839,19 @@ namespace Jellyfin.Networking.Manager try { await Task.Delay(2000).ConfigureAwait(false); - InitialiseInterfaces(); - // Recalculate LAN caches. - InitialiseLAN(_configurationManager.GetNetworkConfiguration()); + + var config = _configurationManager.GetNetworkConfiguration(); + // Have we lost IPv6 capability? + if (IsIP6Enabled && !Socket.OSSupportsIPv6) + { + UpdateSettings(config); + } + else + { + InitialiseInterfaces(); + // Recalculate LAN caches. + InitialiseLAN(config); + } NetworkChanged?.Invoke(this, EventArgs.Empty); } diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 1954a5c558..4417bb7226 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -7,6 +7,7 @@ using System.Net.Mime; using System.Text; using Jellyfin.MediaEncoding.Hls.Extensions; using Jellyfin.Networking.Configuration; +using Jellyfin.Networking.HappyEyeballs; using Jellyfin.Server.Extensions; using Jellyfin.Server.Implementations; using Jellyfin.Server.Infrastructure; @@ -24,6 +25,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting; +using Microsoft.VisualBasic; using Prometheus; namespace Jellyfin.Server @@ -79,6 +81,13 @@ namespace Jellyfin.Server var acceptJsonHeader = new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json, 1.0); var acceptXmlHeader = new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Xml, 0.9); var acceptAnyHeader = new MediaTypeWithQualityHeaderValue("*/*", 0.8); + Func eyeballsHttpClientHandlerDelegate = (_) => new SocketsHttpHandler() + { + AutomaticDecompression = DecompressionMethods.All, + RequestHeaderEncodingSelector = (_, _) => Encoding.UTF8, + ConnectCallback = HttpClientExtension.OnConnect + }; + Func defaultHttpClientHandlerDelegate = (_) => new SocketsHttpHandler() { AutomaticDecompression = DecompressionMethods.All, @@ -93,7 +102,7 @@ namespace Jellyfin.Server c.DefaultRequestHeaders.Accept.Add(acceptXmlHeader); c.DefaultRequestHeaders.Accept.Add(acceptAnyHeader); }) - .ConfigurePrimaryHttpMessageHandler(defaultHttpClientHandlerDelegate); + .ConfigurePrimaryHttpMessageHandler(eyeballsHttpClientHandlerDelegate); services.AddHttpClient(NamedClient.MusicBrainz, c => { @@ -102,6 +111,15 @@ namespace Jellyfin.Server c.DefaultRequestHeaders.Accept.Add(acceptXmlHeader); c.DefaultRequestHeaders.Accept.Add(acceptAnyHeader); }) + .ConfigurePrimaryHttpMessageHandler(eyeballsHttpClientHandlerDelegate); + + services.AddHttpClient(NamedClient.DirectIp, c => + { + c.DefaultRequestHeaders.UserAgent.Add(productHeader); + c.DefaultRequestHeaders.Accept.Add(acceptJsonHeader); + c.DefaultRequestHeaders.Accept.Add(acceptXmlHeader); + c.DefaultRequestHeaders.Accept.Add(acceptAnyHeader); + }) .ConfigurePrimaryHttpMessageHandler(defaultHttpClientHandlerDelegate); services.AddHttpClient(NamedClient.Dlna, c => diff --git a/MediaBrowser.Common/Net/NamedClient.cs b/MediaBrowser.Common/Net/NamedClient.cs index a6cacd4f17..9c5544b0ff 100644 --- a/MediaBrowser.Common/Net/NamedClient.cs +++ b/MediaBrowser.Common/Net/NamedClient.cs @@ -1,4 +1,4 @@ -namespace MediaBrowser.Common.Net +namespace MediaBrowser.Common.Net { /// /// Registered http client names. @@ -6,7 +6,7 @@ public static class NamedClient { /// - /// Gets the value for the default named http client. + /// Gets the value for the default named http client which implements happy eyeballs. /// public const string Default = nameof(Default); @@ -19,5 +19,10 @@ /// Gets the value for the DLNA named http client. /// public const string Dlna = nameof(Dlna); + + /// + /// Non happy eyeballs implementation. + /// + public const string DirectIp = nameof(DirectIp); } } From 6c479dfb36d305444bae7d4b3ad78bfb2112549b Mon Sep 17 00:00:00 2001 From: Neuheit <38368299+Neuheit@users.noreply.github.com> Date: Thu, 20 Oct 2022 16:38:28 -0400 Subject: [PATCH 032/858] Ensure IPv6 property is threadsafe. --- .../HappyEyeballs/HttpClientExtension.cs | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs b/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs index 12b42cc6e1..6760869107 100644 --- a/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs +++ b/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs @@ -35,6 +35,11 @@ namespace Jellyfin.Networking.HappyEyeballs { private const int ConnectionEstablishTimeout = 2000; + /// + /// Interlocked doesn't support bool types. + /// + private static int _useIPv6 = 0; + /// /// Gets a value indicating whether the initial IPv6 check has been performed (to determine whether v6 is available or not). /// @@ -43,7 +48,21 @@ namespace Jellyfin.Networking.HappyEyeballs /// /// Gets or sets a value indicating whether IPv6 should be preferred. Value may change based on runtime failures. /// - public static bool? UseIPv6 { get; set; } = null; + public static bool UseIPv6 + { + get => Interlocked.CompareExchange(ref _useIPv6, 1, 1) == 1; + set + { + if (value) + { + Interlocked.CompareExchange(ref _useIPv6, 1, 0); + } + else + { + Interlocked.CompareExchange(ref _useIPv6, 0, 1); + } + } + } /// /// Implements the httpclient callback method. @@ -56,7 +75,7 @@ namespace Jellyfin.Networking.HappyEyeballs // Until .NET supports an implementation of Happy Eyeballs (https://tools.ietf.org/html/rfc8305#section-2), // let's make IPv4 fallback work in a simple way. This issue is being tracked at https://github.com/dotnet/runtime/issues/26177 // and expected to be fixed in .NET 6. - if (UseIPv6 == true) + if (UseIPv6) { try { From 6caabe68ff05f4100db63a3a98d6eeb02347f689 Mon Sep 17 00:00:00 2001 From: Neuheit <38368299+Neuheit@users.noreply.github.com> Date: Mon, 24 Oct 2022 17:17:41 -0400 Subject: [PATCH 033/858] Change method to call IPv6 and IPv4 in parallel. --- .../HappyEyeballs/HttpClientExtension.cs | 119 ++++++------------ 1 file changed, 38 insertions(+), 81 deletions(-) diff --git a/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs b/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs index 6760869107..049c8dd150 100644 --- a/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs +++ b/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs @@ -1,29 +1,9 @@ -using System.Diagnostics.Metrics; using System.IO; using System.Net.Http; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; -/* - Copyright (c) 2021 ppy Pty Ltd . - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. -*/ - namespace Jellyfin.Networking.HappyEyeballs { /// @@ -33,36 +13,12 @@ namespace Jellyfin.Networking.HappyEyeballs /// public static class HttpClientExtension { - private const int ConnectionEstablishTimeout = 2000; - /// - /// Interlocked doesn't support bool types. + /// Gets or sets a value indicating whether the client should use IPv6. /// - private static int _useIPv6 = 0; + public static bool UseIPv6 { get; set; } = true; - /// - /// Gets a value indicating whether the initial IPv6 check has been performed (to determine whether v6 is available or not). - /// - private static bool _hasResolvedIPv6Availability; - - /// - /// Gets or sets a value indicating whether IPv6 should be preferred. Value may change based on runtime failures. - /// - public static bool UseIPv6 - { - get => Interlocked.CompareExchange(ref _useIPv6, 1, 1) == 1; - set - { - if (value) - { - Interlocked.CompareExchange(ref _useIPv6, 1, 0); - } - else - { - Interlocked.CompareExchange(ref _useIPv6, 0, 1); - } - } - } + // Implementation taken from https://github.com/rmkerr/corefx/blob/SocketsHttpHandler_Connect_HappyEyeballs/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectHelper.cs /// /// Implements the httpclient callback method. @@ -72,45 +28,46 @@ namespace Jellyfin.Networking.HappyEyeballs /// The http steam. public static async ValueTask OnConnect(SocketsHttpConnectionContext context, CancellationToken cancellationToken) { - // Until .NET supports an implementation of Happy Eyeballs (https://tools.ietf.org/html/rfc8305#section-2), - // let's make IPv4 fallback work in a simple way. This issue is being tracked at https://github.com/dotnet/runtime/issues/26177 - // and expected to be fixed in .NET 6. - if (UseIPv6) + if (!UseIPv6) { - try - { - var localToken = cancellationToken; - - if (!_hasResolvedIPv6Availability) - { - // to make things move fast, use a very low timeout for the initial ipv6 attempt. - using var quickFailCts = new CancellationTokenSource(ConnectionEstablishTimeout); - using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, quickFailCts.Token); - - localToken = linkedTokenSource.Token; - } - - return await AttemptConnection(AddressFamily.InterNetworkV6, context, localToken).ConfigureAwait(false); - } -#pragma warning disable CA1031 // Do not catch general exception types - catch -#pragma warning restore CA1031 // Do not catch general exception types - { - // very naively fallback to ipv4 permanently for this execution based on the response of the first connection attempt. - // Network manager will reset this value in the case of physical network changes / interruptions. - UseIPv6 = false; - } - finally - { - _hasResolvedIPv6Availability = true; - } + return await AttemptConnection(AddressFamily.InterNetwork, context, cancellationToken).ConfigureAwait(false); } - // fallback to IPv4. - return await AttemptConnection(AddressFamily.InterNetwork, context, cancellationToken).ConfigureAwait(false); + using var cancelIPv6 = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + var tryConnectAsyncIPv6 = AttemptConnection(AddressFamily.InterNetworkV6, context, cancelIPv6.Token); + + if (await Task.WhenAny(tryConnectAsyncIPv6, Task.Delay(200, cancelIPv6.Token)).ConfigureAwait(false) == tryConnectAsyncIPv6 && tryConnectAsyncIPv6.IsCompletedSuccessfully) + { + cancelIPv6.Cancel(); + return tryConnectAsyncIPv6.GetAwaiter().GetResult(); + } + + using var cancelIPv4 = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + var tryConnectAsyncIPv4 = AttemptConnection(AddressFamily.InterNetwork, context, cancelIPv4.Token); + + if (await Task.WhenAny(tryConnectAsyncIPv6, tryConnectAsyncIPv4).ConfigureAwait(false) == tryConnectAsyncIPv6) + { + if (tryConnectAsyncIPv6.IsCompletedSuccessfully) + { + cancelIPv4.Cancel(); + return tryConnectAsyncIPv6.GetAwaiter().GetResult(); + } + + return tryConnectAsyncIPv4.GetAwaiter().GetResult(); + } + else + { + if (tryConnectAsyncIPv4.IsCompletedSuccessfully) + { + cancelIPv6.Cancel(); + return tryConnectAsyncIPv4.GetAwaiter().GetResult(); + } + + return tryConnectAsyncIPv6.GetAwaiter().GetResult(); + } } - private static async ValueTask AttemptConnection(AddressFamily addressFamily, SocketsHttpConnectionContext context, CancellationToken cancellationToken) + private static async Task AttemptConnection(AddressFamily addressFamily, SocketsHttpConnectionContext context, CancellationToken cancellationToken) { // The following socket constructor will create a dual-mode socket on systems where IPV6 is available. var socket = new Socket(addressFamily, SocketType.Stream, ProtocolType.Tcp) From 6621121c1b958f0e3726f3b4f1d9c2604e398504 Mon Sep 17 00:00:00 2001 From: Stefan Hendricks <38368299+Neuheit@users.noreply.github.com> Date: Sat, 29 Oct 2022 18:54:11 -0400 Subject: [PATCH 034/858] Add .NET MIT License --- .../HappyEyeballs/HttpClientExtension.cs | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs b/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs index 049c8dd150..bab02505dc 100644 --- a/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs +++ b/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs @@ -1,3 +1,29 @@ +/* +The MIT License (MIT) + +Copyright (c) .NET Foundation and Contributors + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + using System.IO; using System.Net.Http; using System.Net.Sockets; @@ -18,8 +44,6 @@ namespace Jellyfin.Networking.HappyEyeballs /// public static bool UseIPv6 { get; set; } = true; - // Implementation taken from https://github.com/rmkerr/corefx/blob/SocketsHttpHandler_Connect_HappyEyeballs/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectHelper.cs - /// /// Implements the httpclient callback method. /// From 5f274beded71a84a28165f5514f1349fd392b3b6 Mon Sep 17 00:00:00 2001 From: Dominik Date: Mon, 14 Nov 2022 10:11:09 +0100 Subject: [PATCH 035/858] Fix logs for TranscodingThrottler not working --- Jellyfin.Api/Helpers/TranscodingJobHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index c663c6e310..f54f9eadfe 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -655,7 +655,7 @@ namespace Jellyfin.Api.Helpers { if (EnableThrottling(state)) { - transcodingJob.TranscodingThrottler = new TranscodingThrottler(transcodingJob, new Logger(new LoggerFactory()), _serverConfigurationManager, _fileSystem, _mediaEncoder); + transcodingJob.TranscodingThrottler = new TranscodingThrottler(transcodingJob, _loggerFactory.CreateLogger(), _serverConfigurationManager, _fileSystem, _mediaEncoder); transcodingJob.TranscodingThrottler.Start(); } } From 832b36562aef04b2c0f6d012920fcf80e0dd080c Mon Sep 17 00:00:00 2001 From: Dominik Date: Mon, 14 Nov 2022 10:11:25 +0100 Subject: [PATCH 036/858] Add new options for segment deletion --- MediaBrowser.Model/Configuration/EncodingOptions.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index f4cd2f0065..c23d4e6422 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -11,7 +11,9 @@ namespace MediaBrowser.Model.Configuration DownMixAudioBoost = 2; MaxMuxingQueueSize = 2048; EnableThrottling = false; + EnableSegmentDeletion = false; ThrottleDelaySeconds = 180; + SegmentKeepSeconds = 200; EncodingThreadCount = -1; // This is a DRM device that is almost guaranteed to be there on every intel platform, // plus it's the default one in ffmpeg if you don't specify anything @@ -57,8 +59,12 @@ namespace MediaBrowser.Model.Configuration public bool EnableThrottling { get; set; } + public bool EnableSegmentDeletion { get; set; } + public int ThrottleDelaySeconds { get; set; } + public int SegmentKeepSeconds { get; set; } + public string HardwareAccelerationType { get; set; } /// From c2c182d099e8f5d77d43af76d7098848ab74c86c Mon Sep 17 00:00:00 2001 From: Dominik Date: Mon, 14 Nov 2022 10:13:17 +0100 Subject: [PATCH 037/858] Add support for ffmpeg's segment deletion and segment wrapping --- .../Controllers/DynamicHlsController.cs | 44 +++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 0f4d3c1ebc..b86f62cf82 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1503,8 +1503,17 @@ namespace Jellyfin.Api.Controllers // If the playlist doesn't already exist, startup ffmpeg try { - await _transcodingJobHelper.KillTranscodingJobs(streamingRequest.DeviceId, streamingRequest.PlaySessionId, p => false) + if (_encodingOptions.EnableThrottling && _encodingOptions.EnableSegmentDeletion) + { + // Delete old HLS files when segment deletion is active since ffmpeg doesn't clean them up by itself + await _transcodingJobHelper.KillTranscodingJobs(streamingRequest.DeviceId, streamingRequest.PlaySessionId, p => true) .ConfigureAwait(false); + } + else + { + await _transcodingJobHelper.KillTranscodingJobs(streamingRequest.DeviceId, streamingRequest.PlaySessionId, p => false) + .ConfigureAwait(false); + } if (currentTranscodingIndex.HasValue) { @@ -1639,9 +1648,11 @@ namespace Jellyfin.Api.Controllers Path.GetFileNameWithoutExtension(outputPath)); } + var hlsArguments = GetHlsArguments(isEventPlaylist, state.SegmentLength); + return string.Format( CultureInfo.InvariantCulture, - "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -hls_segment_type {8} -start_number {9}{10} -hls_segment_filename \"{12}\" -hls_playlist_type {11} -hls_list_size 0 -y \"{13}\"", + "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -hls_segment_type {8} -start_number {9}{10} -hls_segment_filename \"{11}\" {12} -y \"{13}\"", inputModifier, _encodingHelper.GetInputArgument(state, _encodingOptions, segmentContainer), threads, @@ -1653,11 +1664,38 @@ namespace Jellyfin.Api.Controllers segmentFormat, startNumber.ToString(CultureInfo.InvariantCulture), baseUrlParam, - isEventPlaylist ? "event" : "vod", outputTsArg, + hlsArguments, outputPath).Trim(); } + /// + /// Gets the HLS arguments for transcoding. + /// + /// The command line arguments for HLS transcoding. + private string GetHlsArguments(bool isEventPlaylist, int segmentLength) + { + var enableThrottling = _encodingOptions.EnableThrottling; + var enableSegmentDeletion = _encodingOptions.EnableSegmentDeletion; + + // Only enable segment deletion when throttling is enabled + if (enableThrottling && enableSegmentDeletion) + { + // Store enough segments for configured seconds of playback; this needs to be above throttling settings + var segmentCount = _encodingOptions.SegmentKeepSeconds / segmentLength; + + _logger.LogDebug("Using throttling and segment deletion, keeping {0} segments", segmentCount); + + return string.Format(CultureInfo.InvariantCulture, "-hls_list_size {0} -segment_wrap {0} -hls_flags delete_segments", segmentCount.ToString(CultureInfo.InvariantCulture)); + } + else + { + _logger.LogDebug("Using normal playback, is event playlist? {0}", isEventPlaylist); + + return string.Format(CultureInfo.InvariantCulture, "-hls_playlist_type {0} -hls_list_size 0", isEventPlaylist ? "event" : "vod"); + } + } + /// /// Gets the audio arguments for transcoding. /// From 9f4f76b0ab0f3c3e0d7833d37b28f9915cff0549 Mon Sep 17 00:00:00 2001 From: Dominik Date: Mon, 14 Nov 2022 14:39:08 +0100 Subject: [PATCH 038/858] Remove segment wrapping --- Jellyfin.Api/Controllers/DynamicHlsController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index b86f62cf82..556057a0fd 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1686,7 +1686,7 @@ namespace Jellyfin.Api.Controllers _logger.LogDebug("Using throttling and segment deletion, keeping {0} segments", segmentCount); - return string.Format(CultureInfo.InvariantCulture, "-hls_list_size {0} -segment_wrap {0} -hls_flags delete_segments", segmentCount.ToString(CultureInfo.InvariantCulture)); + return string.Format(CultureInfo.InvariantCulture, "-hls_list_size {0} -hls_flags delete_segments", segmentCount.ToString(CultureInfo.InvariantCulture)); } else { From abcf9c4819f241be7c80102d7cc80f3b3b27eae2 Mon Sep 17 00:00:00 2001 From: Dominik Date: Mon, 14 Nov 2022 14:45:42 +0100 Subject: [PATCH 039/858] Simplify HLS file deletion Co-authored-by: Claus Vium --- Jellyfin.Api/Controllers/DynamicHlsController.cs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 556057a0fd..8c385761b2 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1503,17 +1503,10 @@ namespace Jellyfin.Api.Controllers // If the playlist doesn't already exist, startup ffmpeg try { - if (_encodingOptions.EnableThrottling && _encodingOptions.EnableSegmentDeletion) - { - // Delete old HLS files when segment deletion is active since ffmpeg doesn't clean them up by itself - await _transcodingJobHelper.KillTranscodingJobs(streamingRequest.DeviceId, streamingRequest.PlaySessionId, p => true) + // Delete old HLS files when segment deletion is active since ffmpeg doesn't clean them up by itself + var deleteFiles = _encodingOptions.EnableThrottling && _encodingOptions.EnableSegmentDeletion; + await _transcodingJobHelper.KillTranscodingJobs(streamingRequest.DeviceId, streamingRequest.PlaySessionId, _ => deleteFiles) .ConfigureAwait(false); - } - else - { - await _transcodingJobHelper.KillTranscodingJobs(streamingRequest.DeviceId, streamingRequest.PlaySessionId, p => false) - .ConfigureAwait(false); - } if (currentTranscodingIndex.HasValue) { From 87f3bdb918954cbb5c4bdc7408b1ca41b246c83b Mon Sep 17 00:00:00 2001 From: Dominik Date: Tue, 15 Nov 2022 11:32:58 +0100 Subject: [PATCH 040/858] Do not set different force_key_frames for vod streams --- .../MediaEncoding/EncodingHelper.cs | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index cee08eedac..0ddb3be426 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -1272,22 +1272,11 @@ namespace MediaBrowser.Controller.MediaEncoding { var args = string.Empty; var gopArg = string.Empty; - var keyFrameArg = string.Empty; - if (isEventPlaylist) - { - keyFrameArg = string.Format( - CultureInfo.InvariantCulture, - " -force_key_frames:0 \"expr:gte(t,n_forced*{0})\"", - segmentLength); - } - else if (startNumber.HasValue) - { - keyFrameArg = string.Format( - CultureInfo.InvariantCulture, - " -force_key_frames:0 \"expr:gte(t,{0}+n_forced*{1})\"", - startNumber.Value * segmentLength, - segmentLength); - } + + var keyFrameArg = string.Format( + CultureInfo.InvariantCulture, + " -force_key_frames:0 \"expr:gte(t,n_forced*{0})\"", + segmentLength); var framerate = state.VideoStream?.RealFrameRate; if (framerate.HasValue) From 09a1d6786a0cad593cc862d62351a0c5e748497f Mon Sep 17 00:00:00 2001 From: Dominik Date: Tue, 15 Nov 2022 16:06:24 +0100 Subject: [PATCH 041/858] Increase default SegmentKeepSeconds --- MediaBrowser.Model/Configuration/EncodingOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index c23d4e6422..0080506020 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -13,7 +13,7 @@ namespace MediaBrowser.Model.Configuration EnableThrottling = false; EnableSegmentDeletion = false; ThrottleDelaySeconds = 180; - SegmentKeepSeconds = 200; + SegmentKeepSeconds = 360; EncodingThreadCount = -1; // This is a DRM device that is almost guaranteed to be there on every intel platform, // plus it's the default one in ffmpeg if you don't specify anything From e8ae7e5c38e28f13fa8de295e26c930cb46d9b79 Mon Sep 17 00:00:00 2001 From: Dominik Date: Tue, 15 Nov 2022 17:14:18 +0100 Subject: [PATCH 042/858] Do not delete segments when seeking --- Jellyfin.Api/Controllers/DynamicHlsController.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 8c385761b2..81d77663e9 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1503,9 +1503,7 @@ namespace Jellyfin.Api.Controllers // If the playlist doesn't already exist, startup ffmpeg try { - // Delete old HLS files when segment deletion is active since ffmpeg doesn't clean them up by itself - var deleteFiles = _encodingOptions.EnableThrottling && _encodingOptions.EnableSegmentDeletion; - await _transcodingJobHelper.KillTranscodingJobs(streamingRequest.DeviceId, streamingRequest.PlaySessionId, _ => deleteFiles) + await _transcodingJobHelper.KillTranscodingJobs(streamingRequest.DeviceId, streamingRequest.PlaySessionId, p => false) .ConfigureAwait(false); if (currentTranscodingIndex.HasValue) From 36994c17bf5f71f37a5002a51840306fa09fb0ef Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Thu, 17 Nov 2022 11:34:48 +0100 Subject: [PATCH 043/858] Apply review suggestions --- Jellyfin.Networking/Manager/NetworkManager.cs | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 4a32423e5e..4a6a4a5766 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -34,7 +34,7 @@ namespace Jellyfin.Networking.Manager private readonly IConfigurationManager _configurationManager; - private readonly SemaphoreSlim _networkEvent; + private readonly object _networkEventLock; /// /// Holds the published server URLs and the IPs to use them on. @@ -86,7 +86,7 @@ namespace Jellyfin.Networking.Manager _interfaces = new List(); _macAddresses = new List(); _publishedServerUrls = new Dictionary(); - _networkEvent = new SemaphoreSlim(1, 1); + _networkEventLock = new object(); _remoteAddressFilter = new List(); UpdateSettings(_configurationManager.GetNetworkConfiguration()); @@ -162,16 +162,15 @@ namespace Jellyfin.Networking.Manager /// private void HandleNetworkChange() { - _networkEvent.Wait(); - if (!_eventfire) - { - _logger.LogDebug("Network Address Change Event."); - // As network events tend to fire one after the other only fire once every second. - _eventfire = true; - OnNetworkChange(); + lock(_networkEventLock){ + if (!_eventfire) + { + _logger.LogDebug("Network Address Change Event."); + // As network events tend to fire one after the other only fire once every second. + _eventfire = true; + OnNetworkChange(); + } } - - _networkEvent.Release(); } /// @@ -546,7 +545,6 @@ namespace Jellyfin.Networking.Manager _configurationManager.NamedConfigurationUpdated -= ConfigurationUpdated; NetworkChange.NetworkAddressChanged -= OnNetworkAddressChanged; NetworkChange.NetworkAvailabilityChanged -= OnNetworkAvailabilityChanged; - _networkEvent.Dispose(); } _disposed = true; From 95740ef9a21f76b51ab777ef34162910b3da1b3c Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Sat, 3 Dec 2022 12:44:59 +0100 Subject: [PATCH 044/858] Fix build --- Jellyfin.Networking/Manager/NetworkManager.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 4a6a4a5766..0318582061 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -162,7 +162,8 @@ namespace Jellyfin.Networking.Manager /// private void HandleNetworkChange() { - lock(_networkEventLock){ + lock (_networkEventLock) + { if (!_eventfire) { _logger.LogDebug("Network Address Change Event."); From dd5f90802e71083347b6095eb79a9de0b9d34615 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Sat, 3 Dec 2022 14:32:20 +0100 Subject: [PATCH 045/858] Apply review suggestions --- Emby.Server.Implementations/ApplicationHost.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 1398bb373b..d4bf8a418b 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1093,8 +1093,9 @@ namespace Emby.Server.Implementations if (ConfigurationManager.GetNetworkConfiguration().EnablePublishedServerUriByRequest) { int? requestPort = request.Host.Port; - if (((requestPort == 80 || requestPort == null) && string.Equals(request.Scheme, "http", StringComparison.OrdinalIgnoreCase)) - || ((requestPort == 443 || requestPort == null) && string.Equals(request.Scheme, "https", StringComparison.OrdinalIgnoreCase))) + if (requestPort == null + || (requestPort == 80 && string.Equals(request.Scheme, "http", StringComparison.OrdinalIgnoreCase)) + || (requestPort == 443 && string.Equals(request.Scheme, "https", StringComparison.OrdinalIgnoreCase))) { requestPort = -1; } From 3f6354cdb832560ec811f1766666dd9ca1f2a208 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 7 Dec 2022 17:41:32 +0100 Subject: [PATCH 046/858] Fix .NET 7 compatibility --- Emby.Server.Implementations/ApplicationHost.cs | 2 +- MediaBrowser.Common/Net/NetworkExtensions.cs | 10 +++++----- tests/Jellyfin.Networking.Tests/NetworkParseTests.cs | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 4733b39abe..df4f3e6762 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1094,7 +1094,7 @@ namespace Emby.Server.Implementations { int? requestPort = request.Host.Port; if (requestPort == null - || (requestPort == 80 && string.Equals(request.Scheme, "http", StringComparison.OrdinalIgnoreCase)) + || (requestPort == 80 && string.Equals(request.Scheme, "http", StringComparison.OrdinalIgnoreCase)) || (requestPort == 443 && string.Equals(request.Scheme, "https", StringComparison.OrdinalIgnoreCase))) { requestPort = -1; diff --git a/MediaBrowser.Common/Net/NetworkExtensions.cs b/MediaBrowser.Common/Net/NetworkExtensions.cs index e37a5dc6bc..97f0abbb52 100644 --- a/MediaBrowser.Common/Net/NetworkExtensions.cs +++ b/MediaBrowser.Common/Net/NetworkExtensions.cs @@ -1,10 +1,10 @@ -using Microsoft.AspNetCore.HttpOverrides; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.Sockets; using System.Text.RegularExpressions; +using Microsoft.AspNetCore.HttpOverrides; namespace MediaBrowser.Common.Net { @@ -131,7 +131,7 @@ namespace MediaBrowser.Common.Net /// URI safe conversion of the address. public static string FormatIpString(IPAddress? address) { - if (address == null) + if (address is null) { return string.Empty; } @@ -163,7 +163,7 @@ namespace MediaBrowser.Common.Net { result = new List(); - if (values == null || values.Length == 0) + if (values is null || values.Length == 0) { return false; } @@ -182,7 +182,7 @@ namespace MediaBrowser.Common.Net _ = IPAddress.TryParse(v[0][0..], out address); } - if (address != IPAddress.None && address != null) + if (address != IPAddress.None && address is not null) { if (v.Length > 1 && int.TryParse(v[1], out var netmask)) { @@ -240,7 +240,7 @@ namespace MediaBrowser.Common.Net _ = IPAddress.TryParse(v[0][0..], out address); } - if (address != IPAddress.None && address != null) + if (address != IPAddress.None && address is not null) { if (v.Length > 1 && int.TryParse(v[1], out var netmask)) { diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs index 86b2ab21a3..241d2314bf 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs @@ -207,7 +207,7 @@ namespace Jellyfin.Networking.Tests // Check to see if dns resolution is working. If not, skip test. _ = NetworkExtensions.TryParseHost(source, out var host); - if (resultObj != null && host.Length > 0) + if (resultObj is not null && host.Length > 0) { result = resultObj.First().Address.ToString(); var intf = nm.GetBindInterface(source, out _); @@ -265,7 +265,7 @@ namespace Jellyfin.Networking.Tests using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); NetworkManager.MockNetworkSettings = string.Empty; - if (nm.TryParseInterface(result, out List? resultObj) && resultObj != null) + if (nm.TryParseInterface(result, out List? resultObj) && resultObj is not null) { // Parse out IPAddresses so we can do a string comparison (ignore subnet masks). result = resultObj.First().Address.ToString(); From 9e791a92c3dbf12420ec13e8c0b15f70d4dcba78 Mon Sep 17 00:00:00 2001 From: Neuheit <38368299+Neuheit@users.noreply.github.com> Date: Thu, 8 Dec 2022 00:16:18 -0500 Subject: [PATCH 047/858] Add comments explaining GetAwaiter() --- Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs b/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs index bab02505dc..0ceaeaf968 100644 --- a/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs +++ b/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs @@ -69,6 +69,9 @@ namespace Jellyfin.Networking.HappyEyeballs using var cancelIPv4 = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); var tryConnectAsyncIPv4 = AttemptConnection(AddressFamily.InterNetwork, context, cancelIPv4.Token); + //Both connect tasks use GetAwaiter().GetResult() as the appropriate task has already been completed. + //This results in improved exception handling. + //See https://github.com/dotnet/corefx/pull/29792/files#r189415885 for more details. if (await Task.WhenAny(tryConnectAsyncIPv6, tryConnectAsyncIPv4).ConfigureAwait(false) == tryConnectAsyncIPv6) { if (tryConnectAsyncIPv6.IsCompletedSuccessfully) From a7fc5e6e12e2330344640c92cc4d0afc36c74fe2 Mon Sep 17 00:00:00 2001 From: Neuheit <38368299+Neuheit@users.noreply.github.com> Date: Thu, 8 Dec 2022 00:17:46 -0500 Subject: [PATCH 048/858] Add additional awaiter code comment. --- Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs b/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs index 0ceaeaf968..add70c9cd6 100644 --- a/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs +++ b/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs @@ -60,6 +60,9 @@ namespace Jellyfin.Networking.HappyEyeballs using var cancelIPv6 = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); var tryConnectAsyncIPv6 = AttemptConnection(AddressFamily.InterNetworkV6, context, cancelIPv6.Token); + //This connect task uses GetAwaiter().GetResult() as the appropriate task has already been completed. + //This results in improved exception handling. + //See https://github.com/dotnet/corefx/pull/29792/files#r189415885 for more details. if (await Task.WhenAny(tryConnectAsyncIPv6, Task.Delay(200, cancelIPv6.Token)).ConfigureAwait(false) == tryConnectAsyncIPv6 && tryConnectAsyncIPv6.IsCompletedSuccessfully) { cancelIPv6.Cancel(); @@ -71,7 +74,6 @@ namespace Jellyfin.Networking.HappyEyeballs //Both connect tasks use GetAwaiter().GetResult() as the appropriate task has already been completed. //This results in improved exception handling. - //See https://github.com/dotnet/corefx/pull/29792/files#r189415885 for more details. if (await Task.WhenAny(tryConnectAsyncIPv6, tryConnectAsyncIPv4).ConfigureAwait(false) == tryConnectAsyncIPv6) { if (tryConnectAsyncIPv6.IsCompletedSuccessfully) From 292c4ebe72d293184f7a60f68f5ad76296539b89 Mon Sep 17 00:00:00 2001 From: Neuheit <38368299+Neuheit@users.noreply.github.com> Date: Thu, 8 Dec 2022 16:15:36 -0500 Subject: [PATCH 049/858] Clarify code comment. Co-authored-by: Claus Vium --- Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs b/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs index add70c9cd6..ddbf841b07 100644 --- a/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs +++ b/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs @@ -72,8 +72,6 @@ namespace Jellyfin.Networking.HappyEyeballs using var cancelIPv4 = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); var tryConnectAsyncIPv4 = AttemptConnection(AddressFamily.InterNetwork, context, cancelIPv4.Token); - //Both connect tasks use GetAwaiter().GetResult() as the appropriate task has already been completed. - //This results in improved exception handling. if (await Task.WhenAny(tryConnectAsyncIPv6, tryConnectAsyncIPv4).ConfigureAwait(false) == tryConnectAsyncIPv6) { if (tryConnectAsyncIPv6.IsCompletedSuccessfully) From 93fe47c7cb7a3aa5eb8504edc2fd4f27c4824993 Mon Sep 17 00:00:00 2001 From: Neuheit <38368299+Neuheit@users.noreply.github.com> Date: Thu, 8 Dec 2022 16:16:06 -0500 Subject: [PATCH 050/858] Clarify code comment. Co-authored-by: Claus Vium --- Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs b/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs index ddbf841b07..59e6956c71 100644 --- a/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs +++ b/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs @@ -60,9 +60,9 @@ namespace Jellyfin.Networking.HappyEyeballs using var cancelIPv6 = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); var tryConnectAsyncIPv6 = AttemptConnection(AddressFamily.InterNetworkV6, context, cancelIPv6.Token); - //This connect task uses GetAwaiter().GetResult() as the appropriate task has already been completed. - //This results in improved exception handling. - //See https://github.com/dotnet/corefx/pull/29792/files#r189415885 for more details. + // GetAwaiter().GetResult() is used instead of .Result as this results in improved exception handling. + // The tasks have already been completed. + // See https://github.com/dotnet/corefx/pull/29792/files#r189415885 for more details. if (await Task.WhenAny(tryConnectAsyncIPv6, Task.Delay(200, cancelIPv6.Token)).ConfigureAwait(false) == tryConnectAsyncIPv6 && tryConnectAsyncIPv6.IsCompletedSuccessfully) { cancelIPv6.Cancel(); From b725fbe51af22ca270912eeb4929a8ee2a3b37a4 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Tue, 13 Dec 2022 10:39:37 +0100 Subject: [PATCH 051/858] Apply review suggestions --- Emby.Dlna/Main/DlnaEntryPoint.cs | 2 +- Emby.Server.Implementations/ApplicationHost.cs | 4 ++-- Emby.Server.Implementations/Net/SocketFactory.cs | 2 +- tests/Jellyfin.Server.Tests/ParseNetworkTests.cs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index ec483199d8..22903ec8f3 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -288,7 +288,7 @@ namespace Emby.Dlna.Main var bindAddresses = _networkManager .GetInternalBindAddresses() .Where(i => i.Address.AddressFamily == AddressFamily.InterNetwork - || (i.AddressFamily == AddressFamily.InterNetworkV6 && i.Address.ScopeId != 0)) + || (i.AddressFamily == AddressFamily.InterNetworkV6 && i.Address.ScopeId != 0)) .ToList(); if (bindAddresses.Count == 0) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index df4f3e6762..d0c744d2fb 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1094,8 +1094,8 @@ namespace Emby.Server.Implementations { int? requestPort = request.Host.Port; if (requestPort == null - || (requestPort == 80 && string.Equals(request.Scheme, "http", StringComparison.OrdinalIgnoreCase)) - || (requestPort == 443 && string.Equals(request.Scheme, "https", StringComparison.OrdinalIgnoreCase))) + || (requestPort == 80 && string.Equals(request.Scheme, "http", StringComparison.OrdinalIgnoreCase)) + || (requestPort == 443 && string.Equals(request.Scheme, "https", StringComparison.OrdinalIgnoreCase))) { requestPort = -1; } diff --git a/Emby.Server.Implementations/Net/SocketFactory.cs b/Emby.Server.Implementations/Net/SocketFactory.cs index 02e45e6899..e0c75698d2 100644 --- a/Emby.Server.Implementations/Net/SocketFactory.cs +++ b/Emby.Server.Implementations/Net/SocketFactory.cs @@ -61,7 +61,7 @@ namespace Emby.Server.Implementations.Net } /// - public ISocket CreateUdpMulticastSocket(IPAddress ipAddress, IPAddress bindIpAddress, int multicastTimeToLive, int localPort) + public ISocket CreateUdpMulticastSocket(IPAddress ipAddress, IPAddress? bindIpAddress, int multicastTimeToLive, int localPort) { ArgumentNullException.ThrowIfNull(ipAddress); diff --git a/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs index fc5f5f4c6c..12a9beb9e3 100644 --- a/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs +++ b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs @@ -22,7 +22,7 @@ namespace Jellyfin.Server.Tests true, true, new string[] { "192.168.t", "127.0.0.1", "::1", "1234.1232.12.1234" }, - new IPAddress[] { IPAddress.Loopback, }, + new IPAddress[] { IPAddress.Loopback }, new IPNetwork[] { new IPNetwork(IPAddress.IPv6Loopback, 128) }); data.Add( From f0376cdad9626f5ae0b41a5f54e765fc10dffa05 Mon Sep 17 00:00:00 2001 From: Brad Beattie Date: Wed, 14 Dec 2022 19:14:15 -0800 Subject: [PATCH 052/858] Augment tag searching to consider all ItemValues --- Emby.Server.Implementations/Data/SqliteItemRepository.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 1514762608..c189fc8780 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -2461,7 +2461,9 @@ namespace Emby.Server.Implementations.Data if (query.SearchTerm.Length > 1) { builder.Append("+ ((CleanName like @SearchTermContains or (OriginalTitle not null and OriginalTitle like @SearchTermContains)) * 10)"); - builder.Append("+ ((Tags not null and Tags like @SearchTermContains) * 5)"); + builder.Append("+ (SELECT COUNT(1) * 1 from ItemValues where ItemId=Guid and CleanValue like @SearchTermContains)"); + builder.Append("+ (SELECT COUNT(1) * 2 from ItemValues where ItemId=Guid and CleanValue like @SearchTermStartsWith)"); + builder.Append("+ (SELECT COUNT(1) * 10 from ItemValues where ItemId=Guid and CleanValue like @SearchTermEquals)"); } builder.Append(") as SearchScore"); @@ -2492,6 +2494,11 @@ namespace Emby.Server.Implementations.Data { statement.TryBind("@SearchTermContains", "%" + searchTerm + "%"); } + + if (commandText.Contains("@SearchTermEquals", StringComparison.OrdinalIgnoreCase)) + { + statement.TryBind("@SearchTermEquals", searchTerm); + } } private void BindSimilarParams(InternalItemsQuery query, IStatement statement) From 776294b5f8499434de515ec6c595bc6db354d9a2 Mon Sep 17 00:00:00 2001 From: DarrenRuane Date: Sun, 15 Jan 2023 23:09:33 +0000 Subject: [PATCH 053/858] Fix pre-existing chapter images not being deleted after the chapter image generation setting is disabled --- .../ScheduledTasks/Tasks/ChapterImagesTask.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs index abc203618d..6ad6c4cbd6 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs @@ -100,7 +100,6 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks EnableImages = false }, SourceTypes = new SourceType[] { SourceType.Library }, - HasChapterImages = false, IsVirtualItem = false }) .OfType /// The video. + /// /// true if [is eligible for chapter image extraction] [the specified video]; otherwise, false. - private bool IsEligibleForChapterImageExtraction(Video video) + private bool IsEligibleForChapterImageExtraction(Video video, LibraryOptions libraryOptions) { if (video.IsPlaceHolder) { return false; } - var libraryOptions = _libraryManager.GetLibraryOptions(video); - if (libraryOptions is not null) - { - if (!libraryOptions.EnableChapterImageExtraction) - { - return false; - } - } - else + if (libraryOptions is null || !libraryOptions.EnableChapterImageExtraction) { return false; } @@ -99,7 +93,9 @@ namespace Emby.Server.Implementations.MediaEncoder public async Task RefreshChapterImages(Video video, IDirectoryService directoryService, IReadOnlyList chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken) { - if (!IsEligibleForChapterImageExtraction(video)) + var libraryOptions = _libraryManager.GetLibraryOptions(video); + + if (!IsEligibleForChapterImageExtraction(video, libraryOptions)) { extractImages = false; } @@ -179,6 +175,12 @@ namespace Emby.Server.Implementations.MediaEncoder chapter.ImageDateModified = _fileSystem.GetLastWriteTimeUtc(path); changesMade = true; } + else if (libraryOptions?.EnableChapterImageExtraction != true) + { + // We have an image for the current chapter but the user has disabled chapter image extraction -> delete this chapter's image + chapter.ImagePath = null; + changesMade = true; + } } if (saveChapters && changesMade) From cf8db70af8ed407ce35983fd74c1f7a6b34b5003 Mon Sep 17 00:00:00 2001 From: DarrenRuane Date: Mon, 16 Jan 2023 20:06:56 +0000 Subject: [PATCH 055/858] Fix build failing --- Emby.Server.Implementations/MediaEncoder/EncodingManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs index 8103e22649..7732e32d0a 100644 --- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs +++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs @@ -63,7 +63,7 @@ namespace Emby.Server.Implementations.MediaEncoder /// Determines whether [is eligible for chapter image extraction] [the specified video]. /// /// The video. - /// + /// The library options for the video. /// true if [is eligible for chapter image extraction] [the specified video]; otherwise, false. private bool IsEligibleForChapterImageExtraction(Video video, LibraryOptions libraryOptions) { From 343a94f185213d238364896a27a2968cc32ea618 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Thu, 19 Jan 2023 10:19:53 +0100 Subject: [PATCH 056/858] Fix CA1851 --- Jellyfin.Networking/Manager/NetworkManager.cs | 7 ++++--- Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 0318582061..f4bd4e7662 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -564,13 +564,13 @@ namespace Jellyfin.Networking.Manager if (_interfaces != null) { // Match all interfaces starting with names starting with token - var matchedInterfaces = _interfaces.Where(s => s.Name.Equals(intf, StringComparison.OrdinalIgnoreCase)).OrderBy(x => x.Index); + var matchedInterfaces = _interfaces.Where(s => s.Name.Equals(intf, StringComparison.OrdinalIgnoreCase)).OrderBy(x => x.Index).ToList(); if (matchedInterfaces.Any()) { _logger.LogInformation("Interface {Token} used in settings. Using its interface addresses.", intf); // Use interface IP instead of name - foreach (IPData iface in matchedInterfaces) + foreach (var iface in matchedInterfaces) { if ((IsIpv4Enabled && iface.Address.AddressFamily == AddressFamily.InterNetwork) || (IsIpv6Enabled && iface.Address.AddressFamily == AddressFamily.InterNetworkV6)) @@ -746,7 +746,8 @@ namespace Jellyfin.Networking.Manager // Get the first LAN interface address that's not excluded and not a loopback address. var availableInterfaces = _interfaces.Where(x => !IPAddress.IsLoopback(x.Address)) .OrderByDescending(x => IsInLocalNetwork(x.Address)) - .ThenBy(x => x.Index); + .ThenBy(x => x.Index) + .ToList(); if (availableInterfaces.Any()) { diff --git a/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs b/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs index 58d3e1b2d9..eac13f7610 100644 --- a/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs @@ -39,7 +39,7 @@ public static class WebHostBuilderExtensions var addresses = appHost.NetManager.GetAllBindInterfaces(); bool flagged = false; - foreach (IPObject netAdd in addresses) + foreach (var netAdd in addresses) { logger.LogInformation("Kestrel listening on {Address}", IPAddress.IPv6Any.Equals(netAdd.Address) ? "All Addresses" : netAdd); options.Listen(netAdd.Address, appHost.HttpPort); From 6954283af3b5c75d38aa1bf2a399538fb19a8f0f Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Thu, 19 Jan 2023 18:48:22 +0100 Subject: [PATCH 057/858] Update Emby.Dlna/Main/DlnaEntryPoint.cs Co-authored-by: Patrick Barron --- Emby.Dlna/Main/DlnaEntryPoint.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index 22903ec8f3..c9a091d8c2 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -286,10 +286,10 @@ namespace Emby.Dlna.Main // Only get bind addresses in LAN var bindAddresses = _networkManager - .GetInternalBindAddresses() - .Where(i => i.Address.AddressFamily == AddressFamily.InterNetwork - || (i.AddressFamily == AddressFamily.InterNetworkV6 && i.Address.ScopeId != 0)) - .ToList(); + .GetInternalBindAddresses() + .Where(i => i.Address.AddressFamily == AddressFamily.InterNetwork + || (i.AddressFamily == AddressFamily.InterNetworkV6 && i.Address.ScopeId != 0)) + .ToList(); if (bindAddresses.Count == 0) { From 6e460754142380eb7d2a509ee41b58e6a1c1665f Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Thu, 19 Jan 2023 19:03:43 +0100 Subject: [PATCH 058/858] Apply review suggestions --- Emby.Server.Implementations/Net/SocketFactory.cs | 8 ++------ RSSDP/SsdpCommunicationsServer.cs | 8 +++++--- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/Emby.Server.Implementations/Net/SocketFactory.cs b/Emby.Server.Implementations/Net/SocketFactory.cs index e0c75698d2..b6d87a7880 100644 --- a/Emby.Server.Implementations/Net/SocketFactory.cs +++ b/Emby.Server.Implementations/Net/SocketFactory.cs @@ -61,14 +61,10 @@ namespace Emby.Server.Implementations.Net } /// - public ISocket CreateUdpMulticastSocket(IPAddress ipAddress, IPAddress? bindIpAddress, int multicastTimeToLive, int localPort) + public ISocket CreateUdpMulticastSocket(IPAddress ipAddress, IPAddress bindIpAddress, int multicastTimeToLive, int localPort) { ArgumentNullException.ThrowIfNull(ipAddress); - - if (bindIpAddress == null) - { - bindIpAddress = IPAddress.Any; - } + ArgumentNullException.ThrowIfNull(bindIpAddress); if (multicastTimeToLive <= 0) { diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs index da357546dd..fb5a66aa10 100644 --- a/RSSDP/SsdpCommunicationsServer.cs +++ b/RSSDP/SsdpCommunicationsServer.cs @@ -336,13 +336,15 @@ namespace Rssdp.Infrastructure private List ListenForBroadcasts() { var sockets = new List(); + var nonNullBindAddresses = _networkManager.GetInternalBindAddresses().Where(x => x.Address != null); + if (_enableMultiSocketBinding) { - foreach (var address in _networkManager.GetInternalBindAddresses()) + foreach (var address in nonNullBindAddresses) { if (address.AddressFamily == AddressFamily.InterNetworkV6) { - // Not support IPv6 right now + // Not supporting IPv6 right now continue; } @@ -379,7 +381,7 @@ namespace Rssdp.Infrastructure { if (address.AddressFamily == AddressFamily.InterNetworkV6) { - // Not support IPv6 right now + // Not supporting IPv6 right now continue; } From a2ac791bb779297bedb608825602912228d415c1 Mon Sep 17 00:00:00 2001 From: Ronan Charles-Lorel Date: Tue, 31 Jan 2023 15:20:57 +0100 Subject: [PATCH 059/858] Add a way to add more invalid characters when sanitizing a filename --- Emby.Server.Implementations/IO/ManagedFileSystem.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 55f384ae8f..9b8831a1fe 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -294,7 +294,9 @@ namespace Emby.Server.Implementations.IO /// The filename is null. public string GetValidFilename(string filename) { - var invalid = Path.GetInvalidFileNameChars(); + //necessary because (as per the doc) GetInvalidFileNameChars is not exhaustive and may not return all invalid chars, which creates issues + char[] genericInvalidChars = {':'}; + var invalid = Path.GetInvalidFileNameChars().Concat(genericInvalidChars).ToArray(); var first = filename.IndexOfAny(invalid); if (first == -1) { From 31ac861b8560547c7e0c46513077abf76e6bc618 Mon Sep 17 00:00:00 2001 From: Ronan Charles-Lorel Date: Tue, 31 Jan 2023 15:47:47 +0100 Subject: [PATCH 060/858] Formatting --- Emby.Server.Implementations/IO/ManagedFileSystem.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 9b8831a1fe..7b8c79e8a9 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -294,8 +294,8 @@ namespace Emby.Server.Implementations.IO /// The filename is null. public string GetValidFilename(string filename) { - //necessary because (as per the doc) GetInvalidFileNameChars is not exhaustive and may not return all invalid chars, which creates issues - char[] genericInvalidChars = {':'}; + // necessary because (as per the doc) GetInvalidFileNameChars is not exhaustive and may not return all invalid chars, which creates issues + char[] genericInvalidChars = { ':' }; var invalid = Path.GetInvalidFileNameChars().Concat(genericInvalidChars).ToArray(); var first = filename.IndexOfAny(invalid); if (first == -1) From 6b8d1695297c28098924c5da18da133083ca9c92 Mon Sep 17 00:00:00 2001 From: Jean-Pierre Bachmann Date: Wed, 1 Feb 2023 19:34:58 +0100 Subject: [PATCH 061/858] Added CleanupCollection task --- .../Collections/CollectionManager.cs | 3 +- .../Tasks/CleanupCollectionPathsTask.cs | 118 ++++++++++++++++++ .../Collections/ICollectionManager.cs | 7 ++ 3 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs index b53c8ca512..20370fff5d 100644 --- a/Emby.Server.Implementations/Collections/CollectionManager.cs +++ b/Emby.Server.Implementations/Collections/CollectionManager.cs @@ -112,7 +112,8 @@ namespace Emby.Server.Implementations.Collections return Path.Combine(_appPaths.DataPath, "collections"); } - private Task GetCollectionsFolder(bool createIfNeeded) + /// + public Task GetCollectionsFolder(bool createIfNeeded) { return EnsureLibraryFolder(GetCollectionsFolderPath(), createIfNeeded); } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs new file mode 100644 index 0000000000..8e9270a125 --- /dev/null +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Emby.Server.Implementations.Collections; +using MediaBrowser.Controller.Collections; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Tasks; +using Microsoft.Extensions.Logging; + +namespace Emby.Server.Implementations.ScheduledTasks.Tasks; + +/// +/// Deletes Path references from collections that no longer exists. +/// +public class CleanupCollectionPathsTask : IScheduledTask +{ + private readonly ILocalizationManager _localization; + private readonly ICollectionManager _collectionManager; + private readonly ILogger _logger; + private readonly IProviderManager _providerManager; + private readonly IFileSystem _fileSystem; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// The logger. + /// The provider manager. + /// The filesystem. + public CleanupCollectionPathsTask( + ILocalizationManager localization, + ICollectionManager collectionManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem) + { + _localization = localization; + _collectionManager = collectionManager; + _logger = logger; + _providerManager = providerManager; + _fileSystem = fileSystem; + } + + /// + public string Name => _localization.GetLocalizedString("TaskCleanCollections"); + + /// + public string Key => "CleanCollections"; + + /// + public string Description => _localization.GetLocalizedString("TaskCleanCollectionsDescription"); + + /// + public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory"); + + /// + public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) + { + var collectionsFolder = await _collectionManager.GetCollectionsFolder(true).ConfigureAwait(false); + if (collectionsFolder is null) + { + _logger.LogInformation("There is no collection folder to be found."); + return; + } + + var collections = collectionsFolder.Children.OfType() + .ToArray(); + _logger.LogTrace("Found {CollectionLength} Boxsets.", collections.Length); + for (var index = 0; index < collections.Length; index++) + { + var collection = collections[index]; + _logger.LogTrace("Check Boxset {CollectionName}.", collection.Name); + var itemsToRemove = new List(); + foreach (var collectionLinkedChild in collection.LinkedChildren.ToArray()) + { + if (!File.Exists(collectionLinkedChild.Path)) + { + _logger.LogInformation("Item in boxset {0} cannot be found at {1}.", collection.Name, collectionLinkedChild.Path); + itemsToRemove.Add(collectionLinkedChild); + } + } + + if (itemsToRemove.Any()) + { + _logger.LogTrace("Update Boxset {CollectionName}.", collection.Name); + collection.LinkedChildren = collection.LinkedChildren.Except(itemsToRemove).ToArray(); + await collection.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken) + .ConfigureAwait(false); + + _providerManager.QueueRefresh( + collection.Id, + new MetadataRefreshOptions(new DirectoryService(_fileSystem)) + { + ForceSave = true + }, + RefreshPriority.High); + } + + progress.Report(100D / collections.Length * (index + 1)); + } + } + + /// + public IEnumerable GetDefaultTriggers() + { + return new[] { new TaskTriggerInfo() { Type = TaskTriggerInfo.TriggerStartup } }; + // return Enumerable.Empty(); + } +} diff --git a/MediaBrowser.Controller/Collections/ICollectionManager.cs b/MediaBrowser.Controller/Collections/ICollectionManager.cs index b8c33ee5a0..38a78a67b5 100644 --- a/MediaBrowser.Controller/Collections/ICollectionManager.cs +++ b/MediaBrowser.Controller/Collections/ICollectionManager.cs @@ -56,5 +56,12 @@ namespace MediaBrowser.Controller.Collections /// The user. /// IEnumerable{BaseItem}. IEnumerable CollapseItemsWithinBoxSets(IEnumerable items, User user); + + /// + /// Gets the folder where collections are stored. + /// + /// Will create the collection folder on the storage if set to true. + /// The folder instance referencing the collection storage. + Task GetCollectionsFolder(bool createIfNeeded); } } From 341658b55259084635b52983245800deff8d561d Mon Sep 17 00:00:00 2001 From: JPVenson Date: Wed, 1 Feb 2023 22:42:10 +0100 Subject: [PATCH 062/858] Update CleanupCollectionPathsTask.cs Removed code smell and switched to non creation for non existing collection folder --- .../ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs index 8e9270a125..7519af5b0d 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs @@ -65,16 +65,16 @@ public class CleanupCollectionPathsTask : IScheduledTask /// public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) { - var collectionsFolder = await _collectionManager.GetCollectionsFolder(true).ConfigureAwait(false); + var collectionsFolder = await _collectionManager.GetCollectionsFolder(false).ConfigureAwait(false); if (collectionsFolder is null) { _logger.LogInformation("There is no collection folder to be found."); return; } - var collections = collectionsFolder.Children.OfType() - .ToArray(); + var collections = collectionsFolder.Children.OfType().ToArray(); _logger.LogTrace("Found {CollectionLength} Boxsets.", collections.Length); + for (var index = 0; index < collections.Length; index++) { var collection = collections[index]; @@ -84,7 +84,7 @@ public class CleanupCollectionPathsTask : IScheduledTask { if (!File.Exists(collectionLinkedChild.Path)) { - _logger.LogInformation("Item in boxset {0} cannot be found at {1}.", collection.Name, collectionLinkedChild.Path); + _logger.LogInformation("Item in boxset {CollectionName} cannot be found at {ItemPath}.", collection.Name, collectionLinkedChild.Path); itemsToRemove.Add(collectionLinkedChild); } } @@ -113,6 +113,5 @@ public class CleanupCollectionPathsTask : IScheduledTask public IEnumerable GetDefaultTriggers() { return new[] { new TaskTriggerInfo() { Type = TaskTriggerInfo.TriggerStartup } }; - // return Enumerable.Empty(); } } From 0b71974054b4f520fa0ef3ead8f1b688ec2d1d74 Mon Sep 17 00:00:00 2001 From: Jean-Pierre Bachmann Date: Wed, 1 Feb 2023 23:45:06 +0100 Subject: [PATCH 063/858] Fixed whitespace --- .../ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs index 7519af5b0d..e1be43b9b2 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs @@ -74,7 +74,7 @@ public class CleanupCollectionPathsTask : IScheduledTask var collections = collectionsFolder.Children.OfType().ToArray(); _logger.LogTrace("Found {CollectionLength} Boxsets.", collections.Length); - + for (var index = 0; index < collections.Length; index++) { var collection = collections[index]; From 519709bf10e30a3666af100df4fcca206dcef498 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Fri, 3 Feb 2023 18:49:23 +0100 Subject: [PATCH 064/858] Revert "Remove DvdLib (#9068)" This reverts commit db1913b08fac0749133634efebd1ee7a7876147a. --- DvdLib/BigEndianBinaryReader.cs | 25 +++ DvdLib/DvdLib.csproj | 20 ++ DvdLib/Ifo/Cell.cs | 23 +++ DvdLib/Ifo/CellPlaybackInfo.cs | 52 +++++ DvdLib/Ifo/CellPositionInfo.cs | 19 ++ DvdLib/Ifo/Chapter.cs | 20 ++ DvdLib/Ifo/Dvd.cs | 167 +++++++++++++++ DvdLib/Ifo/DvdTime.cs | 39 ++++ DvdLib/Ifo/Program.cs | 16 ++ DvdLib/Ifo/ProgramChain.cs | 121 +++++++++++ DvdLib/Ifo/Title.cs | 70 +++++++ DvdLib/Ifo/UserOperation.cs | 37 ++++ DvdLib/Properties/AssemblyInfo.cs | 21 ++ .../ApplicationHost.cs | 4 + Jellyfin.sln | 6 + .../MediaEncoding/IMediaEncoder.cs | 8 + .../BdInfo/BdInfoDirectoryInfo.cs | 83 ++++++++ .../BdInfo/BdInfoExaminer.cs | 194 ++++++++++++++++++ .../BdInfo/BdInfoFileInfo.cs | 41 ++++ .../Encoder/MediaEncoder.cs | 79 +++++++ .../MediaBrowser.MediaEncoding.csproj | 1 + .../MediaInfo/BlurayDiscInfo.cs | 39 ++++ .../MediaInfo/IBlurayExaminer.cs | 15 ++ .../MediaBrowser.Providers.csproj | 1 + .../MediaInfo/FFProbeVideoInfo.cs | 155 +++++++++++++- .../MediaInfo/ProbeProvider.cs | 3 + 26 files changed, 1258 insertions(+), 1 deletion(-) create mode 100644 DvdLib/BigEndianBinaryReader.cs create mode 100644 DvdLib/DvdLib.csproj create mode 100644 DvdLib/Ifo/Cell.cs create mode 100644 DvdLib/Ifo/CellPlaybackInfo.cs create mode 100644 DvdLib/Ifo/CellPositionInfo.cs create mode 100644 DvdLib/Ifo/Chapter.cs create mode 100644 DvdLib/Ifo/Dvd.cs create mode 100644 DvdLib/Ifo/DvdTime.cs create mode 100644 DvdLib/Ifo/Program.cs create mode 100644 DvdLib/Ifo/ProgramChain.cs create mode 100644 DvdLib/Ifo/Title.cs create mode 100644 DvdLib/Ifo/UserOperation.cs create mode 100644 DvdLib/Properties/AssemblyInfo.cs create mode 100644 MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs create mode 100644 MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs create mode 100644 MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs create mode 100644 MediaBrowser.Model/MediaInfo/BlurayDiscInfo.cs create mode 100644 MediaBrowser.Model/MediaInfo/IBlurayExaminer.cs diff --git a/DvdLib/BigEndianBinaryReader.cs b/DvdLib/BigEndianBinaryReader.cs new file mode 100644 index 0000000000..b3aad85cec --- /dev/null +++ b/DvdLib/BigEndianBinaryReader.cs @@ -0,0 +1,25 @@ +#pragma warning disable CS1591 + +using System.Buffers.Binary; +using System.IO; + +namespace DvdLib +{ + public class BigEndianBinaryReader : BinaryReader + { + public BigEndianBinaryReader(Stream input) + : base(input) + { + } + + public override ushort ReadUInt16() + { + return BinaryPrimitives.ReadUInt16BigEndian(base.ReadBytes(2)); + } + + public override uint ReadUInt32() + { + return BinaryPrimitives.ReadUInt32BigEndian(base.ReadBytes(4)); + } + } +} diff --git a/DvdLib/DvdLib.csproj b/DvdLib/DvdLib.csproj new file mode 100644 index 0000000000..1053c0089f --- /dev/null +++ b/DvdLib/DvdLib.csproj @@ -0,0 +1,20 @@ + + + + + {713F42B5-878E-499D-A878-E4C652B1D5E8} + + + + + + + + net7.0 + false + true + AllDisabledByDefault + disable + + + diff --git a/DvdLib/Ifo/Cell.cs b/DvdLib/Ifo/Cell.cs new file mode 100644 index 0000000000..ea0b50e430 --- /dev/null +++ b/DvdLib/Ifo/Cell.cs @@ -0,0 +1,23 @@ +#pragma warning disable CS1591 + +using System.IO; + +namespace DvdLib.Ifo +{ + public class Cell + { + public CellPlaybackInfo PlaybackInfo { get; private set; } + + public CellPositionInfo PositionInfo { get; private set; } + + internal void ParsePlayback(BinaryReader br) + { + PlaybackInfo = new CellPlaybackInfo(br); + } + + internal void ParsePosition(BinaryReader br) + { + PositionInfo = new CellPositionInfo(br); + } + } +} diff --git a/DvdLib/Ifo/CellPlaybackInfo.cs b/DvdLib/Ifo/CellPlaybackInfo.cs new file mode 100644 index 0000000000..6e33a0ec5a --- /dev/null +++ b/DvdLib/Ifo/CellPlaybackInfo.cs @@ -0,0 +1,52 @@ +#pragma warning disable CS1591 + +using System.IO; + +namespace DvdLib.Ifo +{ + public enum BlockMode + { + NotInBlock = 0, + FirstCell = 1, + InBlock = 2, + LastCell = 3, + } + + public enum BlockType + { + Normal = 0, + Angle = 1, + } + + public enum PlaybackMode + { + Normal = 0, + StillAfterEachVOBU = 1, + } + + public class CellPlaybackInfo + { + public readonly BlockMode Mode; + public readonly BlockType Type; + public readonly bool SeamlessPlay; + public readonly bool Interleaved; + public readonly bool STCDiscontinuity; + public readonly bool SeamlessAngle; + public readonly PlaybackMode PlaybackMode; + public readonly bool Restricted; + public readonly byte StillTime; + public readonly byte CommandNumber; + public readonly DvdTime PlaybackTime; + public readonly uint FirstSector; + public readonly uint FirstILVUEndSector; + public readonly uint LastVOBUStartSector; + public readonly uint LastSector; + + internal CellPlaybackInfo(BinaryReader br) + { + br.BaseStream.Seek(0x4, SeekOrigin.Current); + PlaybackTime = new DvdTime(br.ReadBytes(4)); + br.BaseStream.Seek(0x10, SeekOrigin.Current); + } + } +} diff --git a/DvdLib/Ifo/CellPositionInfo.cs b/DvdLib/Ifo/CellPositionInfo.cs new file mode 100644 index 0000000000..216aa0f77a --- /dev/null +++ b/DvdLib/Ifo/CellPositionInfo.cs @@ -0,0 +1,19 @@ +#pragma warning disable CS1591 + +using System.IO; + +namespace DvdLib.Ifo +{ + public class CellPositionInfo + { + public readonly ushort VOBId; + public readonly byte CellId; + + internal CellPositionInfo(BinaryReader br) + { + VOBId = br.ReadUInt16(); + br.ReadByte(); + CellId = br.ReadByte(); + } + } +} diff --git a/DvdLib/Ifo/Chapter.cs b/DvdLib/Ifo/Chapter.cs new file mode 100644 index 0000000000..e786cb5536 --- /dev/null +++ b/DvdLib/Ifo/Chapter.cs @@ -0,0 +1,20 @@ +#pragma warning disable CS1591 + +namespace DvdLib.Ifo +{ + public class Chapter + { + public ushort ProgramChainNumber { get; private set; } + + public ushort ProgramNumber { get; private set; } + + public uint ChapterNumber { get; private set; } + + public Chapter(ushort pgcNum, ushort programNum, uint chapterNum) + { + ProgramChainNumber = pgcNum; + ProgramNumber = programNum; + ChapterNumber = chapterNum; + } + } +} diff --git a/DvdLib/Ifo/Dvd.cs b/DvdLib/Ifo/Dvd.cs new file mode 100644 index 0000000000..7f8ece47dc --- /dev/null +++ b/DvdLib/Ifo/Dvd.cs @@ -0,0 +1,167 @@ +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; + +namespace DvdLib.Ifo +{ + public class Dvd + { + private readonly ushort _titleSetCount; + public readonly List Titles; + + private ushort _titleCount; + public readonly Dictionary<ushort, string> VTSPaths = new Dictionary<ushort, string>(); + public Dvd(string path) + { + Titles = new List<Title>(); + var allFiles = new DirectoryInfo(path).GetFiles(path, SearchOption.AllDirectories); + + var vmgPath = allFiles.FirstOrDefault(i => string.Equals(i.Name, "VIDEO_TS.IFO", StringComparison.OrdinalIgnoreCase)) ?? + allFiles.FirstOrDefault(i => string.Equals(i.Name, "VIDEO_TS.BUP", StringComparison.OrdinalIgnoreCase)); + + if (vmgPath == null) + { + foreach (var ifo in allFiles) + { + if (!string.Equals(ifo.Extension, ".ifo", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + var nums = ifo.Name.Split('_', StringSplitOptions.RemoveEmptyEntries); + if (nums.Length >= 2 && ushort.TryParse(nums[1], out var ifoNumber)) + { + ReadVTS(ifoNumber, ifo.FullName); + } + } + } + else + { + using (var vmgFs = new FileStream(vmgPath.FullName, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + using (var vmgRead = new BigEndianBinaryReader(vmgFs)) + { + vmgFs.Seek(0x3E, SeekOrigin.Begin); + _titleSetCount = vmgRead.ReadUInt16(); + + // read address of TT_SRPT + vmgFs.Seek(0xC4, SeekOrigin.Begin); + uint ttSectorPtr = vmgRead.ReadUInt32(); + vmgFs.Seek(ttSectorPtr * 2048, SeekOrigin.Begin); + ReadTT_SRPT(vmgRead); + } + } + + for (ushort titleSetNum = 1; titleSetNum <= _titleSetCount; titleSetNum++) + { + ReadVTS(titleSetNum, allFiles); + } + } + } + + private void ReadTT_SRPT(BinaryReader read) + { + _titleCount = read.ReadUInt16(); + read.BaseStream.Seek(6, SeekOrigin.Current); + for (uint titleNum = 1; titleNum <= _titleCount; titleNum++) + { + var t = new Title(titleNum); + t.ParseTT_SRPT(read); + Titles.Add(t); + } + } + + private void ReadVTS(ushort vtsNum, IReadOnlyList<FileInfo> allFiles) + { + var filename = string.Format(CultureInfo.InvariantCulture, "VTS_{0:00}_0.IFO", vtsNum); + + var vtsPath = allFiles.FirstOrDefault(i => string.Equals(i.Name, filename, StringComparison.OrdinalIgnoreCase)) ?? + allFiles.FirstOrDefault(i => string.Equals(i.Name, Path.ChangeExtension(filename, ".bup"), StringComparison.OrdinalIgnoreCase)); + + if (vtsPath == null) + { + throw new FileNotFoundException("Unable to find VTS IFO file"); + } + + ReadVTS(vtsNum, vtsPath.FullName); + } + + private void ReadVTS(ushort vtsNum, string vtsPath) + { + VTSPaths[vtsNum] = vtsPath; + + using (var vtsFs = new FileStream(vtsPath, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + using (var vtsRead = new BigEndianBinaryReader(vtsFs)) + { + // Read VTS_PTT_SRPT + vtsFs.Seek(0xC8, SeekOrigin.Begin); + uint vtsPttSrptSecPtr = vtsRead.ReadUInt32(); + uint baseAddr = (vtsPttSrptSecPtr * 2048); + vtsFs.Seek(baseAddr, SeekOrigin.Begin); + + ushort numTitles = vtsRead.ReadUInt16(); + vtsRead.ReadUInt16(); + uint endaddr = vtsRead.ReadUInt32(); + uint[] offsets = new uint[numTitles]; + for (ushort titleNum = 0; titleNum < numTitles; titleNum++) + { + offsets[titleNum] = vtsRead.ReadUInt32(); + } + + for (uint titleNum = 0; titleNum < numTitles; titleNum++) + { + uint chapNum = 1; + vtsFs.Seek(baseAddr + offsets[titleNum], SeekOrigin.Begin); + var t = Titles.FirstOrDefault(vtst => vtst.IsVTSTitle(vtsNum, titleNum + 1)); + if (t == null) + { + continue; + } + + do + { + t.Chapters.Add(new Chapter(vtsRead.ReadUInt16(), vtsRead.ReadUInt16(), chapNum)); + if (titleNum + 1 < numTitles && vtsFs.Position == (baseAddr + offsets[titleNum + 1])) + { + break; + } + + chapNum++; + } + while (vtsFs.Position < (baseAddr + endaddr)); + } + + // Read VTS_PGCI + vtsFs.Seek(0xCC, SeekOrigin.Begin); + uint vtsPgciSecPtr = vtsRead.ReadUInt32(); + vtsFs.Seek(vtsPgciSecPtr * 2048, SeekOrigin.Begin); + + long startByte = vtsFs.Position; + + ushort numPgcs = vtsRead.ReadUInt16(); + vtsFs.Seek(6, SeekOrigin.Current); + for (ushort pgcNum = 1; pgcNum <= numPgcs; pgcNum++) + { + byte pgcCat = vtsRead.ReadByte(); + bool entryPgc = (pgcCat & 0x80) != 0; + uint titleNum = (uint)(pgcCat & 0x7F); + + vtsFs.Seek(3, SeekOrigin.Current); + uint vtsPgcOffset = vtsRead.ReadUInt32(); + + var t = Titles.FirstOrDefault(vtst => vtst.IsVTSTitle(vtsNum, titleNum)); + if (t != null) + { + t.AddPgc(vtsRead, startByte + vtsPgcOffset, entryPgc, pgcNum); + } + } + } + } + } + } +} diff --git a/DvdLib/Ifo/DvdTime.cs b/DvdLib/Ifo/DvdTime.cs new file mode 100644 index 0000000000..d231406106 --- /dev/null +++ b/DvdLib/Ifo/DvdTime.cs @@ -0,0 +1,39 @@ +#pragma warning disable CS1591 + +using System; + +namespace DvdLib.Ifo +{ + public class DvdTime + { + public readonly byte Hour, Minute, Second, Frames, FrameRate; + + public DvdTime(byte[] data) + { + Hour = GetBCDValue(data[0]); + Minute = GetBCDValue(data[1]); + Second = GetBCDValue(data[2]); + Frames = GetBCDValue((byte)(data[3] & 0x3F)); + + if ((data[3] & 0x80) != 0) + { + FrameRate = 30; + } + else if ((data[3] & 0x40) != 0) + { + FrameRate = 25; + } + } + + private static byte GetBCDValue(byte data) + { + return (byte)((((data & 0xF0) >> 4) * 10) + (data & 0x0F)); + } + + public static explicit operator TimeSpan(DvdTime time) + { + int ms = (int)(((1.0 / (double)time.FrameRate) * time.Frames) * 1000.0); + return new TimeSpan(0, time.Hour, time.Minute, time.Second, ms); + } + } +} diff --git a/DvdLib/Ifo/Program.cs b/DvdLib/Ifo/Program.cs new file mode 100644 index 0000000000..3d94fa7dc1 --- /dev/null +++ b/DvdLib/Ifo/Program.cs @@ -0,0 +1,16 @@ +#pragma warning disable CS1591 + +using System.Collections.Generic; + +namespace DvdLib.Ifo +{ + public class Program + { + public IReadOnlyList<Cell> Cells { get; } + + public Program(List<Cell> cells) + { + Cells = cells; + } + } +} diff --git a/DvdLib/Ifo/ProgramChain.cs b/DvdLib/Ifo/ProgramChain.cs new file mode 100644 index 0000000000..83c0051b90 --- /dev/null +++ b/DvdLib/Ifo/ProgramChain.cs @@ -0,0 +1,121 @@ +#pragma warning disable CS1591 + +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace DvdLib.Ifo +{ + public enum ProgramPlaybackMode + { + Sequential, + Random, + Shuffle + } + + public class ProgramChain + { + private byte _programCount; + public readonly List<Program> Programs; + + private byte _cellCount; + public readonly List<Cell> Cells; + + public DvdTime PlaybackTime { get; private set; } + + public UserOperation ProhibitedUserOperations { get; private set; } + + public byte[] AudioStreamControl { get; private set; } // 8*2 entries + public byte[] SubpictureStreamControl { get; private set; } // 32*4 entries + + private ushort _nextProgramNumber; + + private ushort _prevProgramNumber; + + private ushort _goupProgramNumber; + + public ProgramPlaybackMode PlaybackMode { get; private set; } + + public uint ProgramCount { get; private set; } + + public byte StillTime { get; private set; } + + public byte[] Palette { get; private set; } // 16*4 entries + + private ushort _commandTableOffset; + + private ushort _programMapOffset; + private ushort _cellPlaybackOffset; + private ushort _cellPositionOffset; + + public readonly uint VideoTitleSetIndex; + + internal ProgramChain(uint vtsPgcNum) + { + VideoTitleSetIndex = vtsPgcNum; + Cells = new List<Cell>(); + Programs = new List<Program>(); + } + + internal void ParseHeader(BinaryReader br) + { + long startPos = br.BaseStream.Position; + + br.ReadUInt16(); + _programCount = br.ReadByte(); + _cellCount = br.ReadByte(); + PlaybackTime = new DvdTime(br.ReadBytes(4)); + ProhibitedUserOperations = (UserOperation)br.ReadUInt32(); + AudioStreamControl = br.ReadBytes(16); + SubpictureStreamControl = br.ReadBytes(128); + + _nextProgramNumber = br.ReadUInt16(); + _prevProgramNumber = br.ReadUInt16(); + _goupProgramNumber = br.ReadUInt16(); + + StillTime = br.ReadByte(); + byte pbMode = br.ReadByte(); + if (pbMode == 0) + { + PlaybackMode = ProgramPlaybackMode.Sequential; + } + else + { + PlaybackMode = ((pbMode & 0x80) == 0) ? ProgramPlaybackMode.Random : ProgramPlaybackMode.Shuffle; + } + + ProgramCount = (uint)(pbMode & 0x7F); + + Palette = br.ReadBytes(64); + _commandTableOffset = br.ReadUInt16(); + _programMapOffset = br.ReadUInt16(); + _cellPlaybackOffset = br.ReadUInt16(); + _cellPositionOffset = br.ReadUInt16(); + + // read position info + br.BaseStream.Seek(startPos + _cellPositionOffset, SeekOrigin.Begin); + for (int cellNum = 0; cellNum < _cellCount; cellNum++) + { + var c = new Cell(); + c.ParsePosition(br); + Cells.Add(c); + } + + br.BaseStream.Seek(startPos + _cellPlaybackOffset, SeekOrigin.Begin); + for (int cellNum = 0; cellNum < _cellCount; cellNum++) + { + Cells[cellNum].ParsePlayback(br); + } + + br.BaseStream.Seek(startPos + _programMapOffset, SeekOrigin.Begin); + var cellNumbers = new List<int>(); + for (int progNum = 0; progNum < _programCount; progNum++) cellNumbers.Add(br.ReadByte() - 1); + + for (int i = 0; i < cellNumbers.Count; i++) + { + int max = (i + 1 == cellNumbers.Count) ? _cellCount : cellNumbers[i + 1]; + Programs.Add(new Program(Cells.Where((c, idx) => idx >= cellNumbers[i] && idx < max).ToList())); + } + } + } +} diff --git a/DvdLib/Ifo/Title.cs b/DvdLib/Ifo/Title.cs new file mode 100644 index 0000000000..29a0b95c72 --- /dev/null +++ b/DvdLib/Ifo/Title.cs @@ -0,0 +1,70 @@ +#pragma warning disable CS1591 + +using System.Collections.Generic; +using System.IO; + +namespace DvdLib.Ifo +{ + public class Title + { + public uint TitleNumber { get; private set; } + + public uint AngleCount { get; private set; } + + public ushort ChapterCount { get; private set; } + + public byte VideoTitleSetNumber { get; private set; } + + private ushort _parentalManagementMask; + private byte _titleNumberInVTS; + private uint _vtsStartSector; // relative to start of entire disk + + public ProgramChain EntryProgramChain { get; private set; } + + public readonly List<ProgramChain> ProgramChains; + + public readonly List<Chapter> Chapters; + + public Title(uint titleNum) + { + ProgramChains = new List<ProgramChain>(); + Chapters = new List<Chapter>(); + Chapters = new List<Chapter>(); + TitleNumber = titleNum; + } + + public bool IsVTSTitle(uint vtsNum, uint vtsTitleNum) + { + return (vtsNum == VideoTitleSetNumber && vtsTitleNum == _titleNumberInVTS); + } + + internal void ParseTT_SRPT(BinaryReader br) + { + byte titleType = br.ReadByte(); + // TODO parse Title Type + + AngleCount = br.ReadByte(); + ChapterCount = br.ReadUInt16(); + _parentalManagementMask = br.ReadUInt16(); + VideoTitleSetNumber = br.ReadByte(); + _titleNumberInVTS = br.ReadByte(); + _vtsStartSector = br.ReadUInt32(); + } + + internal void AddPgc(BinaryReader br, long startByte, bool entryPgc, uint pgcNum) + { + long curPos = br.BaseStream.Position; + br.BaseStream.Seek(startByte, SeekOrigin.Begin); + + var pgc = new ProgramChain(pgcNum); + pgc.ParseHeader(br); + ProgramChains.Add(pgc); + if (entryPgc) + { + EntryProgramChain = pgc; + } + + br.BaseStream.Seek(curPos, SeekOrigin.Begin); + } + } +} diff --git a/DvdLib/Ifo/UserOperation.cs b/DvdLib/Ifo/UserOperation.cs new file mode 100644 index 0000000000..5d111ebc06 --- /dev/null +++ b/DvdLib/Ifo/UserOperation.cs @@ -0,0 +1,37 @@ +#pragma warning disable CS1591 + +using System; + +namespace DvdLib.Ifo +{ + [Flags] + public enum UserOperation + { + None = 0, + TitleOrTimePlay = 1, + ChapterSearchOrPlay = 2, + TitlePlay = 4, + Stop = 8, + GoUp = 16, + TimeOrChapterSearch = 32, + PrevOrTopProgramSearch = 64, + NextProgramSearch = 128, + ForwardScan = 256, + BackwardScan = 512, + TitleMenuCall = 1024, + RootMenuCall = 2048, + SubpictureMenuCall = 4096, + AudioMenuCall = 8192, + AngleMenuCall = 16384, + ChapterMenuCall = 32768, + Resume = 65536, + ButtonSelectOrActive = 131072, + StillOff = 262144, + PauseOn = 524288, + AudioStreamChange = 1048576, + SubpictureStreamChange = 2097152, + AngleChange = 4194304, + KaraokeAudioPresentationModeChange = 8388608, + VideoPresentationModeChange = 16777216, + } +} diff --git a/DvdLib/Properties/AssemblyInfo.cs b/DvdLib/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..6acd571d68 --- /dev/null +++ b/DvdLib/Properties/AssemblyInfo.cs @@ -0,0 +1,21 @@ +using System.Reflection; +using System.Resources; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("DvdLib")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Jellyfin Project")] +[assembly: AssemblyProduct("Jellyfin Server")] +[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: NeutralResourcesLanguage("en")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 37a9e7715d..d43d368e0b 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -80,11 +80,13 @@ using MediaBrowser.Controller.Subtitles; using MediaBrowser.Controller.SyncPlay; using MediaBrowser.Controller.TV; using MediaBrowser.LocalMetadata.Savers; +using MediaBrowser.MediaEncoding.BdInfo; using MediaBrowser.MediaEncoding.Subtitles; using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; +using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Net; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.System; @@ -529,6 +531,8 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton<ILocalizationManager, LocalizationManager>(); + serviceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>(); + serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>(); serviceCollection.AddSingleton<IUserDataManager, UserDataManager>(); diff --git a/Jellyfin.sln b/Jellyfin.sln index cad23fc5ee..ee772cbe40 100644 --- a/Jellyfin.sln +++ b/Jellyfin.sln @@ -21,6 +21,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Drawing", "src\Jel EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Photos", "Emby.Photos\Emby.Photos.csproj", "{89AB4548-770D-41FD-A891-8DAFF44F452C}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DvdLib", "DvdLib\DvdLib.csproj", "{713F42B5-878E-499D-A878-E4C652B1D5E8}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Server.Implementations", "Emby.Server.Implementations\Emby.Server.Implementations.csproj", "{E383961B-9356-4D5D-8233-9A1079D03055}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RSSDP", "RSSDP\RSSDP.csproj", "{21002819-C39A-4D3E-BE83-2A276A77FB1F}" @@ -135,6 +137,10 @@ Global {89AB4548-770D-41FD-A891-8DAFF44F452C}.Debug|Any CPU.Build.0 = Debug|Any CPU {89AB4548-770D-41FD-A891-8DAFF44F452C}.Release|Any CPU.ActiveCfg = Release|Any CPU {89AB4548-770D-41FD-A891-8DAFF44F452C}.Release|Any CPU.Build.0 = Release|Any CPU + {713F42B5-878E-499D-A878-E4C652B1D5E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {713F42B5-878E-499D-A878-E4C652B1D5E8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {713F42B5-878E-499D-A878-E4C652B1D5E8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {713F42B5-878E-499D-A878-E4C652B1D5E8}.Release|Any CPU.Build.0 = Release|Any CPU {E383961B-9356-4D5D-8233-9A1079D03055}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E383961B-9356-4D5D-8233-9A1079D03055}.Debug|Any CPU.Build.0 = Debug|Any CPU {E383961B-9356-4D5D-8233-9A1079D03055}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index bc6207ac51..fe8e9063ee 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -187,5 +187,13 @@ namespace MediaBrowser.Controller.MediaEncoding /// <param name="path">The path.</param> /// <param name="pathType">The type of path.</param> void UpdateEncoderPath(string path, string pathType); + + /// <summary> + /// Gets the primary playlist of .vob files. + /// </summary> + /// <param name="path">The to the .vob files.</param> + /// <param name="titleNumber">The title number to start with.</param> + /// <returns>A playlist.</returns> + IEnumerable<string> GetPrimaryPlaylistVobFiles(string path, uint? titleNumber); } } diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs new file mode 100644 index 0000000000..7e026b42e3 --- /dev/null +++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs @@ -0,0 +1,83 @@ +#pragma warning disable CS1591 + +using System; +using System.Linq; +using BDInfo.IO; +using MediaBrowser.Model.IO; + +namespace MediaBrowser.MediaEncoding.BdInfo +{ + public class BdInfoDirectoryInfo : IDirectoryInfo + { + private readonly IFileSystem _fileSystem; + + private readonly FileSystemMetadata _impl; + + public BdInfoDirectoryInfo(IFileSystem fileSystem, string path) + { + _fileSystem = fileSystem; + _impl = _fileSystem.GetDirectoryInfo(path); + } + + private BdInfoDirectoryInfo(IFileSystem fileSystem, FileSystemMetadata impl) + { + _fileSystem = fileSystem; + _impl = impl; + } + + public string Name => _impl.Name; + + public string FullName => _impl.FullName; + + public IDirectoryInfo? Parent + { + get + { + var parentFolder = System.IO.Path.GetDirectoryName(_impl.FullName); + if (parentFolder is not null) + { + return new BdInfoDirectoryInfo(_fileSystem, parentFolder); + } + + return null; + } + } + + public IDirectoryInfo[] GetDirectories() + { + return Array.ConvertAll( + _fileSystem.GetDirectories(_impl.FullName).ToArray(), + x => new BdInfoDirectoryInfo(_fileSystem, x)); + } + + public IFileInfo[] GetFiles() + { + return Array.ConvertAll( + _fileSystem.GetFiles(_impl.FullName).ToArray(), + x => new BdInfoFileInfo(x)); + } + + public IFileInfo[] GetFiles(string searchPattern) + { + return Array.ConvertAll( + _fileSystem.GetFiles(_impl.FullName, new[] { searchPattern }, false, false).ToArray(), + x => new BdInfoFileInfo(x)); + } + + public IFileInfo[] GetFiles(string searchPattern, System.IO.SearchOption searchOption) + { + return Array.ConvertAll( + _fileSystem.GetFiles( + _impl.FullName, + new[] { searchPattern }, + false, + (searchOption & System.IO.SearchOption.AllDirectories) == System.IO.SearchOption.AllDirectories).ToArray(), + x => new BdInfoFileInfo(x)); + } + + public static IDirectoryInfo FromFileSystemPath(IFileSystem fs, string path) + { + return new BdInfoDirectoryInfo(fs, path); + } + } +} diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs new file mode 100644 index 0000000000..3e53cbf29f --- /dev/null +++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using BDInfo; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.MediaInfo; + +namespace MediaBrowser.MediaEncoding.BdInfo +{ + /// <summary> + /// Class BdInfoExaminer. + /// </summary> + public class BdInfoExaminer : IBlurayExaminer + { + private readonly IFileSystem _fileSystem; + + /// <summary> + /// Initializes a new instance of the <see cref="BdInfoExaminer" /> class. + /// </summary> + /// <param name="fileSystem">The filesystem.</param> + public BdInfoExaminer(IFileSystem fileSystem) + { + _fileSystem = fileSystem; + } + + /// <summary> + /// Gets the disc info. + /// </summary> + /// <param name="path">The path.</param> + /// <returns>BlurayDiscInfo.</returns> + public BlurayDiscInfo GetDiscInfo(string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + throw new ArgumentNullException(nameof(path)); + } + + var bdrom = new BDROM(BdInfoDirectoryInfo.FromFileSystemPath(_fileSystem, path)); + + bdrom.Scan(); + + // Get the longest playlist + var playlist = bdrom.PlaylistFiles.Values.OrderByDescending(p => p.TotalLength).FirstOrDefault(p => p.IsValid); + + var outputStream = new BlurayDiscInfo + { + MediaStreams = Array.Empty<MediaStream>() + }; + + if (playlist is null) + { + return outputStream; + } + + outputStream.Chapters = playlist.Chapters.ToArray(); + + outputStream.RunTimeTicks = TimeSpan.FromSeconds(playlist.TotalLength).Ticks; + + var mediaStreams = new List<MediaStream>(); + + foreach (var stream in playlist.SortedStreams) + { + if (stream is TSVideoStream videoStream) + { + AddVideoStream(mediaStreams, videoStream); + continue; + } + + if (stream is TSAudioStream audioStream) + { + AddAudioStream(mediaStreams, audioStream); + continue; + } + + if (stream is TSTextStream textStream) + { + AddSubtitleStream(mediaStreams, textStream); + continue; + } + + if (stream is TSGraphicsStream graphicsStream) + { + AddSubtitleStream(mediaStreams, graphicsStream); + } + } + + outputStream.MediaStreams = mediaStreams.ToArray(); + + outputStream.PlaylistName = playlist.Name; + + if (playlist.StreamClips is not null && playlist.StreamClips.Any()) + { + // Get the files in the playlist + outputStream.Files = playlist.StreamClips.Select(i => i.StreamFile.Name).ToArray(); + } + + return outputStream; + } + + /// <summary> + /// Adds the video stream. + /// </summary> + /// <param name="streams">The streams.</param> + /// <param name="videoStream">The video stream.</param> + private void AddVideoStream(List<MediaStream> streams, TSVideoStream videoStream) + { + var mediaStream = new MediaStream + { + BitRate = Convert.ToInt32(videoStream.BitRate), + Width = videoStream.Width, + Height = videoStream.Height, + Codec = videoStream.CodecShortName, + IsInterlaced = videoStream.IsInterlaced, + Type = MediaStreamType.Video, + Index = streams.Count + }; + + if (videoStream.FrameRateDenominator > 0) + { + float frameRateEnumerator = videoStream.FrameRateEnumerator; + float frameRateDenominator = videoStream.FrameRateDenominator; + + mediaStream.AverageFrameRate = mediaStream.RealFrameRate = frameRateEnumerator / frameRateDenominator; + } + + streams.Add(mediaStream); + } + + /// <summary> + /// Adds the audio stream. + /// </summary> + /// <param name="streams">The streams.</param> + /// <param name="audioStream">The audio stream.</param> + private void AddAudioStream(List<MediaStream> streams, TSAudioStream audioStream) + { + var stream = new MediaStream + { + Codec = audioStream.CodecShortName, + Language = audioStream.LanguageCode, + Channels = audioStream.ChannelCount, + SampleRate = audioStream.SampleRate, + Type = MediaStreamType.Audio, + Index = streams.Count + }; + + var bitrate = Convert.ToInt32(audioStream.BitRate); + + if (bitrate > 0) + { + stream.BitRate = bitrate; + } + + if (audioStream.LFE > 0) + { + stream.Channels = audioStream.ChannelCount + 1; + } + + streams.Add(stream); + } + + /// <summary> + /// Adds the subtitle stream. + /// </summary> + /// <param name="streams">The streams.</param> + /// <param name="textStream">The text stream.</param> + private void AddSubtitleStream(List<MediaStream> streams, TSTextStream textStream) + { + streams.Add(new MediaStream + { + Language = textStream.LanguageCode, + Codec = textStream.CodecShortName, + Type = MediaStreamType.Subtitle, + Index = streams.Count + }); + } + + /// <summary> + /// Adds the subtitle stream. + /// </summary> + /// <param name="streams">The streams.</param> + /// <param name="textStream">The text stream.</param> + private void AddSubtitleStream(List<MediaStream> streams, TSGraphicsStream textStream) + { + streams.Add(new MediaStream + { + Language = textStream.LanguageCode, + Codec = textStream.CodecShortName, + Type = MediaStreamType.Subtitle, + Index = streams.Count + }); + } + } +} diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs new file mode 100644 index 0000000000..d55688e3df --- /dev/null +++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs @@ -0,0 +1,41 @@ +#pragma warning disable CS1591 + +using System.IO; +using MediaBrowser.Model.IO; + +namespace MediaBrowser.MediaEncoding.BdInfo +{ + public class BdInfoFileInfo : BDInfo.IO.IFileInfo + { + private FileSystemMetadata _impl; + + public BdInfoFileInfo(FileSystemMetadata impl) + { + _impl = impl; + } + + public string Name => _impl.Name; + + public string FullName => _impl.FullName; + + public string Extension => _impl.Extension; + + public long Length => _impl.Length; + + public bool IsDir => _impl.IsDirectory; + + public Stream OpenRead() + { + return new FileStream( + FullName, + FileMode.Open, + FileAccess.Read, + FileShare.Read); + } + + public StreamReader OpenText() + { + return new StreamReader(OpenRead()); + } + } +} diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index cef02d5f8f..4a795625d0 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -865,6 +865,85 @@ namespace MediaBrowser.MediaEncoding.Encoder throw new NotImplementedException(); } + /// <inheritdoc /> + public IEnumerable<string> GetPrimaryPlaylistVobFiles(string path, uint? titleNumber) + { + // min size 300 mb + const long MinPlayableSize = 314572800; + + // Try to eliminate menus and intros by skipping all files at the front of the list that are less than the minimum size + // Once we reach a file that is at least the minimum, return all subsequent ones + var allVobs = _fileSystem.GetFiles(path, true) + .Where(file => string.Equals(file.Extension, ".vob", StringComparison.OrdinalIgnoreCase)) + .OrderBy(i => i.FullName) + .ToList(); + + // If we didn't find any satisfying the min length, just take them all + if (allVobs.Count == 0) + { + _logger.LogWarning("No vobs found in dvd structure."); + return Enumerable.Empty<string>(); + } + + if (titleNumber.HasValue) + { + var prefix = string.Format( + CultureInfo.InvariantCulture, + titleNumber.Value >= 10 ? "VTS_{0}_" : "VTS_0{0}_", + titleNumber.Value); + var vobs = allVobs.Where(i => i.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)).ToList(); + + if (vobs.Count > 0) + { + var minSizeVobs = vobs + .SkipWhile(f => f.Length < MinPlayableSize) + .ToList(); + + return minSizeVobs.Count == 0 ? vobs.Select(i => i.FullName) : minSizeVobs.Select(i => i.FullName); + } + + _logger.LogWarning("Could not determine vob file list for {Path} using DvdLib. Will scan using file sizes.", path); + } + + var files = allVobs + .SkipWhile(f => f.Length < MinPlayableSize) + .ToList(); + + // If we didn't find any satisfying the min length, just take them all + if (files.Count == 0) + { + _logger.LogWarning("Vob size filter resulted in zero matches. Taking all vobs."); + files = allVobs; + } + + // Assuming they're named "vts_05_01", take all files whose second part matches that of the first file + if (files.Count > 0) + { + var parts = _fileSystem.GetFileNameWithoutExtension(files[0]).Split('_'); + + if (parts.Length == 3) + { + var title = parts[1]; + + files = files.TakeWhile(f => + { + var fileParts = _fileSystem.GetFileNameWithoutExtension(f).Split('_'); + + return fileParts.Length == 3 && string.Equals(title, fileParts[1], StringComparison.OrdinalIgnoreCase); + }).ToList(); + + // If this resulted in not getting any vobs, just take them all + if (files.Count == 0) + { + _logger.LogWarning("Vob filename filter resulted in zero matches. Taking all vobs."); + files = allVobs; + } + } + } + + return files.Select(i => i.FullName); + } + public bool CanExtractSubtitles(string codec) { // TODO is there ever a case when a subtitle can't be extracted?? diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj index f4438fe194..a0624fe76b 100644 --- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj +++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj @@ -22,6 +22,7 @@ </ItemGroup> <ItemGroup> + <PackageReference Include="BDInfo" /> <PackageReference Include="libse" /> <PackageReference Include="Microsoft.Extensions.Http" /> <PackageReference Include="System.Text.Encoding.CodePages" /> diff --git a/MediaBrowser.Model/MediaInfo/BlurayDiscInfo.cs b/MediaBrowser.Model/MediaInfo/BlurayDiscInfo.cs new file mode 100644 index 0000000000..83f982a5c8 --- /dev/null +++ b/MediaBrowser.Model/MediaInfo/BlurayDiscInfo.cs @@ -0,0 +1,39 @@ +#nullable disable +#pragma warning disable CS1591 + +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Model.MediaInfo +{ + /// <summary> + /// Represents the result of BDInfo output. + /// </summary> + public class BlurayDiscInfo + { + /// <summary> + /// Gets or sets the media streams. + /// </summary> + /// <value>The media streams.</value> + public MediaStream[] MediaStreams { get; set; } + + /// <summary> + /// Gets or sets the run time ticks. + /// </summary> + /// <value>The run time ticks.</value> + public long? RunTimeTicks { get; set; } + + /// <summary> + /// Gets or sets the files. + /// </summary> + /// <value>The files.</value> + public string[] Files { get; set; } + + public string PlaylistName { get; set; } + + /// <summary> + /// Gets or sets the chapters. + /// </summary> + /// <value>The chapters.</value> + public double[] Chapters { get; set; } + } +} diff --git a/MediaBrowser.Model/MediaInfo/IBlurayExaminer.cs b/MediaBrowser.Model/MediaInfo/IBlurayExaminer.cs new file mode 100644 index 0000000000..5b7d1d03c3 --- /dev/null +++ b/MediaBrowser.Model/MediaInfo/IBlurayExaminer.cs @@ -0,0 +1,15 @@ +namespace MediaBrowser.Model.MediaInfo +{ + /// <summary> + /// Interface IBlurayExaminer. + /// </summary> + public interface IBlurayExaminer + { + /// <summary> + /// Gets the disc info. + /// </summary> + /// <param name="path">The path.</param> + /// <returns>BlurayDiscInfo.</returns> + BlurayDiscInfo GetDiscInfo(string path); + } +} diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 6a40833d7f..a0f97df40d 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -8,6 +8,7 @@ <ItemGroup> <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" /> <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" /> + <ProjectReference Include="..\DvdLib\DvdLib.csproj" /> </ItemGroup> <ItemGroup> diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index 0f35c6a5ea..81434b8620 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -9,6 +9,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using DvdLib.Ifo; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Chapters; using MediaBrowser.Controller.Configuration; @@ -36,6 +37,7 @@ namespace MediaBrowser.Providers.MediaInfo private readonly ILogger<FFProbeVideoInfo> _logger; private readonly IMediaEncoder _mediaEncoder; private readonly IItemRepository _itemRepo; + private readonly IBlurayExaminer _blurayExaminer; private readonly ILocalizationManager _localization; private readonly IEncodingManager _encodingManager; private readonly IServerConfigurationManager _config; @@ -51,6 +53,7 @@ namespace MediaBrowser.Providers.MediaInfo IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder, IItemRepository itemRepo, + IBlurayExaminer blurayExaminer, ILocalizationManager localization, IEncodingManager encodingManager, IServerConfigurationManager config, @@ -64,6 +67,7 @@ namespace MediaBrowser.Providers.MediaInfo _mediaSourceManager = mediaSourceManager; _mediaEncoder = mediaEncoder; _itemRepo = itemRepo; + _blurayExaminer = blurayExaminer; _localization = localization; _encodingManager = encodingManager; _config = config; @@ -80,16 +84,47 @@ namespace MediaBrowser.Providers.MediaInfo CancellationToken cancellationToken) where T : Video { + BlurayDiscInfo blurayDiscInfo = null; + Model.MediaInfo.MediaInfo mediaInfoResult = null; if (!item.IsShortcut || options.EnableRemoteContentProbe) { + string[] streamFileNames = null; + + if (item.VideoType == VideoType.Dvd) + { + streamFileNames = FetchFromDvdLib(item); + + if (streamFileNames.Length == 0) + { + _logger.LogError("No playable vobs found in dvd structure, skipping ffprobe."); + return ItemUpdateType.MetadataImport; + } + } + else if (item.VideoType == VideoType.BluRay) + { + var inputPath = item.Path; + + blurayDiscInfo = GetBDInfo(inputPath); + + streamFileNames = blurayDiscInfo.Files; + + if (streamFileNames.Length == 0) + { + _logger.LogError("No playable vobs found in bluray structure, skipping ffprobe."); + return ItemUpdateType.MetadataImport; + } + } + + streamFileNames ??= Array.Empty<string>(); + mediaInfoResult = await GetMediaInfo(item, cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); } - await Fetch(item, cancellationToken, mediaInfoResult, options).ConfigureAwait(false); + await Fetch(item, cancellationToken, mediaInfoResult, blurayDiscInfo, options).ConfigureAwait(false); return ItemUpdateType.MetadataImport; } @@ -129,6 +164,7 @@ namespace MediaBrowser.Providers.MediaInfo Video video, CancellationToken cancellationToken, Model.MediaInfo.MediaInfo mediaInfo, + BlurayDiscInfo blurayInfo, MetadataRefreshOptions options) { List<MediaStream> mediaStreams; @@ -182,6 +218,10 @@ namespace MediaBrowser.Providers.MediaInfo video.Container = mediaInfo.Container; chapters = mediaInfo.Chapters ?? Array.Empty<ChapterInfo>(); + if (blurayInfo is not null) + { + FetchBdInfo(video, ref chapters, mediaStreams, blurayInfo); + } } else { @@ -277,6 +317,91 @@ namespace MediaBrowser.Providers.MediaInfo } } + private void FetchBdInfo(BaseItem item, ref ChapterInfo[] chapters, List<MediaStream> mediaStreams, BlurayDiscInfo blurayInfo) + { + var video = (Video)item; + + // video.PlayableStreamFileNames = blurayInfo.Files.ToList(); + + // Use BD Info if it has multiple m2ts. Otherwise, treat it like a video file and rely more on ffprobe output + if (blurayInfo.Files.Length > 1) + { + int? currentHeight = null; + int? currentWidth = null; + int? currentBitRate = null; + + var videoStream = mediaStreams.FirstOrDefault(s => s.Type == MediaStreamType.Video); + + // Grab the values that ffprobe recorded + if (videoStream is not null) + { + currentBitRate = videoStream.BitRate; + currentWidth = videoStream.Width; + currentHeight = videoStream.Height; + } + + // Fill video properties from the BDInfo result + mediaStreams.Clear(); + mediaStreams.AddRange(blurayInfo.MediaStreams); + + if (blurayInfo.RunTimeTicks.HasValue && blurayInfo.RunTimeTicks.Value > 0) + { + video.RunTimeTicks = blurayInfo.RunTimeTicks; + } + + if (blurayInfo.Chapters is not null) + { + double[] brChapter = blurayInfo.Chapters; + chapters = new ChapterInfo[brChapter.Length]; + for (int i = 0; i < brChapter.Length; i++) + { + chapters[i] = new ChapterInfo + { + StartPositionTicks = TimeSpan.FromSeconds(brChapter[i]).Ticks + }; + } + } + + videoStream = mediaStreams.FirstOrDefault(s => s.Type == MediaStreamType.Video); + + // Use the ffprobe values if these are empty + if (videoStream is not null) + { + videoStream.BitRate = IsEmpty(videoStream.BitRate) ? currentBitRate : videoStream.BitRate; + videoStream.Width = IsEmpty(videoStream.Width) ? currentWidth : videoStream.Width; + videoStream.Height = IsEmpty(videoStream.Height) ? currentHeight : videoStream.Height; + } + } + } + + private bool IsEmpty(int? num) + { + return !num.HasValue || num.Value == 0; + } + + /// <summary> + /// Gets information about the longest playlist on a bdrom. + /// </summary> + /// <param name="path">The path.</param> + /// <returns>VideoStream.</returns> + private BlurayDiscInfo GetBDInfo(string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + throw new ArgumentNullException(nameof(path)); + } + + try + { + return _blurayExaminer.GetDiscInfo(path); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting BDInfo"); + return null; + } + } + private void FetchEmbeddedInfo(Video video, Model.MediaInfo.MediaInfo data, MetadataRefreshOptions refreshOptions, LibraryOptions libraryOptions) { var replaceData = refreshOptions.ReplaceAllMetadata; @@ -558,5 +683,33 @@ namespace MediaBrowser.Providers.MediaInfo return chapters; } + + private string[] FetchFromDvdLib(Video item) + { + var path = item.Path; + var dvd = new Dvd(path); + + var primaryTitle = dvd.Titles.OrderByDescending(GetRuntime).FirstOrDefault(); + + byte? titleNumber = null; + + if (primaryTitle is not null) + { + titleNumber = primaryTitle.VideoTitleSetNumber; + item.RunTimeTicks = GetRuntime(primaryTitle); + } + + return _mediaEncoder.GetPrimaryPlaylistVobFiles(item.Path, titleNumber) + .Select(Path.GetFileName) + .ToArray(); + } + + private long GetRuntime(Title title) + { + return title.ProgramChains + .Select(i => (TimeSpan)i.PlaybackTime) + .Select(i => i.Ticks) + .Sum(); + } } } diff --git a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs index 31fa3da1ce..2800219552 100644 --- a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs @@ -53,6 +53,7 @@ namespace MediaBrowser.Providers.MediaInfo /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param> /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> /// <param name="itemRepo">Instance of the <see cref="IItemRepository"/> interface.</param> + /// <param name="blurayExaminer">Instance of the <see cref="IBlurayExaminer"/> interface.</param> /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param> /// <param name="encodingManager">Instance of the <see cref="IEncodingManager"/> interface.</param> /// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> @@ -66,6 +67,7 @@ namespace MediaBrowser.Providers.MediaInfo IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder, IItemRepository itemRepo, + IBlurayExaminer blurayExaminer, ILocalizationManager localization, IEncodingManager encodingManager, IServerConfigurationManager config, @@ -85,6 +87,7 @@ namespace MediaBrowser.Providers.MediaInfo mediaSourceManager, mediaEncoder, itemRepo, + blurayExaminer, localization, encodingManager, config, From ddfdec7f46dd31d437dc6210658392fc7f894138 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Fri, 3 Feb 2023 20:03:55 +0100 Subject: [PATCH 065/858] Fix BD and DVD folder probing and playback --- Jellyfin.Api/Helpers/TranscodingJobHelper.cs | 5 +- .../MediaEncoding/EncodingHelper.cs | 10 ++ .../MediaEncoding/IMediaEncoder.cs | 16 ++ .../Encoder/EncodingUtils.cs | 27 +++- .../Encoder/MediaEncoder.cs | 148 ++++++++---------- .../MediaInfo/FFProbeVideoInfo.cs | 68 ++++---- 6 files changed, 148 insertions(+), 126 deletions(-) diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index 12960f87a2..29cb3d2f9f 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -525,7 +525,10 @@ public class TranscodingJobHelper : IDisposable if (state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode) { var attachmentPath = Path.Combine(_appPaths.CachePath, "attachments", state.MediaSource.Id); - await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false); + if (state.VideoType != VideoType.Dvd) + { + await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false); + } if (state.SubtitleStream.IsExternal && string.Equals(Path.GetExtension(state.SubtitleStream.Path), ".mks", StringComparison.OrdinalIgnoreCase)) { diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index a844e64433..b3a1b2a991 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -535,6 +535,16 @@ namespace MediaBrowser.Controller.MediaEncoding { var mediaPath = state.MediaPath ?? string.Empty; + if (state.MediaSource.VideoType == VideoType.Dvd) + { + return _mediaEncoder.GetInputArgument(_mediaEncoder.GetPrimaryPlaylistVobFiles(state.MediaPath, null).ToList(), state.MediaSource); + } + + if (state.MediaSource.VideoType == VideoType.BluRay) + { + return _mediaEncoder.GetInputArgument(_mediaEncoder.GetPrimaryPlaylistM2TsFiles(state.MediaPath, null).ToList(), state.MediaSource); + } + return _mediaEncoder.GetInputArgument(mediaPath, state.MediaSource); } diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index fe8e9063ee..c34ce5d29b 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -153,6 +153,14 @@ namespace MediaBrowser.Controller.MediaEncoding /// <returns>System.String.</returns> string GetInputArgument(string inputFile, MediaSourceInfo mediaSource); + /// <summary> + /// Gets the input argument. + /// </summary> + /// <param name="inputFiles">The input files.</param> + /// <param name="mediaSource">The mediaSource.</param> + /// <returns>System.String.</returns> + string GetInputArgument(IReadOnlyList<string> inputFiles, MediaSourceInfo mediaSource); + /// <summary> /// Gets the input argument for an external subtitle file. /// </summary> @@ -195,5 +203,13 @@ namespace MediaBrowser.Controller.MediaEncoding /// <param name="titleNumber">The title number to start with.</param> /// <returns>A playlist.</returns> IEnumerable<string> GetPrimaryPlaylistVobFiles(string path, uint? titleNumber); + + /// <summary> + /// Gets the primary playlist of .m2ts files. + /// </summary> + /// <param name="path">The to the .m2ts files.</param> + /// <param name="titleNumber">The title number to start with.</param> + /// <returns>A playlist.</returns> + IEnumerable<string> GetPrimaryPlaylistM2TsFiles(string path, uint? titleNumber); } } diff --git a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs index d0ea0429b5..0f202a90e5 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs @@ -1,7 +1,9 @@ #pragma warning disable CS1591 using System; +using System.Collections.Generic; using System.Globalization; +using System.Linq; using MediaBrowser.Model.MediaInfo; namespace MediaBrowser.MediaEncoding.Encoder @@ -15,21 +17,38 @@ namespace MediaBrowser.MediaEncoding.Encoder return string.Format(CultureInfo.InvariantCulture, "\"{0}\"", inputFile); } - return GetConcatInputArgument(inputFile, inputPrefix); + return GetFileInputArgument(inputFile, inputPrefix); + } + + public static string GetInputArgument(string inputPrefix, IReadOnlyList<string> inputFiles, MediaProtocol protocol) + { + if (protocol != MediaProtocol.File) + { + return string.Format(CultureInfo.InvariantCulture, "\"{0}\"", inputFiles[0]); + } + + return GetConcatInputArgument(inputFiles, inputPrefix); } /// <summary> /// Gets the concat input argument. /// </summary> - /// <param name="inputFile">The input file.</param> + /// <param name="inputFiles">The input files.</param> /// <param name="inputPrefix">The input prefix.</param> /// <returns>System.String.</returns> - private static string GetConcatInputArgument(string inputFile, string inputPrefix) + private static string GetConcatInputArgument(IReadOnlyList<string> inputFiles, 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(inputFile, inputPrefix); + return GetFileInputArgument(inputFiles[0], inputPrefix); } /// <summary> diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 4a795625d0..df31ba83e5 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -51,6 +51,7 @@ namespace MediaBrowser.MediaEncoding.Encoder private readonly IServerConfigurationManager _configurationManager; private readonly IFileSystem _fileSystem; private readonly ILocalizationManager _localization; + private readonly IBlurayExaminer _blurayExaminer; private readonly IConfiguration _config; private readonly IServerConfigurationManager _serverConfig; private readonly string _startupOptionFFmpegPath; @@ -95,6 +96,7 @@ namespace MediaBrowser.MediaEncoding.Encoder ILogger<MediaEncoder> logger, IServerConfigurationManager configurationManager, IFileSystem fileSystem, + IBlurayExaminer blurayExaminer, ILocalizationManager localization, IConfiguration config, IServerConfigurationManager serverConfig) @@ -102,6 +104,7 @@ namespace MediaBrowser.MediaEncoding.Encoder _logger = logger; _configurationManager = configurationManager; _fileSystem = fileSystem; + _blurayExaminer = blurayExaminer; _localization = localization; _config = config; _serverConfig = serverConfig; @@ -117,16 +120,22 @@ namespace MediaBrowser.MediaEncoding.Encoder /// <inheritdoc /> public string ProbePath => _ffprobePath; + /// <inheritdoc /> public Version EncoderVersion => _ffmpegVersion; + /// <inheritdoc /> public bool IsPkeyPauseSupported => _isPkeyPauseSupported; + /// <inheritdoc /> public bool IsVaapiDeviceAmd => _isVaapiDeviceAmd; + /// <inheritdoc /> public bool IsVaapiDeviceInteliHD => _isVaapiDeviceInteliHD; + /// <inheritdoc /> public bool IsVaapiDeviceInteli965 => _isVaapiDeviceInteli965; + /// <inheritdoc /> public bool IsVaapiDeviceSupportVulkanFmtModifier => _isVaapiDeviceSupportVulkanFmtModifier; /// <summary> @@ -344,26 +353,31 @@ namespace MediaBrowser.MediaEncoding.Encoder _ffmpegVersion = validator.GetFFmpegVersion(); } + /// <inheritdoc /> public bool SupportsEncoder(string encoder) { return _encoders.Contains(encoder, StringComparer.OrdinalIgnoreCase); } + /// <inheritdoc /> public bool SupportsDecoder(string decoder) { return _decoders.Contains(decoder, StringComparer.OrdinalIgnoreCase); } + /// <inheritdoc /> public bool SupportsHwaccel(string hwaccel) { return _hwaccels.Contains(hwaccel, StringComparer.OrdinalIgnoreCase); } + /// <inheritdoc /> public bool SupportsFilter(string filter) { return _filters.Contains(filter, StringComparer.OrdinalIgnoreCase); } + /// <inheritdoc /> public bool SupportsFilterWithOption(FilterOptionType option) { if (_filtersWithOption.TryGetValue((int)option, out var val)) @@ -394,24 +408,16 @@ namespace MediaBrowser.MediaEncoding.Encoder return true; } - /// <summary> - /// Gets the media info. - /// </summary> - /// <param name="request">The request.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> + /// <inheritdoc /> public Task<MediaInfo> GetMediaInfo(MediaInfoRequest request, CancellationToken cancellationToken) { var extractChapters = request.MediaType == DlnaProfileType.Video && request.ExtractChapters; - var inputFile = request.MediaSource.Path; - string analyzeDuration = string.Empty; string ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty; if (request.MediaSource.AnalyzeDurationMs > 0) { - analyzeDuration = "-analyzeduration " + - (request.MediaSource.AnalyzeDurationMs * 1000).ToString(); + analyzeDuration = "-analyzeduration " + (request.MediaSource.AnalyzeDurationMs * 1000).ToString(); } else if (!string.IsNullOrEmpty(ffmpegAnalyzeDuration)) { @@ -419,7 +425,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } return GetMediaInfoInternal( - GetInputArgument(inputFile, request.MediaSource), + GetInputArgument(request.MediaSource.Path, request.MediaSource), request.MediaSource.Path, request.MediaSource.Protocol, extractChapters, @@ -429,36 +435,24 @@ namespace MediaBrowser.MediaEncoding.Encoder cancellationToken); } - /// <summary> - /// Gets the input argument. - /// </summary> - /// <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(string inputFile, MediaSourceInfo mediaSource) + /// <inheritdoc /> + public string GetInputArgument(IReadOnlyList<string> inputFiles, MediaSourceInfo mediaSource) { - var prefix = "file"; - if (mediaSource.VideoType == VideoType.BluRay - || mediaSource.IsoType == IsoType.BluRay) - { - prefix = "bluray"; - } - - return EncodingUtils.GetInputArgument(prefix, inputFile, mediaSource.Protocol); + return EncodingUtils.GetInputArgument("file", inputFiles, mediaSource.Protocol); } - /// <summary> - /// Gets the input argument for an external subtitle file. - /// </summary> - /// <param name="inputFile">The input file.</param> - /// <returns>System.String.</returns> - /// <exception cref="ArgumentException">Unrecognized InputType.</exception> + /// <inheritdoc /> + public string GetInputArgument(string inputFile, MediaSourceInfo mediaSource) + { + return EncodingUtils.GetInputArgument("file", new List<string>() { inputFile }, mediaSource.Protocol); + } + + /// <inheritdoc /> public string GetExternalSubtitleInputArgument(string inputFile) { const string Prefix = "file"; - return EncodingUtils.GetInputArgument(Prefix, inputFile, MediaProtocol.File); + return EncodingUtils.GetInputArgument(Prefix, new List<string> { inputFile }, MediaProtocol.File); } /// <summary> @@ -549,6 +543,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } } + /// <inheritdoc /> public Task<string> ExtractAudioImage(string path, int? imageStreamIndex, CancellationToken cancellationToken) { var mediaSource = new MediaSourceInfo @@ -559,11 +554,13 @@ namespace MediaBrowser.MediaEncoding.Encoder return ExtractImage(path, null, null, imageStreamIndex, mediaSource, true, null, null, ImageFormat.Jpg, cancellationToken); } + /// <inheritdoc /> public Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream videoStream, Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken) { return ExtractImage(inputFile, container, videoStream, null, mediaSource, false, threedFormat, offset, ImageFormat.Jpg, cancellationToken); } + /// <inheritdoc /> public Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, ImageFormat? targetFormat, CancellationToken cancellationToken) { return ExtractImage(inputFile, container, imageStream, imageStreamIndex, mediaSource, false, null, null, targetFormat, cancellationToken); @@ -767,6 +764,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } } + /// <inheritdoc /> public string GetTimeParameter(long ticks) { var time = TimeSpan.FromTicks(ticks); @@ -868,80 +866,56 @@ namespace MediaBrowser.MediaEncoding.Encoder /// <inheritdoc /> public IEnumerable<string> GetPrimaryPlaylistVobFiles(string path, uint? titleNumber) { - // min size 300 mb - const long MinPlayableSize = 314572800; - - // Try to eliminate menus and intros by skipping all files at the front of the list that are less than the minimum size - // Once we reach a file that is at least the minimum, return all subsequent ones + // Eliminate menus and intros by omitting VIDEO_TS.VOB and all subsequent title VOBs ending with _0.VOB var allVobs = _fileSystem.GetFiles(path, true) - .Where(file => string.Equals(file.Extension, ".vob", StringComparison.OrdinalIgnoreCase)) + .Where(file => string.Equals(file.Extension, ".VOB", StringComparison.OrdinalIgnoreCase)) + .Where(file => !string.Equals(file.Name, "VIDEO_TS.VOB", StringComparison.OrdinalIgnoreCase)) + .Where(file => !file.Name.EndsWith("_0.VOB", StringComparison.OrdinalIgnoreCase)) .OrderBy(i => i.FullName) .ToList(); - // If we didn't find any satisfying the min length, just take them all - if (allVobs.Count == 0) - { - _logger.LogWarning("No vobs found in dvd structure."); - return Enumerable.Empty<string>(); - } - if (titleNumber.HasValue) { - var prefix = string.Format( - CultureInfo.InvariantCulture, - titleNumber.Value >= 10 ? "VTS_{0}_" : "VTS_0{0}_", - titleNumber.Value); + var prefix = string.Format(CultureInfo.InvariantCulture, "VTS_{0:D2}_", titleNumber.Value); var vobs = allVobs.Where(i => i.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)).ToList(); if (vobs.Count > 0) { - var minSizeVobs = vobs - .SkipWhile(f => f.Length < MinPlayableSize) - .ToList(); - - return minSizeVobs.Count == 0 ? vobs.Select(i => i.FullName) : minSizeVobs.Select(i => i.FullName); + return vobs.Select(i => i.FullName); } - _logger.LogWarning("Could not determine vob file list for {Path} using DvdLib. Will scan using file sizes.", path); + _logger.LogWarning("Could not determine VOB file list for title {Title} of {Path}.", titleNumber, path); } - var files = allVobs - .SkipWhile(f => f.Length < MinPlayableSize) + // Check for multiple big titles (> 900 MB) + var titles = allVobs + .Where(vob => vob.Length >= 900 * 1024 * 1024) + .Select(vob => _fileSystem.GetFileNameWithoutExtension(vob).Split('_')[1]) + .GroupBy(x => x) + .Select(y => y.First()) .ToList(); - // If we didn't find any satisfying the min length, just take them all - if (files.Count == 0) + // Fall back to first title if no big title is found + if (titles.FirstOrDefault() == null) { - _logger.LogWarning("Vob size filter resulted in zero matches. Taking all vobs."); - files = allVobs; + titles.Add(_fileSystem.GetFileNameWithoutExtension(allVobs[0]).Split('_')[1]); } - // Assuming they're named "vts_05_01", take all files whose second part matches that of the first file - if (files.Count > 0) - { - var parts = _fileSystem.GetFileNameWithoutExtension(files[0]).Split('_'); + // Aggregate all VOBs of the titles + return allVobs + .Where(vob => titles.Contains(_fileSystem.GetFileNameWithoutExtension(vob).Split('_')[1])) + .Select(i => i.FullName) + .ToList(); + } - if (parts.Length == 3) - { - var title = parts[1]; + public IEnumerable<string> GetPrimaryPlaylistM2TsFiles(string path, uint? titleNumber) + { + var validPlaybackFiles = _blurayExaminer.GetDiscInfo(path).Files; + var directoryFiles = _fileSystem.GetFiles(path + "/BDMV/STREAM/"); - files = files.TakeWhile(f => - { - var fileParts = _fileSystem.GetFileNameWithoutExtension(f).Split('_'); - - return fileParts.Length == 3 && string.Equals(title, fileParts[1], StringComparison.OrdinalIgnoreCase); - }).ToList(); - - // If this resulted in not getting any vobs, just take them all - if (files.Count == 0) - { - _logger.LogWarning("Vob filename filter resulted in zero matches. Taking all vobs."); - files = allVobs; - } - } - } - - return files.Select(i => i.FullName); + return directoryFiles + .Where(f => validPlaybackFiles.Contains(f.Name, StringComparer.OrdinalIgnoreCase)) + .Select(f => f.FullName); } public bool CanExtractSubtitles(string codec) diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index 81434b8620..393e1f22a7 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -90,37 +90,50 @@ namespace MediaBrowser.Providers.MediaInfo if (!item.IsShortcut || options.EnableRemoteContentProbe) { - string[] streamFileNames = null; - if (item.VideoType == VideoType.Dvd) { - streamFileNames = FetchFromDvdLib(item); + // Fetch metadata of first VOB + var vobs = _mediaEncoder.GetPrimaryPlaylistVobFiles(item.Path, null).ToList(); + mediaInfoResult = await GetMediaInfo( + new Video + { + Path = vobs.First() + }, + cancellationToken).ConfigureAwait(false); - if (streamFileNames.Length == 0) + // Remove first VOB + vobs.RemoveAt(0); + + // Add runtime from all other VOBs + foreach (var vob in vobs) { - _logger.LogError("No playable vobs found in dvd structure, skipping ffprobe."); - return ItemUpdateType.MetadataImport; + var tmpMediaInfo = await GetMediaInfo( + new Video + { + Path = vob + }, + cancellationToken).ConfigureAwait(false); + + mediaInfoResult.RunTimeTicks += tmpMediaInfo.RunTimeTicks; } } - else if (item.VideoType == VideoType.BluRay) + else { - var inputPath = item.Path; - - blurayDiscInfo = GetBDInfo(inputPath); - - streamFileNames = blurayDiscInfo.Files; - - if (streamFileNames.Length == 0) + if (item.VideoType == VideoType.BluRay) { - _logger.LogError("No playable vobs found in bluray structure, skipping ffprobe."); - return ItemUpdateType.MetadataImport; + blurayDiscInfo = GetBDInfo(item.Path); + + if (blurayDiscInfo.Files.Length == 0) + { + _logger.LogError("No playable vobs found in bluray structure, skipping ffprobe."); + return ItemUpdateType.MetadataImport; + } } + + mediaInfoResult = await GetMediaInfo(item, cancellationToken).ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); } - streamFileNames ??= Array.Empty<string>(); - - mediaInfoResult = await GetMediaInfo(item, cancellationToken).ConfigureAwait(false); - cancellationToken.ThrowIfCancellationRequested(); } @@ -189,19 +202,8 @@ namespace MediaBrowser.Providers.MediaInfo } mediaAttachments = mediaInfo.MediaAttachments; - video.TotalBitrate = mediaInfo.Bitrate; - // video.FormatName = (mediaInfo.Container ?? string.Empty) - // .Replace("matroska", "mkv", StringComparison.OrdinalIgnoreCase); - - // For DVDs this may not always be accurate, so don't set the runtime if the item already has one - var needToSetRuntime = video.VideoType != VideoType.Dvd || video.RunTimeTicks is null || video.RunTimeTicks.Value == 0; - - if (needToSetRuntime) - { - video.RunTimeTicks = mediaInfo.RunTimeTicks; - } - + video.RunTimeTicks = mediaInfo.RunTimeTicks; video.Size = mediaInfo.Size; if (video.VideoType == VideoType.VideoFile) @@ -321,8 +323,6 @@ namespace MediaBrowser.Providers.MediaInfo { var video = (Video)item; - // video.PlayableStreamFileNames = blurayInfo.Files.ToList(); - // Use BD Info if it has multiple m2ts. Otherwise, treat it like a video file and rely more on ffprobe output if (blurayInfo.Files.Length > 1) { From edf3909157a5ef10436b7ebf5717b36a6bac9e7c Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Sat, 4 Feb 2023 00:08:51 +0100 Subject: [PATCH 066/858] Use FFmpeg concat for DVD and BD folder playback --- Jellyfin.Api/Helpers/TranscodingJobHelper.cs | 5 +++ .../MediaEncoding/EncodingHelper.cs | 14 ++++++- .../MediaEncoding/IMediaEncoder.cs | 7 ++++ .../Encoder/MediaEncoder.cs | 40 +++++++++++++++++++ 4 files changed, 64 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index 29cb3d2f9f..cb73ad7657 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -323,6 +323,11 @@ public class TranscodingJobHelper : IDisposable if (delete(job.Path!)) { await DeletePartialStreamFiles(job.Path!, job.Type, 0, 1500).ConfigureAwait(false); + if (job.MediaSource?.VideoType == VideoType.Dvd || job.MediaSource?.VideoType == VideoType.BluRay) + { + var path = Path.GetDirectoryName(job.Path) + "/" + job.MediaSource.Id + ".concat"; + File.Delete(path); + } } if (closeLiveStream && !string.IsNullOrWhiteSpace(job.LiveStreamId)) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index b3a1b2a991..7c4892ea50 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -941,8 +941,18 @@ namespace MediaBrowser.Controller.MediaEncoding arg.Append(canvasArgs); } - arg.Append(" -i ") - .Append(GetInputPathArgument(state)); + if (state.MediaSource.VideoType == VideoType.Dvd || state.MediaSource.VideoType == VideoType.BluRay) + { + var tmpConcatPath = options.TranscodingTempPath + "/" + state.MediaSource.Id + ".concat"; + _mediaEncoder.GenerateConcatConfig(state.MediaSource, tmpConcatPath); + arg.Append(" -f concat -safe 0 "); + arg.Append(" -i " + tmpConcatPath + " "); + } + else + { + arg.Append(" -i ") + .Append(GetInputPathArgument(state)); + } // sub2video for external graphical subtitles if (state.SubtitleStream is not null diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index c34ce5d29b..716adc8f0b 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -211,5 +211,12 @@ namespace MediaBrowser.Controller.MediaEncoding /// <param name="titleNumber">The title number to start with.</param> /// <returns>A playlist.</returns> IEnumerable<string> GetPrimaryPlaylistM2TsFiles(string path, uint? titleNumber); + + /// <summary> + /// Generates a FFmpeg concat config for the source. + /// </summary> + /// <param name="source">The <see cref="MediaSourceInfo"/>.</param> + /// <param name="concatFilePath">The path the config should be written to.</param> + void GenerateConcatConfig(MediaSourceInfo source, string concatFilePath); } } diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index df31ba83e5..f4e6ea4285 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -918,6 +918,46 @@ namespace MediaBrowser.MediaEncoding.Encoder .Select(f => f.FullName); } + public void GenerateConcatConfig(MediaSourceInfo source, string concatFilePath) + { + var files = new List<string>(); + var videoType = source.VideoType; + if (videoType == VideoType.Dvd) + { + files = GetPrimaryPlaylistVobFiles(source.Path, null).ToList(); + } + else if (videoType == VideoType.BluRay) + { + files = GetPrimaryPlaylistM2TsFiles(source.Path, null).ToList(); + } + + var lines = new List<string>(); + + foreach (var path in files) + { + var fileinfo = _fileSystem.GetFileInfo(path); + var mediaInfoResult = GetMediaInfo( + new MediaInfoRequest + { + MediaType = DlnaProfileType.Video, + MediaSource = new MediaSourceInfo + { + Path = path, + Protocol = MediaProtocol.File, + VideoType = videoType + } + }, + CancellationToken.None).GetAwaiter().GetResult(); + + var duration = TimeSpan.FromTicks(mediaInfoResult.RunTimeTicks.Value).TotalSeconds; + + lines.Add("file " + "'" + path + "'"); + lines.Add("duration " + duration); + } + + File.WriteAllLinesAsync(concatFilePath, lines, CancellationToken.None).GetAwaiter().GetResult(); + } + public bool CanExtractSubtitles(string codec) { // TODO is there ever a case when a subtitle can't be extracted?? From d89cd188eb015cff3450e3c4f7f1e48adec52e8c Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Sat, 4 Feb 2023 12:33:22 +0100 Subject: [PATCH 067/858] Fix BD ISO playback --- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index f4e6ea4285..0bf89ec477 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -444,7 +444,13 @@ namespace MediaBrowser.MediaEncoding.Encoder /// <inheritdoc /> public string GetInputArgument(string inputFile, MediaSourceInfo mediaSource) { - return EncodingUtils.GetInputArgument("file", new List<string>() { inputFile }, mediaSource.Protocol); + var prefix = "file"; + if (mediaSource.IsoType == IsoType.BluRay) + { + prefix = "bluray"; + } + + return EncodingUtils.GetInputArgument(prefix, new List<string>() { inputFile }, mediaSource.Protocol); } /// <inheritdoc /> From 3d4b2f840a3652490e0dbe559e5aa853a45130e2 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Sat, 4 Feb 2023 12:34:24 +0100 Subject: [PATCH 068/858] Fix BD and DVD folder recognition for tv episodes --- .../Library/Resolvers/BaseVideoResolver.cs | 16 +++++++++-- .../MediaInfo/FFProbeVideoInfo.cs | 28 +++++++++++-------- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs index e8615e7db7..27062228fa 100644 --- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs @@ -67,11 +67,23 @@ namespace Emby.Server.Implementations.Library.Resolvers { if (IsDvdDirectory(child.FullName, filename, args.DirectoryService)) { - videoType = VideoType.Dvd; + var videoTmp = new TVideoType + { + Path = args.Path, + VideoType = VideoType.Dvd + }; + Set3DFormat(videoTmp); + return videoTmp; } else if (IsBluRayDirectory(filename)) { - videoType = VideoType.BluRay; + var videoTmp = new TVideoType + { + Path = args.Path, + VideoType = VideoType.BluRay + }; + Set3DFormat(videoTmp); + return videoTmp; } } else if (IsDvdFile(filename)) diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index 393e1f22a7..f2f6d8a124 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -117,19 +117,25 @@ namespace MediaBrowser.Providers.MediaInfo mediaInfoResult.RunTimeTicks += tmpMediaInfo.RunTimeTicks; } } + else if (item.VideoType == VideoType.BluRay) + { + blurayDiscInfo = GetBDInfo(item.Path); + var m2ts = _mediaEncoder.GetPrimaryPlaylistM2TsFiles(item.Path, null).ToList(); + mediaInfoResult = await GetMediaInfo( + new Video + { + Path = m2ts.First() + }, + cancellationToken).ConfigureAwait(false); + + if (blurayDiscInfo.Files.Length == 0) + { + _logger.LogError("No playable vobs found in bluray structure, skipping ffprobe."); + return ItemUpdateType.MetadataImport; + } + } else { - if (item.VideoType == VideoType.BluRay) - { - blurayDiscInfo = GetBDInfo(item.Path); - - if (blurayDiscInfo.Files.Length == 0) - { - _logger.LogError("No playable vobs found in bluray structure, skipping ffprobe."); - return ItemUpdateType.MetadataImport; - } - } - mediaInfoResult = await GetMediaInfo(item, cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); } From 626bb24bdd0aaca0fafde98bed973f770575b490 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Sat, 4 Feb 2023 16:35:14 +0100 Subject: [PATCH 069/858] Remove DvdLib --- DvdLib/BigEndianBinaryReader.cs | 25 --- DvdLib/DvdLib.csproj | 20 --- DvdLib/Ifo/Cell.cs | 23 --- DvdLib/Ifo/CellPlaybackInfo.cs | 52 ------ DvdLib/Ifo/CellPositionInfo.cs | 19 -- DvdLib/Ifo/Chapter.cs | 20 --- DvdLib/Ifo/Dvd.cs | 167 ------------------ DvdLib/Ifo/DvdTime.cs | 39 ---- DvdLib/Ifo/Program.cs | 16 -- DvdLib/Ifo/ProgramChain.cs | 121 ------------- DvdLib/Ifo/Title.cs | 70 -------- DvdLib/Ifo/UserOperation.cs | 37 ---- DvdLib/Properties/AssemblyInfo.cs | 21 --- Jellyfin.sln | 6 - .../MediaBrowser.Providers.csproj | 1 - .../MediaInfo/FFProbeVideoInfo.cs | 29 --- 16 files changed, 666 deletions(-) delete mode 100644 DvdLib/BigEndianBinaryReader.cs delete mode 100644 DvdLib/DvdLib.csproj delete mode 100644 DvdLib/Ifo/Cell.cs delete mode 100644 DvdLib/Ifo/CellPlaybackInfo.cs delete mode 100644 DvdLib/Ifo/CellPositionInfo.cs delete mode 100644 DvdLib/Ifo/Chapter.cs delete mode 100644 DvdLib/Ifo/Dvd.cs delete mode 100644 DvdLib/Ifo/DvdTime.cs delete mode 100644 DvdLib/Ifo/Program.cs delete mode 100644 DvdLib/Ifo/ProgramChain.cs delete mode 100644 DvdLib/Ifo/Title.cs delete mode 100644 DvdLib/Ifo/UserOperation.cs delete mode 100644 DvdLib/Properties/AssemblyInfo.cs diff --git a/DvdLib/BigEndianBinaryReader.cs b/DvdLib/BigEndianBinaryReader.cs deleted file mode 100644 index b3aad85cec..0000000000 --- a/DvdLib/BigEndianBinaryReader.cs +++ /dev/null @@ -1,25 +0,0 @@ -#pragma warning disable CS1591 - -using System.Buffers.Binary; -using System.IO; - -namespace DvdLib -{ - public class BigEndianBinaryReader : BinaryReader - { - public BigEndianBinaryReader(Stream input) - : base(input) - { - } - - public override ushort ReadUInt16() - { - return BinaryPrimitives.ReadUInt16BigEndian(base.ReadBytes(2)); - } - - public override uint ReadUInt32() - { - return BinaryPrimitives.ReadUInt32BigEndian(base.ReadBytes(4)); - } - } -} diff --git a/DvdLib/DvdLib.csproj b/DvdLib/DvdLib.csproj deleted file mode 100644 index 1053c0089f..0000000000 --- a/DvdLib/DvdLib.csproj +++ /dev/null @@ -1,20 +0,0 @@ -<Project Sdk="Microsoft.NET.Sdk"> - - <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> - <PropertyGroup> - <ProjectGuid>{713F42B5-878E-499D-A878-E4C652B1D5E8}</ProjectGuid> - </PropertyGroup> - - <ItemGroup> - <Compile Include="..\SharedVersion.cs" /> - </ItemGroup> - - <PropertyGroup> - <TargetFramework>net7.0</TargetFramework> - <GenerateAssemblyInfo>false</GenerateAssemblyInfo> - <GenerateDocumentationFile>true</GenerateDocumentationFile> - <AnalysisMode>AllDisabledByDefault</AnalysisMode> - <Nullable>disable</Nullable> - </PropertyGroup> - -</Project> diff --git a/DvdLib/Ifo/Cell.cs b/DvdLib/Ifo/Cell.cs deleted file mode 100644 index ea0b50e430..0000000000 --- a/DvdLib/Ifo/Cell.cs +++ /dev/null @@ -1,23 +0,0 @@ -#pragma warning disable CS1591 - -using System.IO; - -namespace DvdLib.Ifo -{ - public class Cell - { - public CellPlaybackInfo PlaybackInfo { get; private set; } - - public CellPositionInfo PositionInfo { get; private set; } - - internal void ParsePlayback(BinaryReader br) - { - PlaybackInfo = new CellPlaybackInfo(br); - } - - internal void ParsePosition(BinaryReader br) - { - PositionInfo = new CellPositionInfo(br); - } - } -} diff --git a/DvdLib/Ifo/CellPlaybackInfo.cs b/DvdLib/Ifo/CellPlaybackInfo.cs deleted file mode 100644 index 6e33a0ec5a..0000000000 --- a/DvdLib/Ifo/CellPlaybackInfo.cs +++ /dev/null @@ -1,52 +0,0 @@ -#pragma warning disable CS1591 - -using System.IO; - -namespace DvdLib.Ifo -{ - public enum BlockMode - { - NotInBlock = 0, - FirstCell = 1, - InBlock = 2, - LastCell = 3, - } - - public enum BlockType - { - Normal = 0, - Angle = 1, - } - - public enum PlaybackMode - { - Normal = 0, - StillAfterEachVOBU = 1, - } - - public class CellPlaybackInfo - { - public readonly BlockMode Mode; - public readonly BlockType Type; - public readonly bool SeamlessPlay; - public readonly bool Interleaved; - public readonly bool STCDiscontinuity; - public readonly bool SeamlessAngle; - public readonly PlaybackMode PlaybackMode; - public readonly bool Restricted; - public readonly byte StillTime; - public readonly byte CommandNumber; - public readonly DvdTime PlaybackTime; - public readonly uint FirstSector; - public readonly uint FirstILVUEndSector; - public readonly uint LastVOBUStartSector; - public readonly uint LastSector; - - internal CellPlaybackInfo(BinaryReader br) - { - br.BaseStream.Seek(0x4, SeekOrigin.Current); - PlaybackTime = new DvdTime(br.ReadBytes(4)); - br.BaseStream.Seek(0x10, SeekOrigin.Current); - } - } -} diff --git a/DvdLib/Ifo/CellPositionInfo.cs b/DvdLib/Ifo/CellPositionInfo.cs deleted file mode 100644 index 216aa0f77a..0000000000 --- a/DvdLib/Ifo/CellPositionInfo.cs +++ /dev/null @@ -1,19 +0,0 @@ -#pragma warning disable CS1591 - -using System.IO; - -namespace DvdLib.Ifo -{ - public class CellPositionInfo - { - public readonly ushort VOBId; - public readonly byte CellId; - - internal CellPositionInfo(BinaryReader br) - { - VOBId = br.ReadUInt16(); - br.ReadByte(); - CellId = br.ReadByte(); - } - } -} diff --git a/DvdLib/Ifo/Chapter.cs b/DvdLib/Ifo/Chapter.cs deleted file mode 100644 index e786cb5536..0000000000 --- a/DvdLib/Ifo/Chapter.cs +++ /dev/null @@ -1,20 +0,0 @@ -#pragma warning disable CS1591 - -namespace DvdLib.Ifo -{ - public class Chapter - { - public ushort ProgramChainNumber { get; private set; } - - public ushort ProgramNumber { get; private set; } - - public uint ChapterNumber { get; private set; } - - public Chapter(ushort pgcNum, ushort programNum, uint chapterNum) - { - ProgramChainNumber = pgcNum; - ProgramNumber = programNum; - ChapterNumber = chapterNum; - } - } -} diff --git a/DvdLib/Ifo/Dvd.cs b/DvdLib/Ifo/Dvd.cs deleted file mode 100644 index 7f8ece47dc..0000000000 --- a/DvdLib/Ifo/Dvd.cs +++ /dev/null @@ -1,167 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; - -namespace DvdLib.Ifo -{ - public class Dvd - { - private readonly ushort _titleSetCount; - public readonly List<Title> Titles; - - private ushort _titleCount; - public readonly Dictionary<ushort, string> VTSPaths = new Dictionary<ushort, string>(); - public Dvd(string path) - { - Titles = new List<Title>(); - var allFiles = new DirectoryInfo(path).GetFiles(path, SearchOption.AllDirectories); - - var vmgPath = allFiles.FirstOrDefault(i => string.Equals(i.Name, "VIDEO_TS.IFO", StringComparison.OrdinalIgnoreCase)) ?? - allFiles.FirstOrDefault(i => string.Equals(i.Name, "VIDEO_TS.BUP", StringComparison.OrdinalIgnoreCase)); - - if (vmgPath == null) - { - foreach (var ifo in allFiles) - { - if (!string.Equals(ifo.Extension, ".ifo", StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - var nums = ifo.Name.Split('_', StringSplitOptions.RemoveEmptyEntries); - if (nums.Length >= 2 && ushort.TryParse(nums[1], out var ifoNumber)) - { - ReadVTS(ifoNumber, ifo.FullName); - } - } - } - else - { - using (var vmgFs = new FileStream(vmgPath.FullName, FileMode.Open, FileAccess.Read, FileShare.Read)) - { - using (var vmgRead = new BigEndianBinaryReader(vmgFs)) - { - vmgFs.Seek(0x3E, SeekOrigin.Begin); - _titleSetCount = vmgRead.ReadUInt16(); - - // read address of TT_SRPT - vmgFs.Seek(0xC4, SeekOrigin.Begin); - uint ttSectorPtr = vmgRead.ReadUInt32(); - vmgFs.Seek(ttSectorPtr * 2048, SeekOrigin.Begin); - ReadTT_SRPT(vmgRead); - } - } - - for (ushort titleSetNum = 1; titleSetNum <= _titleSetCount; titleSetNum++) - { - ReadVTS(titleSetNum, allFiles); - } - } - } - - private void ReadTT_SRPT(BinaryReader read) - { - _titleCount = read.ReadUInt16(); - read.BaseStream.Seek(6, SeekOrigin.Current); - for (uint titleNum = 1; titleNum <= _titleCount; titleNum++) - { - var t = new Title(titleNum); - t.ParseTT_SRPT(read); - Titles.Add(t); - } - } - - private void ReadVTS(ushort vtsNum, IReadOnlyList<FileInfo> allFiles) - { - var filename = string.Format(CultureInfo.InvariantCulture, "VTS_{0:00}_0.IFO", vtsNum); - - var vtsPath = allFiles.FirstOrDefault(i => string.Equals(i.Name, filename, StringComparison.OrdinalIgnoreCase)) ?? - allFiles.FirstOrDefault(i => string.Equals(i.Name, Path.ChangeExtension(filename, ".bup"), StringComparison.OrdinalIgnoreCase)); - - if (vtsPath == null) - { - throw new FileNotFoundException("Unable to find VTS IFO file"); - } - - ReadVTS(vtsNum, vtsPath.FullName); - } - - private void ReadVTS(ushort vtsNum, string vtsPath) - { - VTSPaths[vtsNum] = vtsPath; - - using (var vtsFs = new FileStream(vtsPath, FileMode.Open, FileAccess.Read, FileShare.Read)) - { - using (var vtsRead = new BigEndianBinaryReader(vtsFs)) - { - // Read VTS_PTT_SRPT - vtsFs.Seek(0xC8, SeekOrigin.Begin); - uint vtsPttSrptSecPtr = vtsRead.ReadUInt32(); - uint baseAddr = (vtsPttSrptSecPtr * 2048); - vtsFs.Seek(baseAddr, SeekOrigin.Begin); - - ushort numTitles = vtsRead.ReadUInt16(); - vtsRead.ReadUInt16(); - uint endaddr = vtsRead.ReadUInt32(); - uint[] offsets = new uint[numTitles]; - for (ushort titleNum = 0; titleNum < numTitles; titleNum++) - { - offsets[titleNum] = vtsRead.ReadUInt32(); - } - - for (uint titleNum = 0; titleNum < numTitles; titleNum++) - { - uint chapNum = 1; - vtsFs.Seek(baseAddr + offsets[titleNum], SeekOrigin.Begin); - var t = Titles.FirstOrDefault(vtst => vtst.IsVTSTitle(vtsNum, titleNum + 1)); - if (t == null) - { - continue; - } - - do - { - t.Chapters.Add(new Chapter(vtsRead.ReadUInt16(), vtsRead.ReadUInt16(), chapNum)); - if (titleNum + 1 < numTitles && vtsFs.Position == (baseAddr + offsets[titleNum + 1])) - { - break; - } - - chapNum++; - } - while (vtsFs.Position < (baseAddr + endaddr)); - } - - // Read VTS_PGCI - vtsFs.Seek(0xCC, SeekOrigin.Begin); - uint vtsPgciSecPtr = vtsRead.ReadUInt32(); - vtsFs.Seek(vtsPgciSecPtr * 2048, SeekOrigin.Begin); - - long startByte = vtsFs.Position; - - ushort numPgcs = vtsRead.ReadUInt16(); - vtsFs.Seek(6, SeekOrigin.Current); - for (ushort pgcNum = 1; pgcNum <= numPgcs; pgcNum++) - { - byte pgcCat = vtsRead.ReadByte(); - bool entryPgc = (pgcCat & 0x80) != 0; - uint titleNum = (uint)(pgcCat & 0x7F); - - vtsFs.Seek(3, SeekOrigin.Current); - uint vtsPgcOffset = vtsRead.ReadUInt32(); - - var t = Titles.FirstOrDefault(vtst => vtst.IsVTSTitle(vtsNum, titleNum)); - if (t != null) - { - t.AddPgc(vtsRead, startByte + vtsPgcOffset, entryPgc, pgcNum); - } - } - } - } - } - } -} diff --git a/DvdLib/Ifo/DvdTime.cs b/DvdLib/Ifo/DvdTime.cs deleted file mode 100644 index d231406106..0000000000 --- a/DvdLib/Ifo/DvdTime.cs +++ /dev/null @@ -1,39 +0,0 @@ -#pragma warning disable CS1591 - -using System; - -namespace DvdLib.Ifo -{ - public class DvdTime - { - public readonly byte Hour, Minute, Second, Frames, FrameRate; - - public DvdTime(byte[] data) - { - Hour = GetBCDValue(data[0]); - Minute = GetBCDValue(data[1]); - Second = GetBCDValue(data[2]); - Frames = GetBCDValue((byte)(data[3] & 0x3F)); - - if ((data[3] & 0x80) != 0) - { - FrameRate = 30; - } - else if ((data[3] & 0x40) != 0) - { - FrameRate = 25; - } - } - - private static byte GetBCDValue(byte data) - { - return (byte)((((data & 0xF0) >> 4) * 10) + (data & 0x0F)); - } - - public static explicit operator TimeSpan(DvdTime time) - { - int ms = (int)(((1.0 / (double)time.FrameRate) * time.Frames) * 1000.0); - return new TimeSpan(0, time.Hour, time.Minute, time.Second, ms); - } - } -} diff --git a/DvdLib/Ifo/Program.cs b/DvdLib/Ifo/Program.cs deleted file mode 100644 index 3d94fa7dc1..0000000000 --- a/DvdLib/Ifo/Program.cs +++ /dev/null @@ -1,16 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; - -namespace DvdLib.Ifo -{ - public class Program - { - public IReadOnlyList<Cell> Cells { get; } - - public Program(List<Cell> cells) - { - Cells = cells; - } - } -} diff --git a/DvdLib/Ifo/ProgramChain.cs b/DvdLib/Ifo/ProgramChain.cs deleted file mode 100644 index 83c0051b90..0000000000 --- a/DvdLib/Ifo/ProgramChain.cs +++ /dev/null @@ -1,121 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace DvdLib.Ifo -{ - public enum ProgramPlaybackMode - { - Sequential, - Random, - Shuffle - } - - public class ProgramChain - { - private byte _programCount; - public readonly List<Program> Programs; - - private byte _cellCount; - public readonly List<Cell> Cells; - - public DvdTime PlaybackTime { get; private set; } - - public UserOperation ProhibitedUserOperations { get; private set; } - - public byte[] AudioStreamControl { get; private set; } // 8*2 entries - public byte[] SubpictureStreamControl { get; private set; } // 32*4 entries - - private ushort _nextProgramNumber; - - private ushort _prevProgramNumber; - - private ushort _goupProgramNumber; - - public ProgramPlaybackMode PlaybackMode { get; private set; } - - public uint ProgramCount { get; private set; } - - public byte StillTime { get; private set; } - - public byte[] Palette { get; private set; } // 16*4 entries - - private ushort _commandTableOffset; - - private ushort _programMapOffset; - private ushort _cellPlaybackOffset; - private ushort _cellPositionOffset; - - public readonly uint VideoTitleSetIndex; - - internal ProgramChain(uint vtsPgcNum) - { - VideoTitleSetIndex = vtsPgcNum; - Cells = new List<Cell>(); - Programs = new List<Program>(); - } - - internal void ParseHeader(BinaryReader br) - { - long startPos = br.BaseStream.Position; - - br.ReadUInt16(); - _programCount = br.ReadByte(); - _cellCount = br.ReadByte(); - PlaybackTime = new DvdTime(br.ReadBytes(4)); - ProhibitedUserOperations = (UserOperation)br.ReadUInt32(); - AudioStreamControl = br.ReadBytes(16); - SubpictureStreamControl = br.ReadBytes(128); - - _nextProgramNumber = br.ReadUInt16(); - _prevProgramNumber = br.ReadUInt16(); - _goupProgramNumber = br.ReadUInt16(); - - StillTime = br.ReadByte(); - byte pbMode = br.ReadByte(); - if (pbMode == 0) - { - PlaybackMode = ProgramPlaybackMode.Sequential; - } - else - { - PlaybackMode = ((pbMode & 0x80) == 0) ? ProgramPlaybackMode.Random : ProgramPlaybackMode.Shuffle; - } - - ProgramCount = (uint)(pbMode & 0x7F); - - Palette = br.ReadBytes(64); - _commandTableOffset = br.ReadUInt16(); - _programMapOffset = br.ReadUInt16(); - _cellPlaybackOffset = br.ReadUInt16(); - _cellPositionOffset = br.ReadUInt16(); - - // read position info - br.BaseStream.Seek(startPos + _cellPositionOffset, SeekOrigin.Begin); - for (int cellNum = 0; cellNum < _cellCount; cellNum++) - { - var c = new Cell(); - c.ParsePosition(br); - Cells.Add(c); - } - - br.BaseStream.Seek(startPos + _cellPlaybackOffset, SeekOrigin.Begin); - for (int cellNum = 0; cellNum < _cellCount; cellNum++) - { - Cells[cellNum].ParsePlayback(br); - } - - br.BaseStream.Seek(startPos + _programMapOffset, SeekOrigin.Begin); - var cellNumbers = new List<int>(); - for (int progNum = 0; progNum < _programCount; progNum++) cellNumbers.Add(br.ReadByte() - 1); - - for (int i = 0; i < cellNumbers.Count; i++) - { - int max = (i + 1 == cellNumbers.Count) ? _cellCount : cellNumbers[i + 1]; - Programs.Add(new Program(Cells.Where((c, idx) => idx >= cellNumbers[i] && idx < max).ToList())); - } - } - } -} diff --git a/DvdLib/Ifo/Title.cs b/DvdLib/Ifo/Title.cs deleted file mode 100644 index 29a0b95c72..0000000000 --- a/DvdLib/Ifo/Title.cs +++ /dev/null @@ -1,70 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; -using System.IO; - -namespace DvdLib.Ifo -{ - public class Title - { - public uint TitleNumber { get; private set; } - - public uint AngleCount { get; private set; } - - public ushort ChapterCount { get; private set; } - - public byte VideoTitleSetNumber { get; private set; } - - private ushort _parentalManagementMask; - private byte _titleNumberInVTS; - private uint _vtsStartSector; // relative to start of entire disk - - public ProgramChain EntryProgramChain { get; private set; } - - public readonly List<ProgramChain> ProgramChains; - - public readonly List<Chapter> Chapters; - - public Title(uint titleNum) - { - ProgramChains = new List<ProgramChain>(); - Chapters = new List<Chapter>(); - Chapters = new List<Chapter>(); - TitleNumber = titleNum; - } - - public bool IsVTSTitle(uint vtsNum, uint vtsTitleNum) - { - return (vtsNum == VideoTitleSetNumber && vtsTitleNum == _titleNumberInVTS); - } - - internal void ParseTT_SRPT(BinaryReader br) - { - byte titleType = br.ReadByte(); - // TODO parse Title Type - - AngleCount = br.ReadByte(); - ChapterCount = br.ReadUInt16(); - _parentalManagementMask = br.ReadUInt16(); - VideoTitleSetNumber = br.ReadByte(); - _titleNumberInVTS = br.ReadByte(); - _vtsStartSector = br.ReadUInt32(); - } - - internal void AddPgc(BinaryReader br, long startByte, bool entryPgc, uint pgcNum) - { - long curPos = br.BaseStream.Position; - br.BaseStream.Seek(startByte, SeekOrigin.Begin); - - var pgc = new ProgramChain(pgcNum); - pgc.ParseHeader(br); - ProgramChains.Add(pgc); - if (entryPgc) - { - EntryProgramChain = pgc; - } - - br.BaseStream.Seek(curPos, SeekOrigin.Begin); - } - } -} diff --git a/DvdLib/Ifo/UserOperation.cs b/DvdLib/Ifo/UserOperation.cs deleted file mode 100644 index 5d111ebc06..0000000000 --- a/DvdLib/Ifo/UserOperation.cs +++ /dev/null @@ -1,37 +0,0 @@ -#pragma warning disable CS1591 - -using System; - -namespace DvdLib.Ifo -{ - [Flags] - public enum UserOperation - { - None = 0, - TitleOrTimePlay = 1, - ChapterSearchOrPlay = 2, - TitlePlay = 4, - Stop = 8, - GoUp = 16, - TimeOrChapterSearch = 32, - PrevOrTopProgramSearch = 64, - NextProgramSearch = 128, - ForwardScan = 256, - BackwardScan = 512, - TitleMenuCall = 1024, - RootMenuCall = 2048, - SubpictureMenuCall = 4096, - AudioMenuCall = 8192, - AngleMenuCall = 16384, - ChapterMenuCall = 32768, - Resume = 65536, - ButtonSelectOrActive = 131072, - StillOff = 262144, - PauseOn = 524288, - AudioStreamChange = 1048576, - SubpictureStreamChange = 2097152, - AngleChange = 4194304, - KaraokeAudioPresentationModeChange = 8388608, - VideoPresentationModeChange = 16777216, - } -} diff --git a/DvdLib/Properties/AssemblyInfo.cs b/DvdLib/Properties/AssemblyInfo.cs deleted file mode 100644 index 6acd571d68..0000000000 --- a/DvdLib/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Reflection; -using System.Resources; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("DvdLib")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Jellyfin Project")] -[assembly: AssemblyProduct("Jellyfin Server")] -[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] -[assembly: NeutralResourcesLanguage("en")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] diff --git a/Jellyfin.sln b/Jellyfin.sln index ee772cbe40..cad23fc5ee 100644 --- a/Jellyfin.sln +++ b/Jellyfin.sln @@ -21,8 +21,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Drawing", "src\Jel EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Photos", "Emby.Photos\Emby.Photos.csproj", "{89AB4548-770D-41FD-A891-8DAFF44F452C}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DvdLib", "DvdLib\DvdLib.csproj", "{713F42B5-878E-499D-A878-E4C652B1D5E8}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Server.Implementations", "Emby.Server.Implementations\Emby.Server.Implementations.csproj", "{E383961B-9356-4D5D-8233-9A1079D03055}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RSSDP", "RSSDP\RSSDP.csproj", "{21002819-C39A-4D3E-BE83-2A276A77FB1F}" @@ -137,10 +135,6 @@ Global {89AB4548-770D-41FD-A891-8DAFF44F452C}.Debug|Any CPU.Build.0 = Debug|Any CPU {89AB4548-770D-41FD-A891-8DAFF44F452C}.Release|Any CPU.ActiveCfg = Release|Any CPU {89AB4548-770D-41FD-A891-8DAFF44F452C}.Release|Any CPU.Build.0 = Release|Any CPU - {713F42B5-878E-499D-A878-E4C652B1D5E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {713F42B5-878E-499D-A878-E4C652B1D5E8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {713F42B5-878E-499D-A878-E4C652B1D5E8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {713F42B5-878E-499D-A878-E4C652B1D5E8}.Release|Any CPU.Build.0 = Release|Any CPU {E383961B-9356-4D5D-8233-9A1079D03055}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E383961B-9356-4D5D-8233-9A1079D03055}.Debug|Any CPU.Build.0 = Debug|Any CPU {E383961B-9356-4D5D-8233-9A1079D03055}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index a0f97df40d..6a40833d7f 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -8,7 +8,6 @@ <ItemGroup> <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" /> <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" /> - <ProjectReference Include="..\DvdLib\DvdLib.csproj" /> </ItemGroup> <ItemGroup> diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index f2f6d8a124..0ec7b368b4 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -9,7 +9,6 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; -using DvdLib.Ifo; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Chapters; using MediaBrowser.Controller.Configuration; @@ -689,33 +688,5 @@ namespace MediaBrowser.Providers.MediaInfo return chapters; } - - private string[] FetchFromDvdLib(Video item) - { - var path = item.Path; - var dvd = new Dvd(path); - - var primaryTitle = dvd.Titles.OrderByDescending(GetRuntime).FirstOrDefault(); - - byte? titleNumber = null; - - if (primaryTitle is not null) - { - titleNumber = primaryTitle.VideoTitleSetNumber; - item.RunTimeTicks = GetRuntime(primaryTitle); - } - - return _mediaEncoder.GetPrimaryPlaylistVobFiles(item.Path, titleNumber) - .Select(Path.GetFileName) - .ToArray(); - } - - private long GetRuntime(Title title) - { - return title.ProgramChains - .Select(i => (TimeSpan)i.PlaybackTime) - .Select(i => i.Ticks) - .Sum(); - } } } From f2b7f664aa9b3ade38a2402faf95ba9b6989fc41 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Sat, 4 Feb 2023 20:16:45 +0100 Subject: [PATCH 070/858] Apply review suggestions --- Jellyfin.Api/Helpers/TranscodingJobHelper.cs | 2 +- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 8 +++++--- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index cb73ad7657..1ba7395508 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -325,7 +325,7 @@ public class TranscodingJobHelper : IDisposable await DeletePartialStreamFiles(job.Path!, job.Type, 0, 1500).ConfigureAwait(false); if (job.MediaSource?.VideoType == VideoType.Dvd || job.MediaSource?.VideoType == VideoType.BluRay) { - var path = Path.GetDirectoryName(job.Path) + "/" + job.MediaSource.Id + ".concat"; + var path = Path.Join(job.Path, "/" + job.MediaSource.Id + ".concat"); File.Delete(path); } } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 7c4892ea50..075e33cb82 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -943,10 +943,12 @@ namespace MediaBrowser.Controller.MediaEncoding if (state.MediaSource.VideoType == VideoType.Dvd || state.MediaSource.VideoType == VideoType.BluRay) { - var tmpConcatPath = options.TranscodingTempPath + "/" + state.MediaSource.Id + ".concat"; + var tmpConcatPath = Path.Join(options.TranscodingTempPath, "/" + state.MediaSource.Id + ".concat"); _mediaEncoder.GenerateConcatConfig(state.MediaSource, tmpConcatPath); - arg.Append(" -f concat -safe 0 "); - arg.Append(" -i " + tmpConcatPath + " "); + arg.Append(" -f concat -safe 0 ") + .Append(" -i ") + .Append(tmpConcatPath) + .Append(' '); } else { diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 0bf89ec477..347e1de50a 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -450,7 +450,7 @@ namespace MediaBrowser.MediaEncoding.Encoder prefix = "bluray"; } - return EncodingUtils.GetInputArgument(prefix, new List<string>() { inputFile }, mediaSource.Protocol); + return EncodingUtils.GetInputArgument(prefix, new[] { inputFile }, mediaSource.Protocol); } /// <inheritdoc /> @@ -458,7 +458,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { const string Prefix = "file"; - return EncodingUtils.GetInputArgument(Prefix, new List<string> { inputFile }, MediaProtocol.File); + return EncodingUtils.GetInputArgument(Prefix, new[] { inputFile }, MediaProtocol.File); } /// <summary> From 979964ef4bba2e4a1e2f67d805dd5b5d638c2850 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Sat, 4 Feb 2023 21:39:52 +0100 Subject: [PATCH 071/858] Force transcode/remux for DVDs and BDs --- MediaBrowser.Model/Dlna/StreamBuilder.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 62d67c0257..f6c930f5c8 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -635,6 +635,13 @@ namespace MediaBrowser.Model.Dlna var isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || !bitrateLimitExceeded); TranscodeReason transcodeReasons = 0; + // Force transcode or remux for BD/DVD folders + if (item.VideoType == VideoType.Dvd || item.VideoType == VideoType.BluRay) + { + isEligibleForDirectPlay = false; + isEligibleForDirectStream = false; + } + if (bitrateLimitExceeded) { transcodeReasons = TranscodeReason.ContainerBitrateExceedsLimit; From 2a4cc4d942828886eb0c10389e602d67155aeca4 Mon Sep 17 00:00:00 2001 From: Jean-Pierre Bachmann <info@jean-pierre-bachmann.dev> Date: Sun, 5 Feb 2023 21:08:08 +0200 Subject: [PATCH 072/858] Updated logging level and formatting --- .../Tasks/CleanupCollectionPathsTask.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs index e1be43b9b2..617fd687a3 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs @@ -4,7 +4,6 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Emby.Server.Implementations.Collections; using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; @@ -68,30 +67,30 @@ public class CleanupCollectionPathsTask : IScheduledTask var collectionsFolder = await _collectionManager.GetCollectionsFolder(false).ConfigureAwait(false); if (collectionsFolder is null) { - _logger.LogInformation("There is no collection folder to be found."); + _logger.LogDebug("There is no collection folder to be found"); return; } var collections = collectionsFolder.Children.OfType<BoxSet>().ToArray(); - _logger.LogTrace("Found {CollectionLength} Boxsets.", collections.Length); + _logger.LogTrace("Found {CollectionLength} Boxsets", collections.Length); for (var index = 0; index < collections.Length; index++) { var collection = collections[index]; - _logger.LogTrace("Check Boxset {CollectionName}.", collection.Name); + _logger.LogDebug("Check Boxset {CollectionName}", collection.Name); var itemsToRemove = new List<LinkedChild>(); foreach (var collectionLinkedChild in collection.LinkedChildren.ToArray()) { if (!File.Exists(collectionLinkedChild.Path)) { - _logger.LogInformation("Item in boxset {CollectionName} cannot be found at {ItemPath}.", collection.Name, collectionLinkedChild.Path); + _logger.LogInformation("Item in boxset {CollectionName} cannot be found at {ItemPath}", collection.Name, collectionLinkedChild.Path); itemsToRemove.Add(collectionLinkedChild); } } if (itemsToRemove.Any()) { - _logger.LogTrace("Update Boxset {CollectionName}.", collection.Name); + _logger.LogDebug("Update Boxset {CollectionName}", collection.Name); collection.LinkedChildren = collection.LinkedChildren.Except(itemsToRemove).ToArray(); await collection.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken) .ConfigureAwait(false); From 0ed4fd5759156dd12b0de749d5c452972f8d9fb8 Mon Sep 17 00:00:00 2001 From: Jean-Pierre Bachmann <info@jean-pierre-bachmann.dev> Date: Sun, 5 Feb 2023 21:38:50 +0200 Subject: [PATCH 073/858] Changed LogTrace to LogDebug --- .../ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs index 617fd687a3..83e07478d2 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs @@ -72,7 +72,7 @@ public class CleanupCollectionPathsTask : IScheduledTask } var collections = collectionsFolder.Children.OfType<BoxSet>().ToArray(); - _logger.LogTrace("Found {CollectionLength} Boxsets", collections.Length); + _logger.LogDebug("Found {CollectionLength} Boxsets", collections.Length); for (var index = 0; index < collections.Length; index++) { From 1cc7572445789c703bb9456224e3f768083c4f65 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Wed, 15 Feb 2023 11:31:47 +0100 Subject: [PATCH 074/858] Apply review suggestions --- Jellyfin.Networking/Manager/NetworkManager.cs | 384 +++++++++--------- MediaBrowser.Common/Net/IPData.cs | 14 +- 2 files changed, 212 insertions(+), 186 deletions(-) diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index f4bd4e7662..ca9e9274f3 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -263,6 +263,7 @@ namespace Jellyfin.Networking.Manager _logger.LogError(ex, "Error obtaining interfaces."); } + // If no interfaces are found, fallback to loopback interfaces. if (_interfaces.Count == 0) { _logger.LogWarning("No interface information available. Using loopback interface(s)."); @@ -278,8 +279,8 @@ namespace Jellyfin.Networking.Manager } } - _logger.LogDebug("Discovered {0} interfaces.", _interfaces.Count); - _logger.LogDebug("Interfaces addresses: {0}", _interfaces.OrderByDescending(s => s.AddressFamily == AddressFamily.InterNetwork).Select(s => s.Address.ToString())); + _logger.LogDebug("Discovered {NumberOfInterfaces} interfaces.", _interfaces.Count); + _logger.LogDebug("Interfaces addresses: {Addresses}", _interfaces.OrderByDescending(s => s.AddressFamily == AddressFamily.InterNetwork).Select(s => s.Address.ToString())); } } @@ -293,14 +294,21 @@ namespace Jellyfin.Networking.Manager _logger.LogDebug("Refreshing LAN information."); // Get configuration options - string[] subnets = config.LocalNetworkSubnets; + var subnets = config.LocalNetworkSubnets; - _ = NetworkExtensions.TryParseToSubnets(subnets, out _lanSubnets, false); - _ = NetworkExtensions.TryParseToSubnets(subnets, out _excludedSubnets, true); + if (!NetworkExtensions.TryParseToSubnets(subnets, out _lanSubnets, false)) + { + _lanSubnets.Clear(); + } + if (!NetworkExtensions.TryParseToSubnets(subnets, out _excludedSubnets, true)) + { + _excludedSubnets.Clear(); + } + + // If no LAN addresses are specified, all private subnets and Loopback are deemed to be the LAN if (_lanSubnets.Count == 0) { - // If no LAN addresses are specified, all private subnets and Loopback are deemed to be the LAN _logger.LogDebug("Using LAN interface addresses as user provided no LAN details."); if (IsIpv6Enabled) @@ -334,15 +342,15 @@ namespace Jellyfin.Networking.Manager { // Respect explicit bind addresses var localNetworkAddresses = config.LocalNetworkAddresses; - if (localNetworkAddresses.Length > 0 && !string.IsNullOrWhiteSpace(localNetworkAddresses.First())) + if (localNetworkAddresses.Length > 0 && !string.IsNullOrWhiteSpace(localNetworkAddresses[0])) { var bindAddresses = localNetworkAddresses.Select(p => NetworkExtensions.TryParseToSubnet(p, out var network) ? network.Prefix : (_interfaces.Where(x => x.Name.Equals(p, StringComparison.OrdinalIgnoreCase)) .Select(x => x.Address) .FirstOrDefault() ?? IPAddress.None)) - .ToList(); - bindAddresses.RemoveAll(x => x == IPAddress.None); + .Where(x => x != IPAddress.None) + .ToHashSet(); _interfaces = _interfaces.Where(x => bindAddresses.Contains(x.Address)).ToList(); if (bindAddresses.Contains(IPAddress.Loopback)) @@ -401,11 +409,15 @@ namespace Jellyfin.Networking.Manager if (remoteIPFilter.Any() && !string.IsNullOrWhiteSpace(remoteIPFilter.First())) { // Parse all IPs with netmask to a subnet - _ = NetworkExtensions.TryParseToSubnets(remoteIPFilter.Where(x => x.Contains('/', StringComparison.OrdinalIgnoreCase)).ToArray(), out _remoteAddressFilter, false); + var remoteFilteredSubnets = remoteIPFilter.Where(x => x.Contains('/', StringComparison.OrdinalIgnoreCase)).ToArray(); + if (!NetworkExtensions.TryParseToSubnets(remoteFilteredSubnets, out _remoteAddressFilter, false)) + { + _remoteAddressFilter.Clear(); + } // Parse everything else as an IP and construct subnet with a single IP - var ips = remoteIPFilter.Where(x => !x.Contains('/', StringComparison.OrdinalIgnoreCase)); - foreach (var ip in ips) + var remoteFilteredIPs = remoteIPFilter.Where(x => !x.Contains('/', StringComparison.OrdinalIgnoreCase)); + foreach (var ip in remoteFilteredIPs) { if (IPAddress.TryParse(ip, out var ipp)) { @@ -436,44 +448,43 @@ namespace Jellyfin.Networking.Manager if (parts.Length != 2) { _logger.LogError("Unable to parse bind override: {Entry}", entry); + return; + } + + var replacement = parts[1].Trim(); + var identifier = parts[0]; + if (string.Equals(identifier, "all", StringComparison.OrdinalIgnoreCase)) + { + _publishedServerUrls[new IPData(IPAddress.Broadcast, null)] = replacement; + } + else if (string.Equals(identifier, "external", StringComparison.OrdinalIgnoreCase)) + { + _publishedServerUrls[new IPData(IPAddress.Any, new IPNetwork(IPAddress.Any, 0))] = replacement; + _publishedServerUrls[new IPData(IPAddress.IPv6Any, new IPNetwork(IPAddress.IPv6Any, 0))] = replacement; + } + else if (string.Equals(identifier, "internal", StringComparison.OrdinalIgnoreCase)) + { + foreach (var lan in _lanSubnets) + { + var lanPrefix = lan.Prefix; + _publishedServerUrls[new IPData(lanPrefix, new IPNetwork(lanPrefix, lan.PrefixLength))] = replacement; + } + } + else if (NetworkExtensions.TryParseToSubnet(identifier, out var result) && result != null) + { + var data = new IPData(result.Prefix, result); + _publishedServerUrls[data] = replacement; + } + else if (TryParseInterface(identifier, out var ifaces)) + { + foreach (var iface in ifaces) + { + _publishedServerUrls[iface] = replacement; + } } else { - var replacement = parts[1].Trim(); - var identifier = parts[0]; - if (string.Equals(identifier, "all", StringComparison.OrdinalIgnoreCase)) - { - _publishedServerUrls[new IPData(IPAddress.Broadcast, null)] = replacement; - } - else if (string.Equals(identifier, "external", StringComparison.OrdinalIgnoreCase)) - { - _publishedServerUrls[new IPData(IPAddress.Any, new IPNetwork(IPAddress.Any, 0))] = replacement; - _publishedServerUrls[new IPData(IPAddress.IPv6Any, new IPNetwork(IPAddress.IPv6Any, 0))] = replacement; - } - else if (string.Equals(identifier, "internal", StringComparison.OrdinalIgnoreCase)) - { - foreach (var lan in _lanSubnets) - { - var lanPrefix = lan.Prefix; - _publishedServerUrls[new IPData(lanPrefix, new IPNetwork(lanPrefix, lan.PrefixLength))] = replacement; - } - } - else if (NetworkExtensions.TryParseToSubnet(identifier, out var result) && result != null) - { - var data = new IPData(result.Prefix, result); - _publishedServerUrls[data] = replacement; - } - else if (TryParseInterface(identifier, out var ifaces)) - { - foreach (var iface in ifaces) - { - _publishedServerUrls[iface] = replacement; - } - } - else - { - _logger.LogError("Unable to parse bind override: {Entry}", entry); - } + _logger.LogError("Unable to parse bind override: {Entry}", entry); } } } @@ -556,31 +567,28 @@ namespace Jellyfin.Networking.Manager public bool TryParseInterface(string intf, out List<IPData> result) { result = new List<IPData>(); - if (string.IsNullOrEmpty(intf)) + if (string.IsNullOrEmpty(intf) || _interfaces is null) { return false; } - if (_interfaces != null) + // Match all interfaces starting with names starting with token + var matchedInterfaces = _interfaces.Where(s => s.Name.Equals(intf, StringComparison.OrdinalIgnoreCase)).OrderBy(x => x.Index).ToList(); + if (matchedInterfaces.Count > 0) { - // Match all interfaces starting with names starting with token - var matchedInterfaces = _interfaces.Where(s => s.Name.Equals(intf, StringComparison.OrdinalIgnoreCase)).OrderBy(x => x.Index).ToList(); - if (matchedInterfaces.Any()) + _logger.LogInformation("Interface {Token} used in settings. Using its interface addresses.", intf); + + // Use interface IP instead of name + foreach (var iface in matchedInterfaces) { - _logger.LogInformation("Interface {Token} used in settings. Using its interface addresses.", intf); - - // Use interface IP instead of name - foreach (var iface in matchedInterfaces) + if ((IsIpv4Enabled && iface.Address.AddressFamily == AddressFamily.InterNetwork) + || (IsIpv6Enabled && iface.Address.AddressFamily == AddressFamily.InterNetworkV6)) { - if ((IsIpv4Enabled && iface.Address.AddressFamily == AddressFamily.InterNetwork) - || (IsIpv6Enabled && iface.Address.AddressFamily == AddressFamily.InterNetworkV6)) - { - result.Add(iface); - } + result.Add(iface); } - - return true; } + + return true; } return false; @@ -626,6 +634,11 @@ namespace Jellyfin.Networking.Manager /// <inheritdoc/> public IReadOnlyList<IPData> GetLoopbacks() { + if (!(IsIpv4Enabled && IsIpv4Enabled)) + { + return Array.Empty<IPData>(); + } + var loopbackNetworks = new List<IPData>(); if (IsIpv4Enabled) { @@ -643,62 +656,64 @@ namespace Jellyfin.Networking.Manager /// <inheritdoc/> public IReadOnlyList<IPData> GetAllBindInterfaces(bool individualInterfaces = false) { - if (_interfaces.Count == 0) + if (_interfaces.Count != 0) { - // No bind address and no exclusions, so listen on all interfaces. - var result = new List<IPData>(); + return _interfaces; + } - if (individualInterfaces) - { - foreach (var iface in _interfaces) - { - result.Add(iface); - } + // No bind address and no exclusions, so listen on all interfaces. + var result = new List<IPData>(); - return result; - } - - if (IsIpv4Enabled && IsIpv6Enabled) + if (individualInterfaces) + { + foreach (var iface in _interfaces) { - // Kestrel source code shows it uses Sockets.DualMode - so this also covers IPAddress.Any by default - result.Add(new IPData(IPAddress.IPv6Any, new IPNetwork(IPAddress.IPv6Any, 0))); - } - else if (IsIpv4Enabled) - { - result.Add(new IPData(IPAddress.Any, new IPNetwork(IPAddress.Any, 0))); - } - else if (IsIpv6Enabled) - { - // Cannot use IPv6Any as Kestrel will bind to IPv4 addresses too. - foreach (var iface in _interfaces) - { - if (iface.AddressFamily == AddressFamily.InterNetworkV6) - { - result.Add(iface); - } - } + result.Add(iface); } return result; } - return _interfaces; + if (IsIpv4Enabled && IsIpv6Enabled) + { + // Kestrel source code shows it uses Sockets.DualMode - so this also covers IPAddress.Any by default + result.Add(new IPData(IPAddress.IPv6Any, new IPNetwork(IPAddress.IPv6Any, 0))); + } + else if (IsIpv4Enabled) + { + result.Add(new IPData(IPAddress.Any, new IPNetwork(IPAddress.Any, 0))); + } + else if (IsIpv6Enabled) + { + // Cannot use IPv6Any as Kestrel will bind to IPv4 addresses too. + foreach (var iface in _interfaces) + { + if (iface.AddressFamily == AddressFamily.InterNetworkV6) + { + result.Add(iface); + } + } + } + + return result; } /// <inheritdoc/> public string GetBindInterface(string source, out int? port) { - _ = NetworkExtensions.TryParseHost(source, out var address, IsIpv4Enabled, IsIpv6Enabled); - var result = GetBindAddress(address.FirstOrDefault(), out port); + if (!NetworkExtensions.TryParseHost(source, out var addresses, IsIpv4Enabled, IsIpv6Enabled)) + { + addresses = Array.Empty<IPAddress>(); + } + + var result = GetBindAddress(addresses.FirstOrDefault(), out port); return result; } /// <inheritdoc/> public string GetBindInterface(HttpRequest source, out int? port) { - string result; - _ = NetworkExtensions.TryParseHost(source.Host.Host, out var addresses, IsIpv4Enabled, IsIpv6Enabled); - result = GetBindAddress(addresses.FirstOrDefault(), out port); + var result = GetBindInterface(source.Host.Host, out port); port ??= source.Host.Port; return result; @@ -749,36 +764,36 @@ namespace Jellyfin.Networking.Manager .ThenBy(x => x.Index) .ToList(); - if (availableInterfaces.Any()) + if (availableInterfaces.Count > 0) { - if (source != null) + if (source is null) { - foreach (var intf in availableInterfaces) - { - if (intf.Address.Equals(source)) - { - result = NetworkExtensions.FormatIpString(intf.Address); - _logger.LogDebug("{Source}: Found matching interface to use as bind address: {Result}", source, result); - return result; - } - } + result = NetworkExtensions.FormatIpString(availableInterfaces.First().Address); + _logger.LogDebug("{Source}: Using first internal interface as bind address: {Result}", source, result); + return result; + } - // Does the request originate in one of the interface subnets? - // (For systems with multiple internal network cards, and multiple subnets) - foreach (var intf in availableInterfaces) + foreach (var intf in availableInterfaces) + { + if (intf.Address.Equals(source)) { - if (intf.Subnet.Contains(source)) - { - result = NetworkExtensions.FormatIpString(intf.Address); - _logger.LogDebug("{Source}: Found internal interface with matching subnet, using it as bind address: {Result}", source, result); - return result; - } + result = NetworkExtensions.FormatIpString(intf.Address); + _logger.LogDebug("{Source}: Found matching interface to use as bind address: {Result}", source, result); + return result; } } - result = NetworkExtensions.FormatIpString(availableInterfaces.First().Address); - _logger.LogDebug("{Source}: Using first internal interface as bind address: {Result}", source, result); - return result; + // Does the request originate in one of the interface subnets? + // (For systems with multiple internal network cards, and multiple subnets) + foreach (var intf in availableInterfaces) + { + if (intf.Subnet.Contains(source)) + { + result = NetworkExtensions.FormatIpString(intf.Address); + _logger.LogDebug("{Source}: Found internal interface with matching subnet, using it as bind address: {Result}", source, result); + return result; + } + } } // There isn't any others, so we'll use the loopback. @@ -810,6 +825,11 @@ namespace Jellyfin.Networking.Manager foreach (var ept in addresses) { match |= IPAddress.IsLoopback(ept) || (_lanSubnets.Any(x => x.Contains(ept)) && !_excludedSubnets.Any(x => x.Contains(ept))); + + if (match) + { + break; + } } return match; @@ -824,15 +844,15 @@ namespace Jellyfin.Networking.Manager ArgumentNullException.ThrowIfNull(address); // See conversation at https://github.com/jellyfin/jellyfin/pull/3515. - if (TrustAllIpv6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6) + if ((TrustAllIpv6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6) + || address.Equals(IPAddress.Loopback) + || address.Equals(IPAddress.IPv6Loopback)) { return true; } // As private addresses can be redefined by Configuration.LocalNetworkAddresses - var match = CheckIfLanAndNotExcluded(address); - - return address.Equals(IPAddress.Loopback) || address.Equals(IPAddress.IPv6Loopback) || match; + return CheckIfLanAndNotExcluded(address); } private bool CheckIfLanAndNotExcluded(IPAddress address) @@ -865,20 +885,16 @@ namespace Jellyfin.Networking.Manager int? port = null; var validPublishedServerUrls = _publishedServerUrls.Where(x => x.Key.Address.Equals(IPAddress.Any) - || x.Key.Address.Equals(IPAddress.IPv6Any) - || x.Key.Subnet.Contains(source)) - .GroupBy(x => x.Key) - .Select(x => x.First()) - .OrderBy(x => x.Key.Address.Equals(IPAddress.Any) - || x.Key.Address.Equals(IPAddress.IPv6Any)) - .ToList(); + || x.Key.Address.Equals(IPAddress.IPv6Any) + || x.Key.Subnet.Contains(source)) + .DistinctBy(x => x.Key) + .OrderBy(x => x.Key.Address.Equals(IPAddress.Any) + || x.Key.Address.Equals(IPAddress.IPv6Any)) + .ToList(); // Check for user override. foreach (var data in validPublishedServerUrls) { - // Get address interface. - var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Key.Subnet.Contains(x.Address)); - if (isInExternalSubnet && (data.Key.Address.Equals(IPAddress.Any) || data.Key.Address.Equals(IPAddress.IPv6Any))) { // External. @@ -886,6 +902,9 @@ namespace Jellyfin.Networking.Manager break; } + // Get address interface. + var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Key.Subnet.Contains(x.Address)); + if (intf?.Address != null) { // Match IP address. @@ -896,7 +915,7 @@ namespace Jellyfin.Networking.Manager if (string.IsNullOrEmpty(bindPreference)) { - _logger.LogDebug("{Source}: No matching bind address override found.", source); + _logger.LogDebug("{Source}: No matching bind address override found", source); return false; } @@ -941,40 +960,22 @@ namespace Jellyfin.Networking.Manager count = 0; } - if (count > 0) + if (count == 0) + { + return false; + } + + IPAddress? bindAddress = null; + if (isInExternalSubnet) { - IPAddress? bindAddress = null; var externalInterfaces = _interfaces.Where(x => !IsInLocalNetwork(x.Address)) .OrderBy(x => x.Index) .ToList(); - - if (isInExternalSubnet) + if (externalInterfaces.Count > 0) { - if (externalInterfaces.Any()) - { - // Check to see if any of the external bind interfaces are in the same subnet as the source. - // If none exists, this will select the first external interface if there is one. - bindAddress = externalInterfaces - .OrderByDescending(x => x.Subnet.Contains(source)) - .ThenBy(x => x.Index) - .Select(x => x.Address) - .FirstOrDefault(); - - if (bindAddress != null) - { - result = NetworkExtensions.FormatIpString(bindAddress); - _logger.LogDebug("{Source}: External request received, matching external bind address found: {Result}", source, result); - return true; - } - } - - _logger.LogWarning("{Source}: External request received, no matching external bind address found, trying internal addresses.", source); - } - else - { - // Check to see if any of the internal bind interfaces are in the same subnet as the source. - // If none exists, this will select the first internal interface if there is one. - bindAddress = _interfaces.Where(x => IsInLocalNetwork(x.Address)) + // Check to see if any of the external bind interfaces are in the same subnet as the source. + // If none exists, this will select the first external interface if there is one. + bindAddress = externalInterfaces .OrderByDescending(x => x.Subnet.Contains(source)) .ThenBy(x => x.Index) .Select(x => x.Address) @@ -983,10 +984,29 @@ namespace Jellyfin.Networking.Manager if (bindAddress != null) { result = NetworkExtensions.FormatIpString(bindAddress); - _logger.LogDebug("{Source}: Internal request received, matching internal bind address found: {Result}", source, result); + _logger.LogDebug("{Source}: External request received, matching external bind address found: {Result}", source, result); return true; } } + + _logger.LogWarning("{Source}: External request received, no matching external bind address found, trying internal addresses.", source); + } + else + { + // Check to see if any of the internal bind interfaces are in the same subnet as the source. + // If none exists, this will select the first internal interface if there is one. + bindAddress = _interfaces.Where(x => IsInLocalNetwork(x.Address)) + .OrderByDescending(x => x.Subnet.Contains(source)) + .ThenBy(x => x.Index) + .Select(x => x.Address) + .FirstOrDefault(); + + if (bindAddress != null) + { + result = NetworkExtensions.FormatIpString(bindAddress); + _logger.LogDebug("{Source}: Internal request received, matching internal bind address found: {Result}", source, result); + return true; + } } return false; @@ -1000,16 +1020,21 @@ namespace Jellyfin.Networking.Manager /// <returns><c>true</c> if a match is found, <c>false</c> otherwise.</returns> private bool MatchesExternalInterface(IPAddress source, out string result) { - result = string.Empty; - // Get the first WAN interface address that isn't a loopback. - var extResult = _interfaces.Where(p => !IsInLocalNetwork(p.Address)).OrderBy(x => x.Index); + // Get the first external interface address that isn't a loopback. + var extResult = _interfaces.Where(p => !IsInLocalNetwork(p.Address)).OrderBy(x => x.Index).ToArray(); + + // No external interface found + if (extResult.Length == 0) + { + result = string.Empty; + _logger.LogWarning("{Source}: External request received, but no external interface found. Need to route through internal network.", source); + return false; + } - IPAddress? hasResult = null; // Does the request originate in one of the interface subnets? - // (For systems with multiple internal network cards, and multiple subnets) + // (For systems with multiple network cards and/or multiple subnets) foreach (var intf in extResult) { - hasResult ??= intf.Address; if (!IsInLocalNetwork(intf.Address) && intf.Subnet.Contains(source)) { result = NetworkExtensions.FormatIpString(intf.Address); @@ -1018,15 +1043,10 @@ namespace Jellyfin.Networking.Manager } } - if (hasResult != null) - { - result = NetworkExtensions.FormatIpString(hasResult); - _logger.LogDebug("{Source}: Using first external interface as bind address: {Result}", source, result); - return true; - } - - _logger.LogWarning("{Source}: External request received, but no external interface found. Need to route through internal network.", source); - return false; + // Fallback to first external interface. + result = NetworkExtensions.FormatIpString(extResult.First().Address); + _logger.LogDebug("{Source}: Using first external interface as bind address: {Result}", source, result); + return true; } } } diff --git a/MediaBrowser.Common/Net/IPData.cs b/MediaBrowser.Common/Net/IPData.cs index 384efe8f68..05842632c8 100644 --- a/MediaBrowser.Common/Net/IPData.cs +++ b/MediaBrowser.Common/Net/IPData.cs @@ -59,10 +59,16 @@ namespace MediaBrowser.Common.Net { get { - return Address.Equals(IPAddress.None) - ? (Subnet.Prefix.AddressFamily.Equals(IPAddress.None) - ? AddressFamily.Unspecified : Subnet.Prefix.AddressFamily) - : Address.AddressFamily; + if (Address.Equals(IPAddress.None)) + { + return Subnet.Prefix.AddressFamily.Equals(IPAddress.None) + ? AddressFamily.Unspecified + : Subnet.Prefix.AddressFamily; + } + else + { + return Address.AddressFamily; + } } } } From 4eba16c6726564b159e395e188ec89f69d990e52 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Wed, 15 Feb 2023 22:34:44 +0100 Subject: [PATCH 075/858] Apply review suggestions --- .../EntryPoints/UdpServerEntryPoint.cs | 5 +- .../Configuration/NetworkConfiguration.cs | 4 +- .../NetworkConfigurationExtensions.cs | 2 +- Jellyfin.Networking/Manager/NetworkManager.cs | 101 ++++++++++-------- .../ApiServiceCollectionExtensions.cs | 2 +- MediaBrowser.Common/Net/INetworkManager.cs | 6 +- MediaBrowser.Common/Net/NetworkExtensions.cs | 67 +++++------- .../NetworkManagerTests.cs | 8 +- .../NetworkParseTests.cs | 32 +++--- .../ParseNetworkTests.cs | 8 +- 10 files changed, 117 insertions(+), 118 deletions(-) diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs index c2ffaf1bdd..b5a33a735d 100644 --- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs +++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs @@ -102,7 +102,10 @@ namespace Emby.Server.Implementations.EntryPoints _udpServers.Add(new UdpServer(_logger, _appHost, _config, System.Net.IPAddress.Any, PortNumber)); } - _udpServers.ForEach(u => u.Start(_cancellationTokenSource.Token)); + foreach (var server in _udpServers) + { + server.Start(_cancellationTokenSource.Token); + } } catch (SocketException ex) { diff --git a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs index f904198510..f31d2bce2d 100644 --- a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs +++ b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs @@ -115,12 +115,12 @@ namespace Jellyfin.Networking.Configuration /// <summary> /// Gets or sets a value indicating whether IPv6 is enabled or not. /// </summary> - public bool EnableIPV4 { get; set; } = true; + public bool EnableIPv4 { get; set; } = true; /// <summary> /// Gets or sets a value indicating whether IPv6 is enabled or not. /// </summary> - public bool EnableIPV6 { get; set; } + public bool EnableIPv6 { get; set; } /// <summary> /// Gets or sets a value indicating whether access outside of the LAN is permitted. diff --git a/Jellyfin.Networking/Configuration/NetworkConfigurationExtensions.cs b/Jellyfin.Networking/Configuration/NetworkConfigurationExtensions.cs index 8cbe398b07..3ba6bb8fcb 100644 --- a/Jellyfin.Networking/Configuration/NetworkConfigurationExtensions.cs +++ b/Jellyfin.Networking/Configuration/NetworkConfigurationExtensions.cs @@ -14,7 +14,7 @@ namespace Jellyfin.Networking.Configuration /// <returns>The <see cref="NetworkConfiguration"/>.</returns> public static NetworkConfiguration GetNetworkConfiguration(this IConfigurationManager config) { - return config.GetConfiguration<NetworkConfiguration>("network"); + return config.GetConfiguration<NetworkConfiguration>(NetworkConfigurationStore.StoreKey); } } } diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index ca9e9274f3..5f82950fce 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -111,12 +111,12 @@ namespace Jellyfin.Networking.Manager /// <summary> /// Gets a value indicating whether IP4 is enabled. /// </summary> - public bool IsIpv4Enabled => _configurationManager.GetNetworkConfiguration().EnableIPV4; + public bool IsIPv4Enabled => _configurationManager.GetNetworkConfiguration().EnableIPv4; /// <summary> /// Gets a value indicating whether IP6 is enabled. /// </summary> - public bool IsIpv6Enabled => _configurationManager.GetNetworkConfiguration().EnableIPV6; + public bool IsIPv6Enabled => _configurationManager.GetNetworkConfiguration().EnableIPv6; /// <summary> /// Gets a value indicating whether is all IPv6 interfaces are trusted as internal. @@ -229,7 +229,7 @@ namespace Jellyfin.Networking.Manager // Populate interface list foreach (var info in ipProperties.UnicastAddresses) { - if (IsIpv4Enabled && info.Address.AddressFamily == AddressFamily.InterNetwork) + if (IsIPv4Enabled && info.Address.AddressFamily == AddressFamily.InterNetwork) { var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name); interfaceObject.Index = ipProperties.GetIPv4Properties().Index; @@ -237,7 +237,7 @@ namespace Jellyfin.Networking.Manager _interfaces.Add(interfaceObject); } - else if (IsIpv6Enabled && info.Address.AddressFamily == AddressFamily.InterNetworkV6) + else if (IsIPv6Enabled && info.Address.AddressFamily == AddressFamily.InterNetworkV6) { var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name); interfaceObject.Index = ipProperties.GetIPv6Properties().Index; @@ -268,12 +268,12 @@ namespace Jellyfin.Networking.Manager { _logger.LogWarning("No interface information available. Using loopback interface(s)."); - if (IsIpv4Enabled && !IsIpv6Enabled) + if (IsIPv4Enabled && !IsIPv6Enabled) { _interfaces.Add(new IPData(IPAddress.Loopback, new IPNetwork(IPAddress.Loopback, 8), "lo")); } - if (!IsIpv4Enabled && IsIpv6Enabled) + if (!IsIPv4Enabled && IsIPv6Enabled) { _interfaces.Add(new IPData(IPAddress.IPv6Loopback, new IPNetwork(IPAddress.IPv6Loopback, 128), "lo")); } @@ -311,14 +311,14 @@ namespace Jellyfin.Networking.Manager { _logger.LogDebug("Using LAN interface addresses as user provided no LAN details."); - if (IsIpv6Enabled) + if (IsIPv6Enabled) { _lanSubnets.Add(new IPNetwork(IPAddress.IPv6Loopback, 128)); // RFC 4291 (Loopback) _lanSubnets.Add(new IPNetwork(IPAddress.Parse("fe80::"), 10)); // RFC 4291 (Site local) _lanSubnets.Add(new IPNetwork(IPAddress.Parse("fc00::"), 7)); // RFC 4193 (Unique local) } - if (IsIpv4Enabled) + if (IsIPv4Enabled) { _lanSubnets.Add(new IPNetwork(IPAddress.Loopback, 8)); // RFC 5735 (Loopback) _lanSubnets.Add(new IPNetwork(IPAddress.Parse("10.0.0.0"), 8)); // RFC 1918 (private) @@ -382,13 +382,13 @@ namespace Jellyfin.Networking.Manager } // Remove all IPv4 interfaces if IPv4 is disabled - if (!IsIpv4Enabled) + if (!IsIPv4Enabled) { _interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetwork); } // Remove all IPv6 interfaces if IPv6 is disabled - if (!IsIpv6Enabled) + if (!IsIPv6Enabled) { _interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetworkV6); } @@ -470,7 +470,7 @@ namespace Jellyfin.Networking.Manager _publishedServerUrls[new IPData(lanPrefix, new IPNetwork(lanPrefix, lan.PrefixLength))] = replacement; } } - else if (NetworkExtensions.TryParseToSubnet(identifier, out var result) && result != null) + else if (NetworkExtensions.TryParseToSubnet(identifier, out var result) && result is not null) { var data = new IPData(result.Prefix, result); _publishedServerUrls[data] = replacement; @@ -492,7 +492,7 @@ namespace Jellyfin.Networking.Manager private void ConfigurationUpdated(object? sender, ConfigurationUpdateEventArgs evt) { - if (evt.Key.Equals("network", StringComparison.Ordinal)) + if (evt.Key.Equals(NetworkConfigurationStore.StoreKey, StringComparison.Ordinal)) { UpdateSettings((NetworkConfiguration)evt.NewConfiguration); } @@ -581,8 +581,8 @@ namespace Jellyfin.Networking.Manager // Use interface IP instead of name foreach (var iface in matchedInterfaces) { - if ((IsIpv4Enabled && iface.Address.AddressFamily == AddressFamily.InterNetwork) - || (IsIpv6Enabled && iface.Address.AddressFamily == AddressFamily.InterNetworkV6)) + if ((IsIPv4Enabled && iface.Address.AddressFamily == AddressFamily.InterNetwork) + || (IsIPv6Enabled && iface.Address.AddressFamily == AddressFamily.InterNetworkV6)) { result.Add(iface); } @@ -634,18 +634,18 @@ namespace Jellyfin.Networking.Manager /// <inheritdoc/> public IReadOnlyList<IPData> GetLoopbacks() { - if (!(IsIpv4Enabled && IsIpv4Enabled)) + if (!(IsIPv4Enabled && IsIPv4Enabled)) { return Array.Empty<IPData>(); } var loopbackNetworks = new List<IPData>(); - if (IsIpv4Enabled) + if (IsIPv4Enabled) { loopbackNetworks.Add(new IPData(IPAddress.Loopback, new IPNetwork(IPAddress.Loopback, 8), "lo")); } - if (IsIpv6Enabled) + if (IsIPv6Enabled) { loopbackNetworks.Add(new IPData(IPAddress.IPv6Loopback, new IPNetwork(IPAddress.IPv6Loopback, 128), "lo")); } @@ -674,16 +674,16 @@ namespace Jellyfin.Networking.Manager return result; } - if (IsIpv4Enabled && IsIpv6Enabled) + if (IsIPv4Enabled && IsIPv6Enabled) { // Kestrel source code shows it uses Sockets.DualMode - so this also covers IPAddress.Any by default result.Add(new IPData(IPAddress.IPv6Any, new IPNetwork(IPAddress.IPv6Any, 0))); } - else if (IsIpv4Enabled) + else if (IsIPv4Enabled) { result.Add(new IPData(IPAddress.Any, new IPNetwork(IPAddress.Any, 0))); } - else if (IsIpv6Enabled) + else if (IsIPv6Enabled) { // Cannot use IPv6Any as Kestrel will bind to IPv4 addresses too. foreach (var iface in _interfaces) @@ -701,7 +701,7 @@ namespace Jellyfin.Networking.Manager /// <inheritdoc/> public string GetBindInterface(string source, out int? port) { - if (!NetworkExtensions.TryParseHost(source, out var addresses, IsIpv4Enabled, IsIpv6Enabled)) + if (!NetworkExtensions.TryParseHost(source, out var addresses, IsIPv4Enabled, IsIPv6Enabled)) { addresses = Array.Empty<IPAddress>(); } @@ -726,14 +726,14 @@ namespace Jellyfin.Networking.Manager string result; - if (source != null) + if (source is not null) { - if (IsIpv4Enabled && !IsIpv6Enabled && source.AddressFamily == AddressFamily.InterNetworkV6) + if (IsIPv4Enabled && !IsIPv6Enabled && source.AddressFamily == AddressFamily.InterNetworkV6) { _logger.LogWarning("IPv6 is disabled in Jellyfin, but enabled in the OS. This may affect how the interface is selected."); } - if (!IsIpv4Enabled && IsIpv6Enabled && source.AddressFamily == AddressFamily.InterNetwork) + if (!IsIPv4Enabled && IsIPv6Enabled && source.AddressFamily == AddressFamily.InterNetwork) { _logger.LogWarning("IPv4 is disabled in Jellyfin, but enabled in the OS. This may affect how the interface is selected."); } @@ -759,6 +759,7 @@ namespace Jellyfin.Networking.Manager } // Get the first LAN interface address that's not excluded and not a loopback address. + // Get all available interfaces, prefer local interfaces var availableInterfaces = _interfaces.Where(x => !IPAddress.IsLoopback(x.Address)) .OrderByDescending(x => IsInLocalNetwork(x.Address)) .ThenBy(x => x.Index) @@ -766,6 +767,7 @@ namespace Jellyfin.Networking.Manager if (availableInterfaces.Count > 0) { + // If no source address is given, use the preferred (first) interface if (source is null) { result = NetworkExtensions.FormatIpString(availableInterfaces.First().Address); @@ -773,16 +775,6 @@ namespace Jellyfin.Networking.Manager return result; } - foreach (var intf in availableInterfaces) - { - if (intf.Address.Equals(source)) - { - result = NetworkExtensions.FormatIpString(intf.Address); - _logger.LogDebug("{Source}: Found matching interface to use as bind address: {Result}", source, result); - return result; - } - } - // Does the request originate in one of the interface subnets? // (For systems with multiple internal network cards, and multiple subnets) foreach (var intf in availableInterfaces) @@ -790,14 +782,19 @@ namespace Jellyfin.Networking.Manager if (intf.Subnet.Contains(source)) { result = NetworkExtensions.FormatIpString(intf.Address); - _logger.LogDebug("{Source}: Found internal interface with matching subnet, using it as bind address: {Result}", source, result); + _logger.LogDebug("{Source}: Found interface with matching subnet, using it as bind address: {Result}", source, result); return result; } } + + // Fallback to first available interface + result = NetworkExtensions.FormatIpString(availableInterfaces[0].Address); + _logger.LogDebug("{Source}: No matching interfaces found, using preferred interface as bind address: {Result}", source, result); + return result; } // There isn't any others, so we'll use the loopback. - result = IsIpv4Enabled && !IsIpv6Enabled ? "127.0.0.1" : "::1"; + result = IsIPv4Enabled && !IsIPv6Enabled ? "127.0.0.1" : "::1"; _logger.LogWarning("{Source}: Only loopback {Result} returned, using that as bind address.", source, result); return result; } @@ -819,12 +816,12 @@ namespace Jellyfin.Networking.Manager return IPAddress.IsLoopback(subnet.Prefix) || (_lanSubnets.Any(x => x.Contains(subnet.Prefix)) && !_excludedSubnets.Any(x => x.Contains(subnet.Prefix))); } - if (NetworkExtensions.TryParseHost(address, out var addresses, IsIpv4Enabled, IsIpv6Enabled)) + if (NetworkExtensions.TryParseHost(address, out var addresses, IsIPv4Enabled, IsIPv6Enabled)) { bool match = false; foreach (var ept in addresses) { - match |= IPAddress.IsLoopback(ept) || (_lanSubnets.Any(x => x.Contains(ept)) && !_excludedSubnets.Any(x => x.Contains(ept))); + match = match || IPAddress.IsLoopback(ept) || (_lanSubnets.Any(x => x.Contains(ept)) && !_excludedSubnets.Any(x => x.Contains(ept))); if (match) { @@ -860,15 +857,29 @@ namespace Jellyfin.Networking.Manager bool match = false; foreach (var lanSubnet in _lanSubnets) { - match |= lanSubnet.Contains(address); + match = lanSubnet.Contains(address); + + if (match) + { + break; + } + } + + if (!match) + { + return match; } foreach (var excludedSubnet in _excludedSubnets) { - match &= !excludedSubnet.Contains(address); + match = match && !excludedSubnet.Contains(address); + + if (!match) + { + break; + } } - NetworkExtensions.IsIPv6LinkLocal(address); return match; } @@ -905,7 +916,7 @@ namespace Jellyfin.Networking.Manager // Get address interface. var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Key.Subnet.Contains(x.Address)); - if (intf?.Address != null) + if (intf?.Address is not null) { // Match IP address. bindPreference = data.Value; @@ -930,7 +941,7 @@ namespace Jellyfin.Networking.Manager } } - if (port != null) + if (port is not null) { _logger.LogDebug("{Source}: Matching bind address override found: {Address}:{Port}", source, bindPreference, port); } @@ -981,7 +992,7 @@ namespace Jellyfin.Networking.Manager .Select(x => x.Address) .FirstOrDefault(); - if (bindAddress != null) + if (bindAddress is not null) { result = NetworkExtensions.FormatIpString(bindAddress); _logger.LogDebug("{Source}: External request received, matching external bind address found: {Result}", source, result); @@ -1001,7 +1012,7 @@ namespace Jellyfin.Networking.Manager .Select(x => x.Address) .FirstOrDefault(); - if (bindAddress != null) + if (bindAddress is not null) { result = NetworkExtensions.FormatIpString(bindAddress); _logger.LogDebug("{Source}: Internal request received, matching internal bind address found: {Result}", source, result); diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 5065fbdbb9..71f23b7889 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -367,7 +367,7 @@ namespace Jellyfin.Server.Extensions private static void AddIpAddress(NetworkConfiguration config, ForwardedHeadersOptions options, IPAddress addr, int prefixLength) { - if ((!config.EnableIPV4 && addr.AddressFamily == AddressFamily.InterNetwork) || (!config.EnableIPV6 && addr.AddressFamily == AddressFamily.InterNetworkV6)) + if ((!config.EnableIPv4 && addr.AddressFamily == AddressFamily.InterNetwork) || (!config.EnableIPv6 && addr.AddressFamily == AddressFamily.InterNetworkV6)) { return; } diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs index f0f16af78f..5721b19ca2 100644 --- a/MediaBrowser.Common/Net/INetworkManager.cs +++ b/MediaBrowser.Common/Net/INetworkManager.cs @@ -19,12 +19,12 @@ namespace MediaBrowser.Common.Net /// <summary> /// Gets a value indicating whether IPv4 is enabled. /// </summary> - bool IsIpv4Enabled { get; } + bool IsIPv4Enabled { get; } /// <summary> /// Gets a value indicating whether IPv6 is enabled. /// </summary> - bool IsIpv6Enabled { get; } + bool IsIPv6Enabled { get; } /// <summary> /// Calculates the list of interfaces to use for Kestrel. @@ -119,7 +119,7 @@ namespace MediaBrowser.Common.Net /// <param name="intf">Interface name.</param> /// <param name="result">Resulting object's IP addresses, if successful.</param> /// <returns>Success of the operation.</returns> - bool TryParseInterface(string intf, out List<IPData>? result); + bool TryParseInterface(string intf, out List<IPData> result); /// <summary> /// Returns all internal (LAN) bind interface addresses. diff --git a/MediaBrowser.Common/Net/NetworkExtensions.cs b/MediaBrowser.Common/Net/NetworkExtensions.cs index 97f0abbb52..7c36081e60 100644 --- a/MediaBrowser.Common/Net/NetworkExtensions.cs +++ b/MediaBrowser.Common/Net/NetworkExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Net; using System.Net.Sockets; using System.Text.RegularExpressions; @@ -170,36 +171,9 @@ namespace MediaBrowser.Common.Net for (int a = 0; a < values.Length; a++) { - string[] v = values[a].Trim().Split("/"); - - var address = IPAddress.None; - if (negated && v[0].StartsWith('!')) + if (TryParseToSubnet(values[a], out var innerResult, negated)) { - _ = IPAddress.TryParse(v[0][1..], out address); - } - else if (!negated) - { - _ = IPAddress.TryParse(v[0][0..], out address); - } - - if (address != IPAddress.None && address is not null) - { - if (v.Length > 1 && int.TryParse(v[1], out var netmask)) - { - result.Add(new IPNetwork(address, netmask)); - } - else if (v.Length > 1 && IPAddress.TryParse(v[1], out var netmaskAddress)) - { - result.Add(new IPNetwork(address, NetworkExtensions.MaskToCidr(netmaskAddress))); - } - else if (address.AddressFamily == AddressFamily.InterNetwork) - { - result.Add(new IPNetwork(address, 32)); - } - else if (address.AddressFamily == AddressFamily.InterNetworkV6) - { - result.Add(new IPNetwork(address, 128)); - } + result.Add(innerResult); } } @@ -228,27 +202,32 @@ namespace MediaBrowser.Common.Net return false; } - string[] v = value.Trim().Split("/"); + var splitString = value.Trim().Split("/"); + var ipBlock = splitString[0]; var address = IPAddress.None; - if (negated && v[0].StartsWith('!')) + if (negated && ipBlock.StartsWith('!') && IPAddress.TryParse(ipBlock[1..], out var tmpAddress)) { - _ = IPAddress.TryParse(v[0][1..], out address); + address = tmpAddress; } - else if (!negated) + else if (!negated && IPAddress.TryParse(ipBlock, out tmpAddress)) { - _ = IPAddress.TryParse(v[0][0..], out address); + address = tmpAddress; } if (address != IPAddress.None && address is not null) { - if (v.Length > 1 && int.TryParse(v[1], out var netmask)) + if (splitString.Length > 1) { - result = new IPNetwork(address, netmask); - } - else if (v.Length > 1 && IPAddress.TryParse(v[1], out var netmaskAddress)) - { - result = new IPNetwork(address, NetworkExtensions.MaskToCidr(netmaskAddress)); + var subnetBlock = splitString[1]; + if (int.TryParse(subnetBlock, out var netmask)) + { + result = new IPNetwork(address, netmask); + } + else if (IPAddress.TryParse(subnetBlock, out var netmaskAddress)) + { + result = new IPNetwork(address, NetworkExtensions.MaskToCidr(netmaskAddress)); + } } else if (address.AddressFamily == AddressFamily.InterNetwork) { @@ -353,7 +332,13 @@ namespace MediaBrowser.Common.Net /// <returns>The broadcast address.</returns> public static IPAddress GetBroadcastAddress(IPNetwork network) { - uint ipAddress = BitConverter.ToUInt32(network.Prefix.GetAddressBytes(), 0); + var addressBytes = network.Prefix.GetAddressBytes(); + if (BitConverter.IsLittleEndian) + { + addressBytes.Reverse(); + } + + uint ipAddress = BitConverter.ToUInt32(addressBytes, 0); uint ipMaskV4 = BitConverter.ToUInt32(CidrToMask(network.PrefixLength, AddressFamily.InterNetwork).GetAddressBytes(), 0); uint broadCastIpAddress = ipAddress | ~ipMaskV4; diff --git a/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs b/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs index 61f9132528..85226d4305 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs @@ -23,8 +23,8 @@ namespace Jellyfin.Networking.Tests var ip = IPAddress.Parse(value); var conf = new NetworkConfiguration() { - EnableIPV6 = true, - EnableIPV4 = true, + EnableIPv6 = true, + EnableIPv4 = true, LocalNetworkSubnets = network.Split(',') }; @@ -50,8 +50,8 @@ namespace Jellyfin.Networking.Tests var ip = IPAddress.Parse(value); var conf = new NetworkConfiguration() { - EnableIPV6 = true, - EnableIPV4 = true, + EnableIPv6 = true, + EnableIPv4 = true, LocalNetworkSubnets = network.Split(',') }; diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs index 241d2314bf..c493ce5ea8 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs @@ -47,8 +47,8 @@ namespace Jellyfin.Networking.Tests { var conf = new NetworkConfiguration() { - EnableIPV6 = true, - EnableIPV4 = true, + EnableIPv6 = true, + EnableIPv4 = true, LocalNetworkSubnets = lan?.Split(';') ?? throw new ArgumentNullException(nameof(lan)) }; @@ -107,7 +107,7 @@ namespace Jellyfin.Networking.Tests [InlineData("10.128.240.50/30", "10.128.240.51")] [InlineData("10.128.240.50/255.255.255.252", "10.128.240.51")] [InlineData("127.0.0.1/8", "127.0.0.1")] - public void IpV4SubnetMaskMatchesValidIpAddress(string netMask, string ipAddress) + public void IPv4SubnetMaskMatchesValidIPAddress(string netMask, string ipAddress) { var ipa = IPAddress.Parse(ipAddress); Assert.True(NetworkExtensions.TryParseToSubnet(netMask, out var subnet) && subnet.Contains(IPAddress.Parse(ipAddress))); @@ -127,7 +127,7 @@ namespace Jellyfin.Networking.Tests [InlineData("10.128.240.50/30", "10.128.239.50")] [InlineData("10.128.240.50/30", "10.127.240.51")] [InlineData("10.128.240.50/255.255.255.252", "10.127.240.51")] - public void IpV4SubnetMaskDoesNotMatchInvalidIpAddress(string netMask, string ipAddress) + public void IPv4SubnetMaskDoesNotMatchInvalidIPAddress(string netMask, string ipAddress) { var ipa = IPAddress.Parse(ipAddress); Assert.False(NetworkExtensions.TryParseToSubnet(netMask, out var subnet) && subnet.Contains(IPAddress.Parse(ipAddress))); @@ -144,7 +144,7 @@ namespace Jellyfin.Networking.Tests [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) + public void IPv6SubnetMaskMatchesValidIPAddress(string netMask, string ipAddress) { Assert.True(NetworkExtensions.TryParseToSubnet(netMask, out var subnet) && subnet.Contains(IPAddress.Parse(ipAddress))); } @@ -155,7 +155,7 @@ namespace Jellyfin.Networking.Tests [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) + public void IPv6SubnetMaskDoesNotMatchInvalidIPAddress(string netMask, string ipAddress) { Assert.False(NetworkExtensions.TryParseToSubnet(netMask, out var subnet) && subnet.Contains(IPAddress.Parse(ipAddress))); } @@ -194,8 +194,8 @@ namespace Jellyfin.Networking.Tests var conf = new NetworkConfiguration() { LocalNetworkAddresses = bindAddresses.Split(','), - EnableIPV6 = ipv6enabled, - EnableIPV4 = true + EnableIPv6 = ipv6enabled, + EnableIPv4 = true }; NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16|200.200.200.200/24,11,eth11"; @@ -256,8 +256,8 @@ namespace Jellyfin.Networking.Tests { LocalNetworkSubnets = lan.Split(','), LocalNetworkAddresses = bindAddresses.Split(','), - EnableIPV6 = ipv6enabled, - EnableIPV4 = true, + EnableIPv6 = ipv6enabled, + EnableIPv4 = true, PublishedServerUriBySubnet = new string[] { publishedServers } }; @@ -281,13 +281,13 @@ namespace Jellyfin.Networking.Tests [InlineData("185.10.10.10", "185.10.10.10", false)] [InlineData("", "100.100.100.100", false)] - public void HasRemoteAccess_GivenWhitelist_AllowsOnlyIpsInWhitelist(string addresses, string remoteIp, bool denied) + public void HasRemoteAccess_GivenWhitelist_AllowsOnlyIPsInWhitelist(string addresses, string remoteIp, bool denied) { // 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 conf = new NetworkConfiguration() { - EnableIPV4 = true, + EnableIPv4 = true, RemoteIPFilter = addresses.Split(','), IsRemoteIPFilterBlacklist = false }; @@ -301,13 +301,13 @@ namespace Jellyfin.Networking.Tests [InlineData("185.10.10.10", "185.10.10.10", true)] [InlineData("", "100.100.100.100", false)] - public void HasRemoteAccess_GivenBlacklist_BlacklistTheIps(string addresses, string remoteIp, bool denied) + public void HasRemoteAccess_GivenBlacklist_BlacklistTheIPs(string addresses, string remoteIp, bool denied) { // 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 conf = new NetworkConfiguration() { - EnableIPV4 = true, + EnableIPv4 = true, RemoteIPFilter = addresses.Split(','), IsRemoteIPFilterBlacklist = true }; @@ -326,7 +326,7 @@ namespace Jellyfin.Networking.Tests { var conf = new NetworkConfiguration { - EnableIPV4 = true, + EnableIPv4 = true, LocalNetworkSubnets = lan.Split(','), LocalNetworkAddresses = bind.Split(',') }; @@ -350,7 +350,7 @@ namespace Jellyfin.Networking.Tests { var conf = new NetworkConfiguration { - EnableIPV4 = true, + EnableIPv4 = true, LocalNetworkSubnets = lan.Split(','), LocalNetworkAddresses = bind.Split(',') }; diff --git a/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs index 12a9beb9e3..49516ccccd 100644 --- a/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs +++ b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs @@ -77,8 +77,8 @@ namespace Jellyfin.Server.Tests var settings = new NetworkConfiguration { - EnableIPV4 = ip4, - EnableIPV6 = ip6 + EnableIPv4 = ip4, + EnableIPv6 = ip6 }; ForwardedHeadersOptions options = new ForwardedHeadersOptions(); @@ -116,8 +116,8 @@ namespace Jellyfin.Server.Tests { var conf = new NetworkConfiguration() { - EnableIPV6 = true, - EnableIPV4 = true, + EnableIPv6 = true, + EnableIPv4 = true, }; return new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); From 58ed50c9d01c4e4e2c38d62025e1820e43b538d3 Mon Sep 17 00:00:00 2001 From: ipitio <21136719+ipitio@users.noreply.github.com> Date: Wed, 15 Feb 2023 21:58:49 -0500 Subject: [PATCH 076/858] Catch Exception when disposing connection --- MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs index fc9ea37d1e..48ee78a7cc 100644 --- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs +++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs @@ -232,6 +232,11 @@ namespace MediaBrowser.Controller.Net // TODO Investigate and properly fix. Logger.LogError(ex, "Object Disposed"); } + catch (Exception ex) + { + // TODO Investigate and properly fix. + Logger.LogError(ex, "Object Disposed Exception"); + } lock (_activeConnections) { From 62204dce00a41a866b89acaf17f2e689c10b8326 Mon Sep 17 00:00:00 2001 From: ipitio <21136719+ipitio@users.noreply.github.com> Date: Wed, 15 Feb 2023 22:06:14 -0500 Subject: [PATCH 077/858] add contributor --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 20ea7a4ab7..c9430b235f 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -163,6 +163,7 @@ - [vgambier](https://github.com/vgambier) - [MinecraftPlaye](https://github.com/MinecraftPlaye) - [RealGreenDragon](https://github.com/RealGreenDragon) + - [ipitio](https://github.com/ipitio) # Emby Contributors From 42498194d9a4069b8cdeb9446f2714f74e3169de Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Thu, 16 Feb 2023 18:15:12 +0100 Subject: [PATCH 078/858] Replace ISocket and UdpSocket, fix DLNA and SSDP binding and discovery --- Emby.Dlna/Configuration/DlnaOptions.cs | 2 +- Emby.Dlna/Main/DlnaEntryPoint.cs | 47 ++- Emby.Dlna/Ssdp/DeviceDiscovery.cs | 6 +- .../ApplicationHost.cs | 4 +- .../EntryPoints/UdpServerEntryPoint.cs | 34 +-- .../TunerHosts/HdHomerun/HdHomerunHost.cs | 10 +- .../Net/SocketFactory.cs | 69 ++--- Emby.Server.Implementations/Net/UdpSocket.cs | 267 ------------------ Jellyfin.Networking/Manager/NetworkManager.cs | 11 +- .../Extensions/WebHostBuilderExtensions.cs | 2 +- MediaBrowser.Common/Net/INetworkManager.cs | 12 +- MediaBrowser.Model/MediaBrowser.Model.csproj | 1 + .../Net/IPData.cs | 2 +- MediaBrowser.Model/Net/ISocket.cs | 34 --- MediaBrowser.Model/Net/ISocketFactory.cs | 24 +- RSSDP/ISsdpCommunicationsServer.cs | 4 +- RSSDP/SsdpCommunicationsServer.cs | 168 +++++------ RSSDP/SsdpConstants.cs | 2 + RSSDP/SsdpDeviceLocator.cs | 74 +++-- RSSDP/SsdpDevicePublisher.cs | 64 ++--- .../NetworkParseTests.cs | 9 +- 21 files changed, 289 insertions(+), 557 deletions(-) delete mode 100644 Emby.Server.Implementations/Net/UdpSocket.cs rename {MediaBrowser.Common => MediaBrowser.Model}/Net/IPData.cs (98%) delete mode 100644 MediaBrowser.Model/Net/ISocket.cs diff --git a/Emby.Dlna/Configuration/DlnaOptions.cs b/Emby.Dlna/Configuration/DlnaOptions.cs index e95a878c67..f233468de3 100644 --- a/Emby.Dlna/Configuration/DlnaOptions.cs +++ b/Emby.Dlna/Configuration/DlnaOptions.cs @@ -17,7 +17,7 @@ namespace Emby.Dlna.Configuration BlastAliveMessages = true; SendOnlyMatchedHost = true; ClientDiscoveryIntervalSeconds = 60; - AliveMessageIntervalSeconds = 1800; + AliveMessageIntervalSeconds = 180; } /// <summary> diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index 87c52163d9..f6ec9574b6 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -7,7 +7,6 @@ using System.Globalization; using System.Linq; using System.Net.Http; using System.Net.Sockets; -using System.Runtime.InteropServices; using System.Threading.Tasks; using Emby.Dlna.PlayTo; using Emby.Dlna.Ssdp; @@ -201,8 +200,7 @@ namespace Emby.Dlna.Main { if (_communicationsServer is null) { - var enableMultiSocketBinding = OperatingSystem.IsWindows() || - OperatingSystem.IsLinux(); + var enableMultiSocketBinding = OperatingSystem.IsWindows() || OperatingSystem.IsLinux(); _communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding) { @@ -248,11 +246,6 @@ namespace Emby.Dlna.Main public void StartDevicePublisher(Configuration.DlnaOptions options) { - if (!options.BlastAliveMessages) - { - return; - } - if (_publisher is not null) { return; @@ -263,7 +256,8 @@ namespace Emby.Dlna.Main _publisher = new SsdpDevicePublisher( _communicationsServer, Environment.OSVersion.Platform.ToString(), - Environment.OSVersion.VersionString, + // Can not use VersionString here since that includes OS and version + Environment.OSVersion.Version.ToString(), _config.GetDlnaConfiguration().SendOnlyMatchedHost) { LogFunction = (msg) => _logger.LogDebug("{Msg}", msg), @@ -272,7 +266,10 @@ namespace Emby.Dlna.Main RegisterServerEndpoints(); - _publisher.StartBroadcastingAliveMessages(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds)); + if (options.BlastAliveMessages) + { + _publisher.StartSendingAliveNotifications(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds)); + } } catch (Exception ex) { @@ -286,38 +283,32 @@ namespace Emby.Dlna.Main var descriptorUri = "/dlna/" + udn + "/description.xml"; // Only get bind addresses in LAN - var bindAddresses = _networkManager - .GetInternalBindAddresses() - .Where(i => i.Address.AddressFamily == AddressFamily.InterNetwork - || (i.AddressFamily == AddressFamily.InterNetworkV6 && i.Address.ScopeId != 0)) + // IPv6 is currently unsupported + var validInterfaces = _networkManager.GetInternalBindAddresses() + .Where(x => x.Address is not null) + .Where(x => x.AddressFamily != AddressFamily.InterNetworkV6) .ToList(); - if (bindAddresses.Count == 0) + if (validInterfaces.Count == 0) { - // No interfaces returned, so use loopback. - bindAddresses = _networkManager.GetLoopbacks().ToList(); + // No interfaces returned, fall back to loopback + validInterfaces = _networkManager.GetLoopbacks().ToList(); } - foreach (var address in bindAddresses) + foreach (var intf in validInterfaces) { - if (address.AddressFamily == AddressFamily.InterNetworkV6) - { - // Not supporting IPv6 right now - continue; - } - var fullService = "urn:schemas-upnp-org:device:MediaServer:1"; - _logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, address.Address); + _logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, intf.Address); - var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(address.Address, false) + descriptorUri); + var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(intf.Address, false) + descriptorUri); var device = new SsdpRootDevice { CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info. Location = uri.Uri, // Must point to the URL that serves your devices UPnP description document. - Address = address.Address, - PrefixLength = NetworkExtensions.MaskToCidr(address.Subnet.Prefix), + Address = intf.Address, + PrefixLength = NetworkExtensions.MaskToCidr(intf.Subnet.Prefix), FriendlyName = "Jellyfin", Manufacturer = "Jellyfin", ModelName = "Jellyfin Server", diff --git a/Emby.Dlna/Ssdp/DeviceDiscovery.cs b/Emby.Dlna/Ssdp/DeviceDiscovery.cs index 8a4e5ff455..43d673c772 100644 --- a/Emby.Dlna/Ssdp/DeviceDiscovery.cs +++ b/Emby.Dlna/Ssdp/DeviceDiscovery.cs @@ -73,7 +73,11 @@ namespace Emby.Dlna.Ssdp { if (_listenerCount > 0 && _deviceLocator is null && _commsServer is not null) { - _deviceLocator = new SsdpDeviceLocator(_commsServer); + _deviceLocator = new SsdpDeviceLocator( + _commsServer, + Environment.OSVersion.Platform.ToString(), + // Can not use VersionString here since that includes OS and version + Environment.OSVersion.Version.ToString()); // (Optional) Set the filter so we only see notifications for devices we care about // (can be any search target value i.e device type, uuid value etc - any value that appears in the diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 50befaa537..485253bf74 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1025,7 +1025,7 @@ namespace Emby.Server.Implementations return PublishedServerUrl.Trim('/'); } - string smart = NetManager.GetBindInterface(hostname, out var port); + string smart = NetManager.GetBindAddress(hostname, out var port); return GetLocalApiUrl(smart.Trim('/'), null, port); } @@ -1033,7 +1033,7 @@ namespace Emby.Server.Implementations public string GetApiUrlForLocalAccess(IPAddress ipAddress = null, bool allowHttps = true) { // With an empty source, the port will be null - var smart = NetManager.GetBindAddress(ipAddress, out _); + var smart = NetManager.GetBindAddress(ipAddress, out _, true); var scheme = !allowHttps ? Uri.UriSchemeHttp : null; int? port = !allowHttps ? HttpPort : null; return GetLocalApiUrl(smart, scheme, port); diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs index b5a33a735d..8fb1f93228 100644 --- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs +++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Net; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; @@ -80,31 +82,26 @@ namespace Emby.Server.Implementations.EntryPoints if (_enableMultiSocketBinding) { // Add global broadcast socket - _udpServers.Add(new UdpServer(_logger, _appHost, _config, System.Net.IPAddress.Broadcast, PortNumber)); + _udpServers.Add(new UdpServer(_logger, _appHost, _config, IPAddress.Broadcast, PortNumber)); // Add bind address specific broadcast sockets - foreach (var bindAddress in _networkManager.GetInternalBindAddresses()) + // IPv6 is currently unsupported + var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork); + foreach (var intf in validInterfaces) { - if (bindAddress.AddressFamily == AddressFamily.InterNetworkV6) - { - // Not supporting IPv6 right now - continue; - } - - var broadcastAddress = NetworkExtensions.GetBroadcastAddress(bindAddress.Subnet); + var broadcastAddress = NetworkExtensions.GetBroadcastAddress(intf.Subnet); _logger.LogDebug("Binding UDP server to {Address} on port {PortNumber}", broadcastAddress.ToString(), PortNumber); - _udpServers.Add(new UdpServer(_logger, _appHost, _config, broadcastAddress, PortNumber)); + var server = new UdpServer(_logger, _appHost, _config, broadcastAddress, PortNumber); + server.Start(_cancellationTokenSource.Token); + _udpServers.Add(server); } } else { - _udpServers.Add(new UdpServer(_logger, _appHost, _config, System.Net.IPAddress.Any, PortNumber)); - } - - foreach (var server in _udpServers) - { + var server = new UdpServer(_logger, _appHost, _config, IPAddress.Any, PortNumber); server.Start(_cancellationTokenSource.Token); + _udpServers.Add(server); } } catch (SocketException ex) @@ -133,9 +130,12 @@ namespace Emby.Server.Implementations.EntryPoints _cancellationTokenSource.Cancel(); _cancellationTokenSource.Dispose(); - _udpServers.ForEach(s => s.Dispose()); - _udpServers.Clear(); + foreach (var server in _udpServers) + { + server.Dispose(); + } + _udpServers.Clear(); _disposed = true; } } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index 98bbc15406..e76961ce9e 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -661,16 +661,16 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun // Need a way to set the Receive timeout on the socket otherwise this might never timeout? try { - await udpClient.SendToAsync(discBytes, 0, discBytes.Length, new IPEndPoint(IPAddress.Parse("255.255.255.255"), 65001), cancellationToken).ConfigureAwait(false); + await udpClient.SendToAsync(discBytes, new IPEndPoint(IPAddress.Parse("255.255.255.255"), 65001), cancellationToken).ConfigureAwait(false); var receiveBuffer = new byte[8192]; while (!cancellationToken.IsCancellationRequested) { - var response = await udpClient.ReceiveAsync(receiveBuffer, 0, receiveBuffer.Length, cancellationToken).ConfigureAwait(false); - var deviceIp = response.RemoteEndPoint.Address.ToString(); + var response = await udpClient.ReceiveMessageFromAsync(receiveBuffer, new IPEndPoint(IPAddress.Any, 0), cancellationToken).ConfigureAwait(false); + var deviceIp = ((IPEndPoint)response.RemoteEndPoint).Address.ToString(); - // check to make sure we have enough bytes received to be a valid message and make sure the 2nd byte is the discover reply byte - if (response.ReceivedBytes > 13 && response.Buffer[1] == 3) + // Check to make sure we have enough bytes received to be a valid message and make sure the 2nd byte is the discover reply byte + if (response.ReceivedBytes > 13 && receiveBuffer[1] == 3) { var deviceAddress = "http://" + deviceIp; diff --git a/Emby.Server.Implementations/Net/SocketFactory.cs b/Emby.Server.Implementations/Net/SocketFactory.cs index b6d87a7880..d134d948ab 100644 --- a/Emby.Server.Implementations/Net/SocketFactory.cs +++ b/Emby.Server.Implementations/Net/SocketFactory.cs @@ -10,61 +10,63 @@ namespace Emby.Server.Implementations.Net public class SocketFactory : ISocketFactory { /// <inheritdoc /> - public ISocket CreateUdpBroadcastSocket(int localPort) + public Socket CreateUdpBroadcastSocket(int localPort) { if (localPort < 0) { throw new ArgumentException("localPort cannot be less than zero.", nameof(localPort)); } - var retVal = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); try { - retVal.EnableBroadcast = true; - retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); - retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, 1); + socket.EnableBroadcast = true; + socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, 1); + socket.Bind(new IPEndPoint(IPAddress.Any, localPort)); - return new UdpSocket(retVal, localPort, IPAddress.Any); + return socket; } catch { - retVal?.Dispose(); + socket?.Dispose(); throw; } } /// <inheritdoc /> - public ISocket CreateSsdpUdpSocket(IPAddress localIp, int localPort) + public Socket CreateSsdpUdpSocket(IPData bindInterface, int localPort) { + ArgumentNullException.ThrowIfNull(bindInterface.Address); + if (localPort < 0) { throw new ArgumentException("localPort cannot be less than zero.", nameof(localPort)); } - var retVal = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); try { - retVal.EnableBroadcast = true; - retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); - retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, 4); + socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + socket.Bind(new IPEndPoint(bindInterface.Address, localPort)); - retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(IPAddress.Parse("239.255.255.250"), localIp)); - return new UdpSocket(retVal, localPort, localIp); + return socket; } catch { - retVal?.Dispose(); + socket?.Dispose(); throw; } } /// <inheritdoc /> - public ISocket CreateUdpMulticastSocket(IPAddress ipAddress, IPAddress bindIpAddress, int multicastTimeToLive, int localPort) + public Socket CreateUdpMulticastSocket(IPAddress multicastAddress, IPData bindInterface, int multicastTimeToLive, int localPort) { - ArgumentNullException.ThrowIfNull(ipAddress); - ArgumentNullException.ThrowIfNull(bindIpAddress); + var bindIPAddress = bindInterface.Address; + ArgumentNullException.ThrowIfNull(multicastAddress); + ArgumentNullException.ThrowIfNull(bindIPAddress); if (multicastTimeToLive <= 0) { @@ -76,34 +78,25 @@ namespace Emby.Server.Implementations.Net throw new ArgumentException("localPort cannot be less than zero.", nameof(localPort)); } - var retVal = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); - - retVal.ExclusiveAddressUse = false; + var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); try { - // seeing occasional exceptions thrown on qnap - // System.Net.Sockets.SocketException (0x80004005): Protocol not available - retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); - } - catch (SocketException) - { - } + var interfaceIndex = (int)IPAddress.HostToNetworkOrder(bindInterface.Index); - try - { - retVal.EnableBroadcast = true; - // retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, true); - retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, multicastTimeToLive); + socket.MulticastLoopback = false; + socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.PacketInformation, true); + socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, multicastTimeToLive); + socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastInterface, interfaceIndex); + socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress, interfaceIndex)); + socket.Bind(new IPEndPoint(multicastAddress, localPort)); - retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(ipAddress, bindIpAddress)); - retVal.MulticastLoopback = true; - - return new UdpSocket(retVal, localPort, bindIpAddress); + return socket; } catch { - retVal?.Dispose(); + socket?.Dispose(); throw; } diff --git a/Emby.Server.Implementations/Net/UdpSocket.cs b/Emby.Server.Implementations/Net/UdpSocket.cs deleted file mode 100644 index 577b79283a..0000000000 --- a/Emby.Server.Implementations/Net/UdpSocket.cs +++ /dev/null @@ -1,267 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System; -using System.Net; -using System.Net.Sockets; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.Net; - -namespace Emby.Server.Implementations.Net -{ - // THIS IS A LINKED FILE - SHARED AMONGST MULTIPLE PLATFORMS - // Be careful to check any changes compile and work for all platform projects it is shared in. - - public sealed class UdpSocket : ISocket, IDisposable - { - private readonly int _localPort; - - private readonly SocketAsyncEventArgs _receiveSocketAsyncEventArgs = new SocketAsyncEventArgs() - { - SocketFlags = SocketFlags.None - }; - - private readonly SocketAsyncEventArgs _sendSocketAsyncEventArgs = new SocketAsyncEventArgs() - { - SocketFlags = SocketFlags.None - }; - - private Socket _socket; - private bool _disposed = false; - private TaskCompletionSource<SocketReceiveResult> _currentReceiveTaskCompletionSource; - private TaskCompletionSource<int> _currentSendTaskCompletionSource; - - public UdpSocket(Socket socket, int localPort, IPAddress ip) - { - ArgumentNullException.ThrowIfNull(socket); - - _socket = socket; - _localPort = localPort; - LocalIPAddress = ip; - - _socket.Bind(new IPEndPoint(ip, _localPort)); - - InitReceiveSocketAsyncEventArgs(); - } - - public UdpSocket(Socket socket, IPEndPoint endPoint) - { - ArgumentNullException.ThrowIfNull(socket); - - _socket = socket; - _socket.Connect(endPoint); - - InitReceiveSocketAsyncEventArgs(); - } - - public Socket Socket => _socket; - - public IPAddress LocalIPAddress { get; } - - private void InitReceiveSocketAsyncEventArgs() - { - var receiveBuffer = new byte[8192]; - _receiveSocketAsyncEventArgs.SetBuffer(receiveBuffer, 0, receiveBuffer.Length); - _receiveSocketAsyncEventArgs.Completed += OnReceiveSocketAsyncEventArgsCompleted; - - var sendBuffer = new byte[8192]; - _sendSocketAsyncEventArgs.SetBuffer(sendBuffer, 0, sendBuffer.Length); - _sendSocketAsyncEventArgs.Completed += OnSendSocketAsyncEventArgsCompleted; - } - - private void OnReceiveSocketAsyncEventArgsCompleted(object sender, SocketAsyncEventArgs e) - { - var tcs = _currentReceiveTaskCompletionSource; - if (tcs is not null) - { - _currentReceiveTaskCompletionSource = null; - - if (e.SocketError == SocketError.Success) - { - tcs.TrySetResult(new SocketReceiveResult - { - Buffer = e.Buffer, - ReceivedBytes = e.BytesTransferred, - RemoteEndPoint = e.RemoteEndPoint as IPEndPoint, - LocalIPAddress = LocalIPAddress - }); - } - else - { - tcs.TrySetException(new SocketException((int)e.SocketError)); - } - } - } - - private void OnSendSocketAsyncEventArgsCompleted(object sender, SocketAsyncEventArgs e) - { - var tcs = _currentSendTaskCompletionSource; - if (tcs is not null) - { - _currentSendTaskCompletionSource = null; - - if (e.SocketError == SocketError.Success) - { - tcs.TrySetResult(e.BytesTransferred); - } - else - { - tcs.TrySetException(new SocketException((int)e.SocketError)); - } - } - } - - public IAsyncResult BeginReceive(byte[] buffer, int offset, int count, AsyncCallback callback) - { - ThrowIfDisposed(); - - EndPoint receivedFromEndPoint = new IPEndPoint(IPAddress.Any, 0); - - return _socket.BeginReceiveFrom(buffer, offset, count, SocketFlags.None, ref receivedFromEndPoint, callback, buffer); - } - - public int Receive(byte[] buffer, int offset, int count) - { - ThrowIfDisposed(); - - return _socket.Receive(buffer, 0, buffer.Length, SocketFlags.None); - } - - public SocketReceiveResult EndReceive(IAsyncResult result) - { - ThrowIfDisposed(); - - var sender = new IPEndPoint(IPAddress.Any, 0); - var remoteEndPoint = (EndPoint)sender; - - var receivedBytes = _socket.EndReceiveFrom(result, ref remoteEndPoint); - - var buffer = (byte[])result.AsyncState; - - return new SocketReceiveResult - { - ReceivedBytes = receivedBytes, - RemoteEndPoint = (IPEndPoint)remoteEndPoint, - Buffer = buffer, - LocalIPAddress = LocalIPAddress - }; - } - - public Task<SocketReceiveResult> ReceiveAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - ThrowIfDisposed(); - - var taskCompletion = new TaskCompletionSource<SocketReceiveResult>(TaskCreationOptions.RunContinuationsAsynchronously); - bool isResultSet = false; - - Action<IAsyncResult> callback = callbackResult => - { - try - { - if (!isResultSet) - { - isResultSet = true; - taskCompletion.TrySetResult(EndReceive(callbackResult)); - } - } - catch (Exception ex) - { - taskCompletion.TrySetException(ex); - } - }; - - var result = BeginReceive(buffer, offset, count, new AsyncCallback(callback)); - - if (result.CompletedSynchronously) - { - callback(result); - return taskCompletion.Task; - } - - cancellationToken.Register(() => taskCompletion.TrySetCanceled()); - - return taskCompletion.Task; - } - - public Task SendToAsync(byte[] buffer, int offset, int bytes, IPEndPoint endPoint, CancellationToken cancellationToken) - { - ThrowIfDisposed(); - - var taskCompletion = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously); - bool isResultSet = false; - - Action<IAsyncResult> callback = callbackResult => - { - try - { - if (!isResultSet) - { - isResultSet = true; - taskCompletion.TrySetResult(EndSendTo(callbackResult)); - } - } - catch (Exception ex) - { - taskCompletion.TrySetException(ex); - } - }; - - var result = BeginSendTo(buffer, offset, bytes, endPoint, new AsyncCallback(callback), null); - - if (result.CompletedSynchronously) - { - callback(result); - return taskCompletion.Task; - } - - cancellationToken.Register(() => taskCompletion.TrySetCanceled()); - - return taskCompletion.Task; - } - - public IAsyncResult BeginSendTo(byte[] buffer, int offset, int size, IPEndPoint endPoint, AsyncCallback callback, object state) - { - ThrowIfDisposed(); - - return _socket.BeginSendTo(buffer, offset, size, SocketFlags.None, endPoint, callback, state); - } - - public int EndSendTo(IAsyncResult result) - { - ThrowIfDisposed(); - - return _socket.EndSendTo(result); - } - - private void ThrowIfDisposed() - { - if (_disposed) - { - throw new ObjectDisposedException(nameof(UdpSocket)); - } - } - - /// <inheritdoc /> - public void Dispose() - { - if (_disposed) - { - return; - } - - _socket?.Dispose(); - _receiveSocketAsyncEventArgs.Dispose(); - _sendSocketAsyncEventArgs.Dispose(); - _currentReceiveTaskCompletionSource?.TrySetCanceled(); - _currentSendTaskCompletionSource?.TrySetCanceled(); - - _socket = null; - _currentReceiveTaskCompletionSource = null; - _currentSendTaskCompletionSource = null; - - _disposed = true; - } - } -} diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 5f82950fce..cdd34bc896 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -9,6 +9,7 @@ using System.Threading; using Jellyfin.Networking.Configuration; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; +using MediaBrowser.Model.Net; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.Extensions.Logging; @@ -699,7 +700,7 @@ namespace Jellyfin.Networking.Manager } /// <inheritdoc/> - public string GetBindInterface(string source, out int? port) + public string GetBindAddress(string source, out int? port) { if (!NetworkExtensions.TryParseHost(source, out var addresses, IsIPv4Enabled, IsIPv6Enabled)) { @@ -711,16 +712,16 @@ namespace Jellyfin.Networking.Manager } /// <inheritdoc/> - public string GetBindInterface(HttpRequest source, out int? port) + public string GetBindAddress(HttpRequest source, out int? port) { - var result = GetBindInterface(source.Host.Host, out port); + var result = GetBindAddress(source.Host.Host, out port); port ??= source.Host.Port; return result; } /// <inheritdoc/> - public string GetBindAddress(IPAddress? source, out int? port) + public string GetBindAddress(IPAddress? source, out int? port, bool skipOverrides = false) { port = null; @@ -741,7 +742,7 @@ namespace Jellyfin.Networking.Manager bool isExternal = !_lanSubnets.Any(network => network.Contains(source)); _logger.LogDebug("Trying to get bind address for source {Source} - External: {IsExternal}", source, isExternal); - if (MatchesPublishedServerUrl(source, isExternal, out result)) + if (!skipOverrides && MatchesPublishedServerUrl(source, isExternal, out result)) { return result; } diff --git a/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs b/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs index eac13f7610..3cb791b571 100644 --- a/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs @@ -41,7 +41,7 @@ public static class WebHostBuilderExtensions bool flagged = false; foreach (var netAdd in addresses) { - logger.LogInformation("Kestrel listening on {Address}", IPAddress.IPv6Any.Equals(netAdd.Address) ? "All Addresses" : netAdd); + logger.LogInformation("Kestrel is listening on {Address}", IPAddress.IPv6Any.Equals(netAdd.Address) ? "All IPv6 addresses" : netAdd.Address); options.Listen(netAdd.Address, appHost.HttpPort); if (appHost.ListenWithHttps) { diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs index 5721b19ca2..68974f738d 100644 --- a/MediaBrowser.Common/Net/INetworkManager.cs +++ b/MediaBrowser.Common/Net/INetworkManager.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Net; using System.Net.NetworkInformation; +using MediaBrowser.Model.Net; using Microsoft.AspNetCore.Http; namespace MediaBrowser.Common.Net @@ -70,27 +71,28 @@ namespace MediaBrowser.Common.Net /// <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); + string GetBindAddress(HttpRequest source, out int? port); /// <summary> /// Retrieves the bind address to use in system URLs. (Server Discovery, PlayTo, LiveTV, SystemInfo) /// If no bind addresses are specified, an internal interface address is selected. - /// (See <see cref="GetBindAddress(IPAddress, out int?)"/>. + /// (See <see cref="GetBindAddress(IPAddress, out int?, bool)"/>. /// </summary> /// <param name="source">IP address of the request.</param> /// <param name="port">Optional port returned, if it's part of an override.</param> + /// <param name="skipOverrides">Optional boolean denoting if published server overrides should be ignored. Defaults to false.</param> /// <returns>IP address to use, or loopback address if all else fails.</returns> - string GetBindAddress(IPAddress source, out int? port); + string GetBindAddress(IPAddress source, out int? port, bool skipOverrides = false); /// <summary> /// Retrieves the bind address to use in system URLs. (Server Discovery, PlayTo, LiveTV, SystemInfo) /// If no bind addresses are specified, an internal interface address is selected. - /// (See <see cref="GetBindAddress(IPAddress, out int?)"/>. + /// (See <see cref="GetBindAddress(IPAddress, out int?, bool)"/>. /// </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); + string GetBindAddress(string source, out int? port); /// <summary> /// Get a list of all the MAC addresses associated with active interfaces. diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 9a58044853..087e6369e6 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -58,6 +58,7 @@ <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" /> </ItemGroup> <ItemGroup> + <FrameworkReference Include="Microsoft.AspNetCore.App" /> <ProjectReference Include="../Jellyfin.Data/Jellyfin.Data.csproj" /> <ProjectReference Include="../src/Jellyfin.Extensions/Jellyfin.Extensions.csproj" /> </ItemGroup> diff --git a/MediaBrowser.Common/Net/IPData.cs b/MediaBrowser.Model/Net/IPData.cs similarity index 98% rename from MediaBrowser.Common/Net/IPData.cs rename to MediaBrowser.Model/Net/IPData.cs index 05842632c8..16d74dcddd 100644 --- a/MediaBrowser.Common/Net/IPData.cs +++ b/MediaBrowser.Model/Net/IPData.cs @@ -2,7 +2,7 @@ using System.Net; using System.Net.Sockets; using Microsoft.AspNetCore.HttpOverrides; -namespace MediaBrowser.Common.Net +namespace MediaBrowser.Model.Net { /// <summary> /// Base network object class. diff --git a/MediaBrowser.Model/Net/ISocket.cs b/MediaBrowser.Model/Net/ISocket.cs deleted file mode 100644 index 3de41d565a..0000000000 --- a/MediaBrowser.Model/Net/ISocket.cs +++ /dev/null @@ -1,34 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Net; -using System.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Model.Net -{ - /// <summary> - /// Provides a common interface across platforms for UDP sockets used by this SSDP implementation. - /// </summary> - public interface ISocket : IDisposable - { - IPAddress LocalIPAddress { get; } - - Task<SocketReceiveResult> ReceiveAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken); - - IAsyncResult BeginReceive(byte[] buffer, int offset, int count, AsyncCallback callback); - - SocketReceiveResult EndReceive(IAsyncResult result); - - /// <summary> - /// Sends a UDP message to a particular end point (uni or multicast). - /// </summary> - /// <param name="buffer">An array of type <see cref="byte" /> that contains the data to send.</param> - /// <param name="offset">The zero-based position in buffer at which to begin sending data.</param> - /// <param name="bytes">The number of bytes to send.</param> - /// <param name="endPoint">An <see cref="IPEndPoint" /> that represents the remote device.</param> - /// <param name="cancellationToken">The cancellation token to cancel operation.</param> - /// <returns>The task object representing the asynchronous operation.</returns> - Task SendToAsync(byte[] buffer, int offset, int bytes, IPEndPoint endPoint, CancellationToken cancellationToken); - } -} diff --git a/MediaBrowser.Model/Net/ISocketFactory.cs b/MediaBrowser.Model/Net/ISocketFactory.cs index f3bc31796d..49a88c2277 100644 --- a/MediaBrowser.Model/Net/ISocketFactory.cs +++ b/MediaBrowser.Model/Net/ISocketFactory.cs @@ -1,32 +1,36 @@ -#pragma warning disable CS1591 - using System.Net; +using System.Net.Sockets; namespace MediaBrowser.Model.Net { /// <summary> - /// Implemented by components that can create a platform specific UDP socket implementation, and wrap it in the cross platform <see cref="ISocket"/> interface. + /// Implemented by components that can create specific socket configurations. /// </summary> public interface ISocketFactory { - ISocket CreateUdpBroadcastSocket(int localPort); + /// <summary> + /// Creates a new unicast socket using the specified local port number. + /// </summary> + /// <param name="localPort">The local port to bind to.</param> + /// <returns>A new unicast socket using the specified local port number.</returns> + Socket CreateUdpBroadcastSocket(int localPort); /// <summary> /// Creates a new unicast socket using the specified local port number. /// </summary> - /// <param name="localIp">The local IP address to bind to.</param> + /// <param name="bindInterface">The bind interface.</param> /// <param name="localPort">The local port to bind to.</param> /// <returns>A new unicast socket using the specified local port number.</returns> - ISocket CreateSsdpUdpSocket(IPAddress localIp, int localPort); + Socket CreateSsdpUdpSocket(IPData bindInterface, int localPort); /// <summary> /// Creates a new multicast socket using the specified multicast IP address, multicast time to live and local port. /// </summary> - /// <param name="ipAddress">The multicast IP address to bind to.</param> - /// <param name="bindIpAddress">The bind IP address.</param> + /// <param name="multicastAddress">The multicast IP address to bind to.</param> + /// <param name="bindInterface">The bind interface.</param> /// <param name="multicastTimeToLive">The multicast time to live value. Actually a maximum number of network hops for UDP packets.</param> /// <param name="localPort">The local port to bind to.</param> - /// <returns>A <see cref="ISocket"/> implementation.</returns> - ISocket CreateUdpMulticastSocket(IPAddress ipAddress, IPAddress bindIpAddress, int multicastTimeToLive, int localPort); + /// <returns>A new multicast socket using the specfied bind interface, multicast address, multicast time to live and port.</returns> + Socket CreateUdpMulticastSocket(IPAddress multicastAddress, IPData bindInterface, int multicastTimeToLive, int localPort); } } diff --git a/RSSDP/ISsdpCommunicationsServer.cs b/RSSDP/ISsdpCommunicationsServer.cs index 3cbc991d60..571c66c107 100644 --- a/RSSDP/ISsdpCommunicationsServer.cs +++ b/RSSDP/ISsdpCommunicationsServer.cs @@ -23,12 +23,12 @@ namespace Rssdp.Infrastructure /// <summary> /// Causes the server to begin listening for multicast messages, being SSDP search requests and notifications. /// </summary> - void BeginListeningForBroadcasts(); + void BeginListeningForMulticast(); /// <summary> /// Causes the server to stop listening for multicast messages, being SSDP search requests and notifications. /// </summary> - void StopListeningForBroadcasts(); + void StopListeningForMulticast(); /// <summary> /// Sends a message to a particular address (uni or multicast) and port. diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs index fb5a66aa10..6ae260d557 100644 --- a/RSSDP/SsdpCommunicationsServer.cs +++ b/RSSDP/SsdpCommunicationsServer.cs @@ -25,18 +25,18 @@ namespace Rssdp.Infrastructure * Since stopping the service would be a bad idea (might not be allowed security wise and might * break other apps running on the system) the only other work around is to use two sockets. * - * We use one socket to listen for/receive notifications and search requests (_BroadcastListenSocket). - * We use a second socket, bound to a different local port, to send search requests and listen for - * responses (_SendSocket). The responses are sent to the local port this socket is bound to, - * which isn't port 1900 so the MS service doesn't steal them. While the caller can specify a local + * We use one group of sockets to listen for/receive notifications and search requests (_MulticastListenSockets). + * We use a second group, bound to a different local port, to send search requests and listen for + * responses (_SendSockets). The responses are sent to the local ports these sockets are bound to, + * which aren't port 1900 so the MS service doesn't steal them. While the caller can specify a local * port to use, we will default to 0 which allows the underlying system to auto-assign a free port. */ private object _BroadcastListenSocketSynchroniser = new object(); - private List<ISocket> _BroadcastListenSockets; + private List<Socket> _MulticastListenSockets; private object _SendSocketSynchroniser = new object(); - private List<ISocket> _sendSockets; + private List<Socket> _sendSockets; private HttpRequestParser _RequestParser; private HttpResponseParser _ResponseParser; @@ -78,7 +78,7 @@ namespace Rssdp.Infrastructure /// <exception cref="ArgumentOutOfRangeException">The <paramref name="multicastTimeToLive"/> argument is less than or equal to zero.</exception> public SsdpCommunicationsServer(ISocketFactory socketFactory, int localPort, int multicastTimeToLive, INetworkManager networkManager, ILogger logger, bool enableMultiSocketBinding) { - if (socketFactory == null) + if (socketFactory is null) { throw new ArgumentNullException(nameof(socketFactory)); } @@ -107,25 +107,25 @@ namespace Rssdp.Infrastructure /// Causes the server to begin listening for multicast messages, being SSDP search requests and notifications. /// </summary> /// <exception cref="ObjectDisposedException">Thrown if the <see cref="DisposableManagedObjectBase.IsDisposed"/> property is true (because <seealso cref="DisposableManagedObjectBase.Dispose()" /> has been called previously).</exception> - public void BeginListeningForBroadcasts() + public void BeginListeningForMulticast() { ThrowIfDisposed(); lock (_BroadcastListenSocketSynchroniser) { - if (_BroadcastListenSockets == null) + if (_MulticastListenSockets is null) { try { - _BroadcastListenSockets = ListenForBroadcasts(); + _MulticastListenSockets = CreateMulticastSocketsAndListen(); } catch (SocketException ex) { - _logger.LogError("Failed to bind to port 1900: {Message}. DLNA will be unavailable", ex.Message); + _logger.LogError("Failed to bind to multicast address: {Message}. DLNA will be unavailable", ex.Message); } catch (Exception ex) { - _logger.LogError(ex, "Error in BeginListeningForBroadcasts"); + _logger.LogError(ex, "Error in BeginListeningForMulticast"); } } } @@ -135,15 +135,19 @@ namespace Rssdp.Infrastructure /// Causes the server to stop listening for multicast messages, being SSDP search requests and notifications. /// </summary> /// <exception cref="ObjectDisposedException">Thrown if the <see cref="DisposableManagedObjectBase.IsDisposed"/> property is true (because <seealso cref="DisposableManagedObjectBase.Dispose()" /> has been called previously).</exception> - public void StopListeningForBroadcasts() + public void StopListeningForMulticast() { lock (_BroadcastListenSocketSynchroniser) { - if (_BroadcastListenSockets != null) + if (_MulticastListenSockets is not null) { _logger.LogInformation("{0} disposing _BroadcastListenSocket", GetType().Name); - _BroadcastListenSockets.ForEach(s => s.Dispose()); - _BroadcastListenSockets = null; + foreach (var socket in _MulticastListenSockets) + { + socket.Dispose(); + } + + _MulticastListenSockets = null; } } } @@ -153,7 +157,7 @@ namespace Rssdp.Infrastructure /// </summary> public async Task SendMessage(byte[] messageData, IPEndPoint destination, IPAddress fromLocalIpAddress, CancellationToken cancellationToken) { - if (messageData == null) + if (messageData is null) { throw new ArgumentNullException(nameof(messageData)); } @@ -177,11 +181,11 @@ namespace Rssdp.Infrastructure } } - private async Task SendFromSocket(ISocket socket, byte[] messageData, IPEndPoint destination, CancellationToken cancellationToken) + private async Task SendFromSocket(Socket socket, byte[] messageData, IPEndPoint destination, CancellationToken cancellationToken) { try { - await socket.SendToAsync(messageData, 0, messageData.Length, destination, cancellationToken).ConfigureAwait(false); + await socket.SendToAsync(messageData, destination, cancellationToken).ConfigureAwait(false); } catch (ObjectDisposedException) { @@ -191,37 +195,42 @@ namespace Rssdp.Infrastructure } catch (Exception ex) { - _logger.LogError(ex, "Error sending socket message from {0} to {1}", socket.LocalIPAddress.ToString(), destination.ToString()); + var localIP = ((IPEndPoint)socket.LocalEndPoint).Address; + _logger.LogError(ex, "Error sending socket message from {0} to {1}", localIP.ToString(), destination.ToString()); } } - private List<ISocket> GetSendSockets(IPAddress fromLocalIpAddress, IPEndPoint destination) + private List<Socket> GetSendSockets(IPAddress fromLocalIpAddress, IPEndPoint destination) { EnsureSendSocketCreated(); lock (_SendSocketSynchroniser) { - var sockets = _sendSockets.Where(i => i.LocalIPAddress.AddressFamily == fromLocalIpAddress.AddressFamily); + var sockets = _sendSockets.Where(s => s.AddressFamily == fromLocalIpAddress.AddressFamily); // Send from the Any socket and the socket with the matching address if (fromLocalIpAddress.AddressFamily == AddressFamily.InterNetwork) { - sockets = sockets.Where(i => i.LocalIPAddress.Equals(IPAddress.Any) || fromLocalIpAddress.Equals(i.LocalIPAddress)); + sockets = sockets.Where(s => ((IPEndPoint)s.LocalEndPoint).Address.Equals(IPAddress.Any) + || ((IPEndPoint)s.LocalEndPoint).Address.Equals(fromLocalIpAddress)); // If sending to the loopback address, filter the socket list as well if (destination.Address.Equals(IPAddress.Loopback)) { - sockets = sockets.Where(i => i.LocalIPAddress.Equals(IPAddress.Any) || i.LocalIPAddress.Equals(IPAddress.Loopback)); + sockets = sockets.Where(s => ((IPEndPoint)s.LocalEndPoint).Address.Equals(IPAddress.Any) + || ((IPEndPoint)s.LocalEndPoint).Address.Equals(IPAddress.Loopback)); } } else if (fromLocalIpAddress.AddressFamily == AddressFamily.InterNetworkV6) { - sockets = sockets.Where(i => i.LocalIPAddress.Equals(IPAddress.IPv6Any) || fromLocalIpAddress.Equals(i.LocalIPAddress)); + sockets = sockets.Where(s => ((IPEndPoint)s.LocalEndPoint).Address.Equals(IPAddress.IPv6Any) + || ((IPEndPoint)s.LocalEndPoint).Address.Equals(fromLocalIpAddress)); // If sending to the loopback address, filter the socket list as well if (destination.Address.Equals(IPAddress.IPv6Loopback)) { - sockets = sockets.Where(i => i.LocalIPAddress.Equals(IPAddress.IPv6Any) || i.LocalIPAddress.Equals(IPAddress.IPv6Loopback)); + sockets = sockets.Where(s => ((IPEndPoint)s.LocalEndPoint).Address.Equals(IPAddress.IPv6Any) + || ((IPEndPoint)s.LocalEndPoint).Address.Equals(IPAddress.IPv6Loopback)); } } @@ -239,7 +248,7 @@ namespace Rssdp.Infrastructure /// </summary> public async Task SendMulticastMessage(string message, int sendCount, IPAddress fromLocalIpAddress, CancellationToken cancellationToken) { - if (message == null) + if (message is null) { throw new ArgumentNullException(nameof(message)); } @@ -275,7 +284,7 @@ namespace Rssdp.Infrastructure { lock (_SendSocketSynchroniser) { - if (_sendSockets != null) + if (_sendSockets is not null) { var sockets = _sendSockets.ToList(); _sendSockets = null; @@ -284,7 +293,8 @@ namespace Rssdp.Infrastructure foreach (var socket in sockets) { - _logger.LogInformation("{0} disposing sendSocket from {1}", GetType().Name, socket.LocalIPAddress); + var socketAddress = ((IPEndPoint)socket.LocalEndPoint).Address; + _logger.LogInformation("{0} disposing sendSocket from {1}", GetType().Name, socketAddress); socket.Dispose(); } } @@ -312,7 +322,7 @@ namespace Rssdp.Infrastructure { if (disposing) { - StopListeningForBroadcasts(); + StopListeningForMulticast(); StopListeningForResponses(); } @@ -321,11 +331,11 @@ namespace Rssdp.Infrastructure private Task SendMessageIfSocketNotDisposed(byte[] messageData, IPEndPoint destination, IPAddress fromLocalIpAddress, CancellationToken cancellationToken) { var sockets = _sendSockets; - if (sockets != null) + if (sockets is not null) { sockets = sockets.ToList(); - var tasks = sockets.Where(s => (fromLocalIpAddress == null || fromLocalIpAddress.Equals(s.LocalIPAddress))) + var tasks = sockets.Where(s => (fromLocalIpAddress is null || fromLocalIpAddress.Equals(((IPEndPoint)s.LocalEndPoint).Address))) .Select(s => SendFromSocket(s, messageData, destination, cancellationToken)); return Task.WhenAll(tasks); } @@ -333,82 +343,78 @@ namespace Rssdp.Infrastructure return Task.CompletedTask; } - private List<ISocket> ListenForBroadcasts() + private List<Socket> CreateMulticastSocketsAndListen() { - var sockets = new List<ISocket>(); - var nonNullBindAddresses = _networkManager.GetInternalBindAddresses().Where(x => x.Address != null); - + var sockets = new List<Socket>(); + var multicastGroupAddress = IPAddress.Parse(SsdpConstants.MulticastLocalAdminAddress); if (_enableMultiSocketBinding) { - foreach (var address in nonNullBindAddresses) - { - if (address.AddressFamily == AddressFamily.InterNetworkV6) - { - // Not supporting IPv6 right now - continue; - } + // IPv6 is currently unsupported + var validInterfaces = _networkManager.GetInternalBindAddresses() + .Where(x => x.Address is not null) + .Where(x => x.AddressFamily == AddressFamily.InterNetwork) + .DistinctBy(x => x.Index); + foreach (var intf in validInterfaces) + { try { - sockets.Add(_SocketFactory.CreateUdpMulticastSocket(IPAddress.Parse(SsdpConstants.MulticastLocalAdminAddress), address.Address, _MulticastTtl, SsdpConstants.MulticastPort)); + var socket = _SocketFactory.CreateUdpMulticastSocket(multicastGroupAddress, intf, _MulticastTtl, SsdpConstants.MulticastPort); + _ = ListenToSocketInternal(socket); + sockets.Add(socket); } catch (Exception ex) { - _logger.LogError(ex, "Error in ListenForBroadcasts. IPAddress: {0}", address); + _logger.LogError(ex, "Error in CreateMulticastSocketsAndListen. IP address: {0}", intf.Address); } } } else { - sockets.Add(_SocketFactory.CreateUdpMulticastSocket(IPAddress.Parse(SsdpConstants.MulticastLocalAdminAddress), IPAddress.Any, _MulticastTtl, SsdpConstants.MulticastPort)); - } - - foreach (var socket in sockets) - { + var socket = _SocketFactory.CreateUdpMulticastSocket(multicastGroupAddress, new IPData(IPAddress.Any, null), _MulticastTtl, SsdpConstants.MulticastPort); _ = ListenToSocketInternal(socket); + sockets.Add(socket); } return sockets; } - private List<ISocket> CreateSocketAndListenForResponsesAsync() + private List<Socket> CreateSendSockets() { - var sockets = new List<ISocket>(); - + var sockets = new List<Socket>(); if (_enableMultiSocketBinding) { - foreach (var address in _networkManager.GetInternalBindAddresses()) - { - if (address.AddressFamily == AddressFamily.InterNetworkV6) - { - // Not supporting IPv6 right now - continue; - } + // IPv6 is currently unsupported + var validInterfaces = _networkManager.GetInternalBindAddresses() + .Where(x => x.Address is not null) + .Where(x => x.AddressFamily == AddressFamily.InterNetwork); + foreach (var intf in validInterfaces) + { try { - sockets.Add(_SocketFactory.CreateSsdpUdpSocket(address.Address, _LocalPort)); + var socket = _SocketFactory.CreateSsdpUdpSocket(intf, _LocalPort); + _ = ListenToSocketInternal(socket); + sockets.Add(socket); } catch (Exception ex) { - _logger.LogError(ex, "Error in CreateSsdpUdpSocket. IPAddress: {0}", address); + _logger.LogError(ex, "Error in CreateSsdpUdpSocket. IPAddress: {0}", intf.Address); } } } else { - sockets.Add(_SocketFactory.CreateSsdpUdpSocket(IPAddress.Any, _LocalPort)); + var socket = _SocketFactory.CreateSsdpUdpSocket(new IPData(IPAddress.Any, null), _LocalPort); + _ = ListenToSocketInternal(socket); + sockets.Add(socket); } - foreach (var socket in sockets) - { - _ = ListenToSocketInternal(socket); - } return sockets; } - private async Task ListenToSocketInternal(ISocket socket) + private async Task ListenToSocketInternal(Socket socket) { var cancelled = false; var receiveBuffer = new byte[8192]; @@ -417,14 +423,17 @@ namespace Rssdp.Infrastructure { try { - var result = await socket.ReceiveAsync(receiveBuffer, 0, receiveBuffer.Length, CancellationToken.None).ConfigureAwait(false); + var result = await socket.ReceiveMessageFromAsync(receiveBuffer, SocketFlags.None, new IPEndPoint(IPAddress.Any, 0), CancellationToken.None).ConfigureAwait(false);; if (result.ReceivedBytes > 0) { - // Strange cannot convert compiler error here if I don't explicitly - // assign or cast to Action first. Assignment is easier to read, - // so went with that. - ProcessMessage(UTF8Encoding.UTF8.GetString(result.Buffer, 0, result.ReceivedBytes), result.RemoteEndPoint, result.LocalIPAddress); + var remoteEndpoint = (IPEndPoint)result.RemoteEndPoint; + var localEndpointAddress = result.PacketInformation.Address; + + ProcessMessage( + UTF8Encoding.UTF8.GetString(receiveBuffer, 0, result.ReceivedBytes), + remoteEndpoint, + localEndpointAddress); } } catch (ObjectDisposedException) @@ -440,11 +449,11 @@ namespace Rssdp.Infrastructure private void EnsureSendSocketCreated() { - if (_sendSockets == null) + if (_sendSockets is null) { lock (_SendSocketSynchroniser) { - _sendSockets ??= CreateSocketAndListenForResponsesAsync(); + _sendSockets ??= CreateSendSockets(); } } } @@ -455,6 +464,7 @@ namespace Rssdp.Infrastructure // requests start with a method which can vary and might be one we haven't // seen/don't know. We'll check if this message is a request or a response // by checking for the HTTP/ prefix on the start of the message. + _logger.LogDebug("Received data from {From} on {Port} at {Address}:\n{Data}", endPoint.Address, endPoint.Port, receivedOnLocalIpAddress, data); if (data.StartsWith("HTTP/", StringComparison.OrdinalIgnoreCase)) { HttpResponseMessage responseMessage = null; @@ -467,7 +477,7 @@ namespace Rssdp.Infrastructure // Ignore invalid packets. } - if (responseMessage != null) + if (responseMessage is not null) { OnResponseReceived(responseMessage, endPoint, receivedOnLocalIpAddress); } @@ -484,7 +494,7 @@ namespace Rssdp.Infrastructure // Ignore invalid packets. } - if (requestMessage != null) + if (requestMessage is not null) { OnRequestReceived(requestMessage, endPoint, receivedOnLocalIpAddress); } @@ -502,7 +512,7 @@ namespace Rssdp.Infrastructure } var handlers = this.RequestReceived; - if (handlers != null) + if (handlers is not null) { handlers(this, new RequestReceivedEventArgs(data, remoteEndPoint, receivedOnLocalIpAddress)); } @@ -511,7 +521,7 @@ namespace Rssdp.Infrastructure private void OnResponseReceived(HttpResponseMessage data, IPEndPoint endPoint, IPAddress localIpAddress) { var handlers = this.ResponseReceived; - if (handlers != null) + if (handlers is not null) { handlers(this, new ResponseReceivedEventArgs(data, endPoint) { diff --git a/RSSDP/SsdpConstants.cs b/RSSDP/SsdpConstants.cs index 798f050e1c..442f2b8f84 100644 --- a/RSSDP/SsdpConstants.cs +++ b/RSSDP/SsdpConstants.cs @@ -26,6 +26,8 @@ namespace Rssdp.Infrastructure internal const string SsdpDeviceDescriptionXmlNamespace = "urn:schemas-upnp-org:device-1-0"; + internal const string ServerVersion = "1.0"; + /// <summary> /// Default buffer size for receiving SSDP broadcasts. Value is 8192 (bytes). /// </summary> diff --git a/RSSDP/SsdpDeviceLocator.cs b/RSSDP/SsdpDeviceLocator.cs index 681ef0a5c1..25c3b4c4e8 100644 --- a/RSSDP/SsdpDeviceLocator.cs +++ b/RSSDP/SsdpDeviceLocator.cs @@ -1,11 +1,11 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; - namespace Rssdp.Infrastructure { /// <summary> @@ -19,19 +19,48 @@ namespace Rssdp.Infrastructure private Timer _BroadcastTimer; private object _timerLock = new object(); + private string _OSName; + + private string _OSVersion; + private readonly TimeSpan DefaultSearchWaitTime = TimeSpan.FromSeconds(4); private readonly TimeSpan OneSecond = TimeSpan.FromSeconds(1); /// <summary> /// Default constructor. /// </summary> - public SsdpDeviceLocator(ISsdpCommunicationsServer communicationsServer) + public SsdpDeviceLocator( + ISsdpCommunicationsServer communicationsServer, + string osName, + string osVersion) { - if (communicationsServer == null) + if (communicationsServer is null) { throw new ArgumentNullException(nameof(communicationsServer)); } + if (osName is null) + { + throw new ArgumentNullException(nameof(osName)); + } + + if (osName.Length == 0) + { + throw new ArgumentException("osName cannot be an empty string.", nameof(osName)); + } + + if (osVersion is null) + { + throw new ArgumentNullException(nameof(osVersion)); + } + + if (osVersion.Length == 0) + { + throw new ArgumentException("osVersion cannot be an empty string.", nameof(osName)); + } + + _OSName = osName; + _OSVersion = osVersion; _CommunicationsServer = communicationsServer; _CommunicationsServer.ResponseReceived += CommsServer_ResponseReceived; @@ -72,7 +101,7 @@ namespace Rssdp.Infrastructure { lock (_timerLock) { - if (_BroadcastTimer == null) + if (_BroadcastTimer is null) { _BroadcastTimer = new Timer(OnBroadcastTimerCallback, null, dueTime, period); } @@ -87,7 +116,7 @@ namespace Rssdp.Infrastructure { lock (_timerLock) { - if (_BroadcastTimer != null) + if (_BroadcastTimer is not null) { _BroadcastTimer.Dispose(); _BroadcastTimer = null; @@ -148,7 +177,7 @@ namespace Rssdp.Infrastructure private Task SearchAsync(string searchTarget, TimeSpan searchWaitTime, CancellationToken cancellationToken) { - if (searchTarget == null) + if (searchTarget is null) { throw new ArgumentNullException(nameof(searchTarget)); } @@ -187,7 +216,7 @@ namespace Rssdp.Infrastructure { _CommunicationsServer.RequestReceived -= CommsServer_RequestReceived; _CommunicationsServer.RequestReceived += CommsServer_RequestReceived; - _CommunicationsServer.BeginListeningForBroadcasts(); + _CommunicationsServer.BeginListeningForMulticast(); } /// <summary> @@ -219,7 +248,7 @@ namespace Rssdp.Infrastructure } var handlers = this.DeviceAvailable; - if (handlers != null) + if (handlers is not null) { handlers(this, new DeviceAvailableEventArgs(device, isNewDevice) { @@ -242,7 +271,7 @@ namespace Rssdp.Infrastructure } var handlers = this.DeviceUnavailable; - if (handlers != null) + if (handlers is not null) { handlers(this, new DeviceUnavailableEventArgs(device, expired)); } @@ -281,7 +310,7 @@ namespace Rssdp.Infrastructure var commsServer = _CommunicationsServer; _CommunicationsServer = null; - if (commsServer != null) + if (commsServer is not null) { commsServer.ResponseReceived -= this.CommsServer_ResponseReceived; commsServer.RequestReceived -= this.CommsServer_RequestReceived; @@ -295,7 +324,7 @@ namespace Rssdp.Infrastructure lock (_Devices) { var existingDevice = FindExistingDeviceNotification(_Devices, device.NotificationType, device.Usn); - if (existingDevice == null) + if (existingDevice is null) { _Devices.Add(device); isNewDevice = true; @@ -329,12 +358,13 @@ namespace Rssdp.Infrastructure private Task BroadcastDiscoverMessage(string serviceType, TimeSpan mxValue, CancellationToken cancellationToken) { + const string header = "M-SEARCH * HTTP/1.1"; + var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); values["HOST"] = "239.255.255.250:1900"; values["USER-AGENT"] = "UPnP/1.0 DLNADOC/1.50 Platinum/1.0.4.2"; - // values["X-EMBY-SERVERID"] = _appHost.SystemId; - + values["USER-AGENT"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion); values["MAN"] = "\"ssdp:discover\""; // Search target @@ -343,8 +373,6 @@ namespace Rssdp.Infrastructure // Seconds to delay response values["MX"] = "3"; - var header = "M-SEARCH * HTTP/1.1"; - var message = BuildMessage(header, values); return _CommunicationsServer.SendMulticastMessage(message, null, cancellationToken); @@ -358,7 +386,7 @@ namespace Rssdp.Infrastructure } var location = GetFirstHeaderUriValue("Location", message); - if (location != null) + if (location is not null) { var device = new DiscoveredSsdpDevice() { @@ -395,7 +423,7 @@ namespace Rssdp.Infrastructure private void ProcessAliveNotification(HttpRequestMessage message, IPAddress IpAddress) { var location = GetFirstHeaderUriValue("Location", message); - if (location != null) + if (location is not null) { var device = new DiscoveredSsdpDevice() { @@ -445,7 +473,7 @@ namespace Rssdp.Infrastructure if (message.Headers.Contains(headerName)) { message.Headers.TryGetValues(headerName, out values); - if (values != null) + if (values is not null) { retVal = values.FirstOrDefault(); } @@ -461,7 +489,7 @@ namespace Rssdp.Infrastructure if (message.Headers.Contains(headerName)) { message.Headers.TryGetValues(headerName, out values); - if (values != null) + if (values is not null) { retVal = values.FirstOrDefault(); } @@ -477,7 +505,7 @@ namespace Rssdp.Infrastructure if (request.Headers.Contains(headerName)) { request.Headers.TryGetValues(headerName, out values); - if (values != null) + if (values is not null) { value = values.FirstOrDefault(); } @@ -495,7 +523,7 @@ namespace Rssdp.Infrastructure if (response.Headers.Contains(headerName)) { response.Headers.TryGetValues(headerName, out values); - if (values != null) + if (values is not null) { value = values.FirstOrDefault(); } @@ -508,7 +536,7 @@ namespace Rssdp.Infrastructure private TimeSpan CacheAgeFromHeader(System.Net.Http.Headers.CacheControlHeaderValue headerValue) { - if (headerValue == null) + if (headerValue is null) { return TimeSpan.Zero; } @@ -565,7 +593,7 @@ namespace Rssdp.Infrastructure } } - if (existingDevices != null && existingDevices.Count > 0) + if (existingDevices is not null && existingDevices.Count > 0) { foreach (var removedDevice in existingDevices) { diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs index adaac5fa38..40d93b6c0c 100644 --- a/RSSDP/SsdpDevicePublisher.cs +++ b/RSSDP/SsdpDevicePublisher.cs @@ -4,10 +4,9 @@ using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.Net; +using System.Text; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Common.Net; -using Microsoft.AspNetCore.HttpOverrides; namespace Rssdp.Infrastructure { @@ -32,8 +31,6 @@ namespace Rssdp.Infrastructure private Random _Random; - private const string ServerVersion = "1.0"; - /// <summary> /// Default constructor. /// </summary> @@ -43,12 +40,12 @@ namespace Rssdp.Infrastructure string osVersion, bool sendOnlyMatchedHost) { - if (communicationsServer == null) + if (communicationsServer is null) { throw new ArgumentNullException(nameof(communicationsServer)); } - if (osName == null) + if (osName is null) { throw new ArgumentNullException(nameof(osName)); } @@ -58,7 +55,7 @@ namespace Rssdp.Infrastructure throw new ArgumentException("osName cannot be an empty string.", nameof(osName)); } - if (osVersion == null) + if (osVersion is null) { throw new ArgumentNullException(nameof(osVersion)); } @@ -80,10 +77,13 @@ namespace Rssdp.Infrastructure _OSVersion = osVersion; _sendOnlyMatchedHost = sendOnlyMatchedHost; - _CommsServer.BeginListeningForBroadcasts(); + _CommsServer.BeginListeningForMulticast(); + + // Send alive notification once on creation + SendAllAliveNotifications(null); } - public void StartBroadcastingAliveMessages(TimeSpan interval) + public void StartSendingAliveNotifications(TimeSpan interval) { _RebroadcastAliveNotificationsTimer = new Timer(SendAllAliveNotifications, null, TimeSpan.FromSeconds(5), interval); } @@ -99,10 +99,9 @@ namespace Rssdp.Infrastructure /// <param name="device">The <see cref="SsdpDevice"/> instance to add.</param> /// <exception cref="ArgumentNullException">Thrown if the <paramref name="device"/> argument is null.</exception> /// <exception cref="InvalidOperationException">Thrown if the <paramref name="device"/> contains property values that are not acceptable to the UPnP 1.0 specification.</exception> - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1804:RemoveUnusedLocals", MessageId = "t", Justification = "Capture task to local variable suppresses compiler warning, but task is not really needed.")] public void AddDevice(SsdpRootDevice device) { - if (device == null) + if (device is null) { throw new ArgumentNullException(nameof(device)); } @@ -138,7 +137,7 @@ namespace Rssdp.Infrastructure /// <exception cref="ArgumentNullException">Thrown if the <paramref name="device"/> argument is null.</exception> public async Task RemoveDevice(SsdpRootDevice device) { - if (device == null) + if (device is null) { throw new ArgumentNullException(nameof(device)); } @@ -200,7 +199,7 @@ namespace Rssdp.Infrastructure DisposeRebroadcastTimer(); var commsServer = _CommsServer; - if (commsServer != null) + if (commsServer is not null) { commsServer.RequestReceived -= this.CommsServer_RequestReceived; } @@ -209,7 +208,7 @@ namespace Rssdp.Infrastructure Task.WaitAll(tasks); _CommsServer = null; - if (commsServer != null) + if (commsServer is not null) { if (!commsServer.IsShared) { @@ -282,23 +281,23 @@ namespace Rssdp.Infrastructure } else if (searchTarget.Trim().StartsWith("uuid:", StringComparison.OrdinalIgnoreCase)) { - devices = (from device in GetAllDevicesAsFlatEnumerable() where String.Compare(device.Uuid, searchTarget.Substring(5), StringComparison.OrdinalIgnoreCase) == 0 select device).ToArray(); + devices = GetAllDevicesAsFlatEnumerable().Where(d => String.Compare(d.Uuid, searchTarget.Substring(5), StringComparison.OrdinalIgnoreCase) == 0).ToArray(); } else if (searchTarget.StartsWith("urn:", StringComparison.OrdinalIgnoreCase)) { - devices = (from device in GetAllDevicesAsFlatEnumerable() where String.Compare(device.FullDeviceType, searchTarget, StringComparison.OrdinalIgnoreCase) == 0 select device).ToArray(); + devices = GetAllDevicesAsFlatEnumerable().Where(d => String.Compare(d.FullDeviceType, searchTarget, StringComparison.OrdinalIgnoreCase) == 0).ToArray(); } } - if (devices != null) + if (devices is not null) { - var deviceList = devices.ToList(); // WriteTrace(String.Format("Sending {0} search responses", deviceList.Count)); - foreach (var device in deviceList) + foreach (var device in devices) { var root = device.ToRootDevice(); - if (!_sendOnlyMatchedHost || root.Address.Equals(remoteEndPoint.Address)) + + if (!_sendOnlyMatchedHost || root.Address.Equals(receivedOnlocalIpAddress)) { SendDeviceSearchResponses(device, remoteEndPoint, receivedOnlocalIpAddress, cancellationToken); } @@ -318,7 +317,7 @@ namespace Rssdp.Infrastructure IPAddress receivedOnlocalIpAddress, CancellationToken cancellationToken) { - bool isRootDevice = (device as SsdpRootDevice) != null; + bool isRootDevice = (device as SsdpRootDevice) is not null; if (isRootDevice) { SendSearchResponse(SsdpConstants.UpnpDeviceTypeRootDevice, device, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), endPoint, receivedOnlocalIpAddress, cancellationToken); @@ -346,19 +345,17 @@ namespace Rssdp.Infrastructure IPAddress receivedOnlocalIpAddress, CancellationToken cancellationToken) { - var rootDevice = device.ToRootDevice(); - - // var additionalheaders = FormatCustomHeadersForResponse(device); - const string header = "HTTP/1.1 200 OK"; + var rootDevice = device.ToRootDevice(); var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); values["EXT"] = ""; values["DATE"] = DateTime.UtcNow.ToString("r"); + values["HOST"] = "239.255.255.250:1900"; values["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds; values["ST"] = searchTarget; - values["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, ServerVersion); + values["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion); values["USN"] = uniqueServiceName; values["LOCATION"] = rootDevice.Location.ToString(); @@ -367,7 +364,7 @@ namespace Rssdp.Infrastructure try { await _CommsServer.SendMessage( - System.Text.Encoding.UTF8.GetBytes(message), + Encoding.UTF8.GetBytes(message), endPoint, receivedOnlocalIpAddress, cancellationToken) @@ -492,7 +489,7 @@ namespace Rssdp.Infrastructure values["DATE"] = DateTime.UtcNow.ToString("r"); values["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds; values["LOCATION"] = rootDevice.Location.ToString(); - values["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, ServerVersion); + values["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion); values["NTS"] = "ssdp:alive"; values["NT"] = notificationType; values["USN"] = uniqueServiceName; @@ -527,7 +524,6 @@ namespace Rssdp.Infrastructure return Task.WhenAll(tasks); } - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "byebye", Justification = "Correct value for this type of notification in SSDP.")] private Task SendByeByeNotification(SsdpDevice device, string notificationType, string uniqueServiceName, CancellationToken cancellationToken) { const string header = "NOTIFY * HTTP/1.1"; @@ -537,7 +533,7 @@ namespace Rssdp.Infrastructure // If needed later for non-server devices, these headers will need to be dynamic values["HOST"] = "239.255.255.250:1900"; values["DATE"] = DateTime.UtcNow.ToString("r"); - values["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, ServerVersion); + values["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion); values["NTS"] = "ssdp:byebye"; values["NT"] = notificationType; values["USN"] = uniqueServiceName; @@ -553,7 +549,7 @@ namespace Rssdp.Infrastructure { var timer = _RebroadcastAliveNotificationsTimer; _RebroadcastAliveNotificationsTimer = null; - if (timer != null) + if (timer is not null) { timer.Dispose(); } @@ -581,7 +577,7 @@ namespace Rssdp.Infrastructure { string retVal = null; IEnumerable<String> values = null; - if (httpRequestHeaders.TryGetValues(headerName, out values) && values != null) + if (httpRequestHeaders.TryGetValues(headerName, out values) && values is not null) { retVal = values.FirstOrDefault(); } @@ -593,7 +589,7 @@ namespace Rssdp.Infrastructure private void WriteTrace(string text) { - if (LogFunction != null) + if (LogFunction is not null) { LogFunction(text); } @@ -603,7 +599,7 @@ namespace Rssdp.Infrastructure private void WriteTrace(string text, SsdpDevice device) { var rootDevice = device as SsdpRootDevice; - if (rootDevice != null) + if (rootDevice is not null) { WriteTrace(text + " " + device.DeviceType + " - " + device.Uuid + " - " + rootDevice.Location); } diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs index c493ce5ea8..10706e9c21 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs @@ -6,6 +6,7 @@ using Jellyfin.Networking.Configuration; using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; +using MediaBrowser.Model.Net; using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; @@ -210,7 +211,7 @@ namespace Jellyfin.Networking.Tests if (resultObj is not null && host.Length > 0) { result = resultObj.First().Address.ToString(); - var intf = nm.GetBindInterface(source, out _); + var intf = nm.GetBindAddress(source, out _); Assert.Equal(intf, result); } @@ -271,7 +272,7 @@ namespace Jellyfin.Networking.Tests result = resultObj.First().Address.ToString(); } - var intf = nm.GetBindInterface(source, out int? _); + var intf = nm.GetBindAddress(source, out int? _); Assert.Equal(result, intf); } @@ -334,7 +335,7 @@ namespace Jellyfin.Networking.Tests NetworkManager.MockNetworkSettings = interfaces; using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); - var interfaceToUse = nm.GetBindInterface(string.Empty, out _); + var interfaceToUse = nm.GetBindAddress(string.Empty, out _); Assert.Equal(result, interfaceToUse); } @@ -358,7 +359,7 @@ namespace Jellyfin.Networking.Tests NetworkManager.MockNetworkSettings = interfaces; using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); - var interfaceToUse = nm.GetBindInterface(source, out _); + var interfaceToUse = nm.GetBindAddress(source, out _); Assert.Equal(result, interfaceToUse); } From bedee7922f3be0cd5d1f4f687e9766c1217e39e7 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Fri, 17 Feb 2023 18:24:13 +0100 Subject: [PATCH 079/858] Fix interface address assignment and resolution in SSDP --- Emby.Server.Implementations/Net/SocketFactory.cs | 5 +++-- RSSDP/SsdpCommunicationsServer.cs | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Emby.Server.Implementations/Net/SocketFactory.cs b/Emby.Server.Implementations/Net/SocketFactory.cs index d134d948ab..51e92953df 100644 --- a/Emby.Server.Implementations/Net/SocketFactory.cs +++ b/Emby.Server.Implementations/Net/SocketFactory.cs @@ -82,13 +82,14 @@ namespace Emby.Server.Implementations.Net try { - var interfaceIndex = (int)IPAddress.HostToNetworkOrder(bindInterface.Index); + var interfaceIndex = bindInterface.Index; + var interfaceIndexSwapped = (int)IPAddress.HostToNetworkOrder(interfaceIndex); socket.MulticastLoopback = false; socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.PacketInformation, true); socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, multicastTimeToLive); - socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastInterface, interfaceIndex); + socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastInterface, interfaceIndexSwapped); socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress, interfaceIndex)); socket.Bind(new IPEndPoint(multicastAddress, localPort)); diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs index 6ae260d557..f70a598fbe 100644 --- a/RSSDP/SsdpCommunicationsServer.cs +++ b/RSSDP/SsdpCommunicationsServer.cs @@ -428,12 +428,12 @@ namespace Rssdp.Infrastructure if (result.ReceivedBytes > 0) { var remoteEndpoint = (IPEndPoint)result.RemoteEndPoint; - var localEndpointAddress = result.PacketInformation.Address; + var localEndpointAdapter = _networkManager.GetAllBindInterfaces().Where(a => a.Index == result.PacketInformation.Interface).First(); ProcessMessage( UTF8Encoding.UTF8.GetString(receiveBuffer, 0, result.ReceivedBytes), remoteEndpoint, - localEndpointAddress); + localEndpointAdapter.Address); } } catch (ObjectDisposedException) From 20fd05b05081ad387e94128b4f26d907808c8f0c Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Fri, 17 Feb 2023 19:27:36 +0100 Subject: [PATCH 080/858] Consistently write IP in upercase --- Emby.Dlna/PlayTo/PlayToManager.cs | 2 +- Emby.Dlna/Ssdp/DeviceDiscovery.cs | 2 +- .../HttpServer/WebSocketManager.cs | 2 +- .../TunerHosts/HdHomerun/HdHomerunHost.cs | 4 +- .../TunerHosts/HdHomerun/HdHomerunManager.cs | 10 ++--- .../AnonymousLanAccessHandler.cs | 2 +- .../DefaultAuthorizationHandler.cs | 2 +- .../LocalAccessOrRequiresElevationHandler.cs | 2 +- .../Controllers/MediaInfoController.cs | 2 +- Jellyfin.Api/Controllers/SystemController.cs | 2 +- .../Controllers/UniversalAudioController.cs | 2 +- Jellyfin.Api/Controllers/UserController.cs | 18 ++++---- Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 2 +- Jellyfin.Api/Helpers/MediaInfoHelper.cs | 4 +- Jellyfin.Api/Helpers/RequestHelpers.cs | 2 +- .../IpBasedAccessValidationMiddleware.cs | 10 ++--- .../Middleware/LanFilteringMiddleware.cs | 2 +- .../Middleware/ResponseTimeMiddleware.cs | 4 +- Jellyfin.Networking/Manager/NetworkManager.cs | 26 +++++------ .../ApiApplicationBuilderExtensions.cs | 4 +- .../ApiServiceCollectionExtensions.cs | 8 ++-- Jellyfin.Server/Startup.cs | 2 +- .../Extensions/HttpContextExtensions.cs | 2 +- MediaBrowser.Common/Net/INetworkManager.cs | 6 +-- MediaBrowser.Common/Net/NetworkExtensions.cs | 20 ++++----- MediaBrowser.Model/Dlna/UpnpDeviceInfo.cs | 4 +- RSSDP/DeviceAvailableEventArgs.cs | 2 +- RSSDP/ISsdpCommunicationsServer.cs | 6 +-- RSSDP/RequestReceivedEventArgs.cs | 6 +-- RSSDP/ResponseReceivedEventArgs.cs | 2 +- RSSDP/SsdpCommunicationsServer.cs | 44 +++++++++---------- RSSDP/SsdpDeviceLocator.cs | 26 +++++------ RSSDP/SsdpDevicePublisher.cs | 22 +++++----- .../NetworkParseTests.cs | 10 ++--- 34 files changed, 132 insertions(+), 132 deletions(-) diff --git a/Emby.Dlna/PlayTo/PlayToManager.cs b/Emby.Dlna/PlayTo/PlayToManager.cs index f4a9a90af4..b9bfad9d9f 100644 --- a/Emby.Dlna/PlayTo/PlayToManager.cs +++ b/Emby.Dlna/PlayTo/PlayToManager.cs @@ -189,7 +189,7 @@ namespace Emby.Dlna.PlayTo _sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName); - string serverAddress = _appHost.GetSmartApiUrl(info.RemoteIpAddress); + string serverAddress = _appHost.GetSmartApiUrl(info.RemoteIPAddress); controller = new PlayToController( sessionInfo, diff --git a/Emby.Dlna/Ssdp/DeviceDiscovery.cs b/Emby.Dlna/Ssdp/DeviceDiscovery.cs index 43d673c772..4fbbc38859 100644 --- a/Emby.Dlna/Ssdp/DeviceDiscovery.cs +++ b/Emby.Dlna/Ssdp/DeviceDiscovery.cs @@ -110,7 +110,7 @@ namespace Emby.Dlna.Ssdp { Location = e.DiscoveredDevice.DescriptionLocation, Headers = headers, - RemoteIpAddress = e.RemoteIpAddress + RemoteIPAddress = e.RemoteIPAddress }); DeviceDiscoveredInternal?.Invoke(this, args); diff --git a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs index 4f7d1c40a6..ecfb242f6f 100644 --- a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs +++ b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs @@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.HttpServer using var connection = new WebSocketConnection( _loggerFactory.CreateLogger<WebSocketConnection>(), webSocket, - context.GetNormalizedRemoteIp()) + context.GetNormalizedRemoteIP()) { OnReceive = ProcessWebSocketMessageReceived }; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index e76961ce9e..a86c329d62 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -667,12 +667,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun while (!cancellationToken.IsCancellationRequested) { var response = await udpClient.ReceiveMessageFromAsync(receiveBuffer, new IPEndPoint(IPAddress.Any, 0), cancellationToken).ConfigureAwait(false); - var deviceIp = ((IPEndPoint)response.RemoteEndPoint).Address.ToString(); + var deviceIP = ((IPEndPoint)response.RemoteEndPoint).Address.ToString(); // Check to make sure we have enough bytes received to be a valid message and make sure the 2nd byte is the discover reply byte if (response.ReceivedBytes > 13 && receiveBuffer[1] == 3) { - var deviceAddress = "http://" + deviceIp; + var deviceAddress = "http://" + deviceIP; var info = await TryGetTunerHostInfo(deviceAddress, cancellationToken).ConfigureAwait(false); diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs index 81eb083f6f..ae7df22f8e 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs @@ -48,10 +48,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun GC.SuppressFinalize(this); } - public async Task<bool> CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken) + public async Task<bool> CheckTunerAvailability(IPAddress remoteIP, int tuner, CancellationToken cancellationToken) { using var client = new TcpClient(); - await client.ConnectAsync(remoteIp, HdHomeRunPort).ConfigureAwait(false); + await client.ConnectAsync(remoteIP, HdHomeRunPort).ConfigureAwait(false); using var stream = client.GetStream(); return await CheckTunerAvailability(stream, tuner, cancellationToken).ConfigureAwait(false); @@ -75,9 +75,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun } } - public async Task StartStreaming(IPAddress remoteIp, IPAddress localIp, int localPort, IHdHomerunChannelCommands commands, int numTuners, CancellationToken cancellationToken) + public async Task StartStreaming(IPAddress remoteIP, IPAddress localIP, int localPort, IHdHomerunChannelCommands commands, int numTuners, CancellationToken cancellationToken) { - _remoteEndPoint = new IPEndPoint(remoteIp, HdHomeRunPort); + _remoteEndPoint = new IPEndPoint(remoteIP, HdHomeRunPort); _tcpClient = new TcpClient(); await _tcpClient.ConnectAsync(_remoteEndPoint, cancellationToken).ConfigureAwait(false); @@ -125,7 +125,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun } } - var targetValue = string.Format(CultureInfo.InvariantCulture, "rtp://{0}:{1}", localIp, localPort); + var targetValue = string.Format(CultureInfo.InvariantCulture, "rtp://{0}:{1}", localIP, localPort); var targetMsgLen = WriteSetMessage(buffer, i, "target", targetValue, lockKeyValue); await stream.WriteAsync(buffer.AsMemory(0, targetMsgLen), cancellationToken).ConfigureAwait(false); diff --git a/Jellyfin.Api/Auth/AnonymousLanAccessPolicy/AnonymousLanAccessHandler.cs b/Jellyfin.Api/Auth/AnonymousLanAccessPolicy/AnonymousLanAccessHandler.cs index 741b88ea95..3c1401dedc 100644 --- a/Jellyfin.Api/Auth/AnonymousLanAccessPolicy/AnonymousLanAccessHandler.cs +++ b/Jellyfin.Api/Auth/AnonymousLanAccessPolicy/AnonymousLanAccessHandler.cs @@ -30,7 +30,7 @@ namespace Jellyfin.Api.Auth.AnonymousLanAccessPolicy /// <inheritdoc /> protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AnonymousLanAccessRequirement requirement) { - var ip = _httpContextAccessor.HttpContext?.GetNormalizedRemoteIp(); + var ip = _httpContextAccessor.HttpContext?.GetNormalizedRemoteIP(); // Loopback will be on LAN, so we can accept null. if (ip is null || _networkManager.IsInLocalNetwork(ip)) diff --git a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs index b1d97e4a1d..c0db4d1fc2 100644 --- a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs +++ b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs @@ -47,7 +47,7 @@ namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy } var isInLocalNetwork = _httpContextAccessor.HttpContext is not null - && _networkManager.IsInLocalNetwork(_httpContextAccessor.HttpContext.GetNormalizedRemoteIp()); + && _networkManager.IsInLocalNetwork(_httpContextAccessor.HttpContext.GetNormalizedRemoteIP()); var user = _userManager.GetUserById(userId); if (user is null) { diff --git a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs index 6ed6fc90be..557b7d3aa4 100644 --- a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs +++ b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs @@ -31,7 +31,7 @@ namespace Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy /// <inheritdoc /> protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalAccessOrRequiresElevationRequirement requirement) { - var ip = _httpContextAccessor.HttpContext?.GetNormalizedRemoteIp(); + var ip = _httpContextAccessor.HttpContext?.GetNormalizedRemoteIP(); // Loopback will be on LAN, so we can accept null. if (ip is null || _networkManager.IsInLocalNetwork(ip)) diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs index ea10dd771f..1bef35c274 100644 --- a/Jellyfin.Api/Controllers/MediaInfoController.cs +++ b/Jellyfin.Api/Controllers/MediaInfoController.cs @@ -183,7 +183,7 @@ public class MediaInfoController : BaseJellyfinApiController enableTranscoding.Value, allowVideoStreamCopy.Value, allowAudioStreamCopy.Value, - Request.HttpContext.GetNormalizedRemoteIp()); + Request.HttpContext.GetNormalizedRemoteIP()); } _mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate); diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index 4ab705f40a..91901518fa 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -179,7 +179,7 @@ public class SystemController : BaseJellyfinApiController return new EndPointInfo { IsLocal = HttpContext.IsLocal(), - IsInNetwork = _network.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIp()) + IsInNetwork = _network.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIP()) }; } diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs index 3455215979..04f2109eaa 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -143,7 +143,7 @@ public class UniversalAudioController : BaseJellyfinApiController true, true, true, - Request.HttpContext.GetNormalizedRemoteIp()); + Request.HttpContext.GetNormalizedRemoteIP()); } _mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate); diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index b0973b8a14..ec54255780 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -129,7 +129,7 @@ public class UserController : BaseJellyfinApiController return NotFound("User not found"); } - var result = _userManager.GetUserDto(user, HttpContext.GetNormalizedRemoteIp().ToString()); + var result = _userManager.GetUserDto(user, HttpContext.GetNormalizedRemoteIP().ToString()); return result; } @@ -211,7 +211,7 @@ public class UserController : BaseJellyfinApiController DeviceId = auth.DeviceId, DeviceName = auth.Device, Password = request.Pw, - RemoteEndPoint = HttpContext.GetNormalizedRemoteIp().ToString(), + RemoteEndPoint = HttpContext.GetNormalizedRemoteIP().ToString(), Username = request.Username }).ConfigureAwait(false); @@ -220,7 +220,7 @@ public class UserController : BaseJellyfinApiController catch (SecurityException e) { // rethrow adding IP address to message - throw new SecurityException($"[{HttpContext.GetNormalizedRemoteIp()}] {e.Message}", e); + throw new SecurityException($"[{HttpContext.GetNormalizedRemoteIP()}] {e.Message}", e); } } @@ -242,7 +242,7 @@ public class UserController : BaseJellyfinApiController catch (SecurityException e) { // rethrow adding IP address to message - throw new SecurityException($"[{HttpContext.GetNormalizedRemoteIp()}] {e.Message}", e); + throw new SecurityException($"[{HttpContext.GetNormalizedRemoteIP()}] {e.Message}", e); } } @@ -288,7 +288,7 @@ public class UserController : BaseJellyfinApiController user.Username, request.CurrentPw ?? string.Empty, request.CurrentPw ?? string.Empty, - HttpContext.GetNormalizedRemoteIp().ToString(), + HttpContext.GetNormalizedRemoteIP().ToString(), false).ConfigureAwait(false); if (success is null) @@ -489,7 +489,7 @@ public class UserController : BaseJellyfinApiController await _userManager.ChangePassword(newUser, request.Password).ConfigureAwait(false); } - var result = _userManager.GetUserDto(newUser, HttpContext.GetNormalizedRemoteIp().ToString()); + var result = _userManager.GetUserDto(newUser, HttpContext.GetNormalizedRemoteIP().ToString()); return result; } @@ -504,7 +504,7 @@ public class UserController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] public async Task<ActionResult<ForgotPasswordResult>> ForgotPassword([FromBody, Required] ForgotPasswordDto forgotPasswordRequest) { - var ip = HttpContext.GetNormalizedRemoteIp(); + var ip = HttpContext.GetNormalizedRemoteIP(); var isLocal = HttpContext.IsLocal() || _networkManager.IsInLocalNetwork(ip); @@ -585,7 +585,7 @@ public class UserController : BaseJellyfinApiController if (filterByNetwork) { - if (!_networkManager.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIp())) + if (!_networkManager.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIP())) { users = users.Where(i => i.HasPermission(PermissionKind.EnableRemoteAccess)); } @@ -593,7 +593,7 @@ public class UserController : BaseJellyfinApiController var result = users .OrderBy(u => u.Username) - .Select(i => _userManager.GetUserDto(i, HttpContext.GetNormalizedRemoteIp().ToString())); + .Select(i => _userManager.GetUserDto(i, HttpContext.GetNormalizedRemoteIP().ToString())); return result; } diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index 245239233c..f9ca392099 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -273,7 +273,7 @@ public class DynamicHlsHelper } } - if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming, _httpContextAccessor.HttpContext.GetNormalizedRemoteIp())) + if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming, _httpContextAccessor.HttpContext.GetNormalizedRemoteIP())) { var requestedVideoBitrate = state.VideoRequest is null ? 0 : state.VideoRequest.VideoBitRate ?? 0; diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs index 5910d80737..a36028cfeb 100644 --- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs +++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs @@ -421,7 +421,7 @@ public class MediaInfoHelper true, true, true, - httpContext.GetNormalizedRemoteIp()); + httpContext.GetNormalizedRemoteIP()); } else { @@ -487,7 +487,7 @@ public class MediaInfoHelper { var isInLocalNetwork = _networkManager.IsInLocalNetwork(ipAddress); - _logger.LogInformation("RemoteClientBitrateLimit: {0}, RemoteIp: {1}, IsInLocalNetwork: {2}", remoteClientMaxBitrate, ipAddress, isInLocalNetwork); + _logger.LogInformation("RemoteClientBitrateLimit: {0}, RemoteIP: {1}, IsInLocalNetwork: {2}", remoteClientMaxBitrate, ipAddress, isInLocalNetwork); if (!isInLocalNetwork) { maxBitrate = Math.Min(maxBitrate ?? remoteClientMaxBitrate, remoteClientMaxBitrate); diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs index 0b7a4fa1ac..1ab55bc312 100644 --- a/Jellyfin.Api/Helpers/RequestHelpers.cs +++ b/Jellyfin.Api/Helpers/RequestHelpers.cs @@ -98,7 +98,7 @@ public static class RequestHelpers httpContext.User.GetVersion(), httpContext.User.GetDeviceId(), httpContext.User.GetDevice(), - httpContext.GetNormalizedRemoteIp().ToString(), + httpContext.GetNormalizedRemoteIP().ToString(), user).ConfigureAwait(false); if (session is null) diff --git a/Jellyfin.Api/Middleware/IpBasedAccessValidationMiddleware.cs b/Jellyfin.Api/Middleware/IpBasedAccessValidationMiddleware.cs index f45b6b5c0a..27bcd5570c 100644 --- a/Jellyfin.Api/Middleware/IpBasedAccessValidationMiddleware.cs +++ b/Jellyfin.Api/Middleware/IpBasedAccessValidationMiddleware.cs @@ -9,15 +9,15 @@ namespace Jellyfin.Api.Middleware; /// <summary> /// Validates the IP of requests coming from local networks wrt. remote access. /// </summary> -public class IpBasedAccessValidationMiddleware +public class IPBasedAccessValidationMiddleware { private readonly RequestDelegate _next; /// <summary> - /// Initializes a new instance of the <see cref="IpBasedAccessValidationMiddleware"/> class. + /// Initializes a new instance of the <see cref="IPBasedAccessValidationMiddleware"/> class. /// </summary> /// <param name="next">The next delegate in the pipeline.</param> - public IpBasedAccessValidationMiddleware(RequestDelegate next) + public IPBasedAccessValidationMiddleware(RequestDelegate next) { _next = next; } @@ -37,9 +37,9 @@ public class IpBasedAccessValidationMiddleware return; } - var remoteIp = httpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback; + var remoteIP = httpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback; - if (!networkManager.HasRemoteAccess(remoteIp)) + if (!networkManager.HasRemoteAccess(remoteIP)) { return; } diff --git a/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs b/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs index 9c2194fafd..94de30d1b1 100644 --- a/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs +++ b/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs @@ -38,7 +38,7 @@ public class LanFilteringMiddleware return; } - var host = httpContext.GetNormalizedRemoteIp(); + var host = httpContext.GetNormalizedRemoteIP(); if (!networkManager.IsInLocalNetwork(host)) { return; diff --git a/Jellyfin.Api/Middleware/ResponseTimeMiddleware.cs b/Jellyfin.Api/Middleware/ResponseTimeMiddleware.cs index db39177436..279ea70d80 100644 --- a/Jellyfin.Api/Middleware/ResponseTimeMiddleware.cs +++ b/Jellyfin.Api/Middleware/ResponseTimeMiddleware.cs @@ -51,9 +51,9 @@ public class ResponseTimeMiddleware if (enableWarning && responseTimeMs > warningThreshold && _logger.IsEnabled(LogLevel.Debug)) { _logger.LogDebug( - "Slow HTTP Response from {Url} to {RemoteIp} in {Elapsed:g} with Status Code {StatusCode}", + "Slow HTTP Response from {Url} to {RemoteIP} in {Elapsed:g} with Status Code {StatusCode}", context.Request.GetDisplayUrl(), - context.GetNormalizedRemoteIp(), + context.GetNormalizedRemoteIP(), responseTime, context.Response.StatusCode); } diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index cdd34bc896..dd90a5b516 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -122,7 +122,7 @@ namespace Jellyfin.Networking.Manager /// <summary> /// Gets a value indicating whether is all IPv6 interfaces are trusted as internal. /// </summary> - public bool TrustAllIpv6Interfaces { get; private set; } + public bool TrustAllIPv6Interfaces { get; private set; } /// <summary> /// Gets the Published server override list. @@ -596,17 +596,17 @@ namespace Jellyfin.Networking.Manager } /// <inheritdoc/> - public bool HasRemoteAccess(IPAddress remoteIp) + public bool HasRemoteAccess(IPAddress remoteIP) { var config = _configurationManager.GetNetworkConfiguration(); if (config.EnableRemoteAccess) { // 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. - if (_remoteAddressFilter.Any() && !_lanSubnets.Any(x => x.Contains(remoteIp))) + if (_remoteAddressFilter.Any() && !_lanSubnets.Any(x => x.Contains(remoteIP))) { // remoteAddressFilter is a whitelist or blacklist. - var matches = _remoteAddressFilter.Count(remoteNetwork => remoteNetwork.Contains(remoteIp)); + var matches = _remoteAddressFilter.Count(remoteNetwork => remoteNetwork.Contains(remoteIP)); if ((!config.IsRemoteIPFilterBlacklist && matches > 0) || (config.IsRemoteIPFilterBlacklist && matches == 0)) { @@ -616,7 +616,7 @@ namespace Jellyfin.Networking.Manager return false; } } - else if (!_lanSubnets.Any(x => x.Contains(remoteIp))) + else if (!_lanSubnets.Any(x => x.Contains(remoteIP))) { // Remote not enabled. So everyone should be LAN. return false; @@ -771,7 +771,7 @@ namespace Jellyfin.Networking.Manager // If no source address is given, use the preferred (first) interface if (source is null) { - result = NetworkExtensions.FormatIpString(availableInterfaces.First().Address); + result = NetworkExtensions.FormatIPString(availableInterfaces.First().Address); _logger.LogDebug("{Source}: Using first internal interface as bind address: {Result}", source, result); return result; } @@ -782,14 +782,14 @@ namespace Jellyfin.Networking.Manager { if (intf.Subnet.Contains(source)) { - result = NetworkExtensions.FormatIpString(intf.Address); + result = NetworkExtensions.FormatIPString(intf.Address); _logger.LogDebug("{Source}: Found interface with matching subnet, using it as bind address: {Result}", source, result); return result; } } // Fallback to first available interface - result = NetworkExtensions.FormatIpString(availableInterfaces[0].Address); + result = NetworkExtensions.FormatIPString(availableInterfaces[0].Address); _logger.LogDebug("{Source}: No matching interfaces found, using preferred interface as bind address: {Result}", source, result); return result; } @@ -842,7 +842,7 @@ namespace Jellyfin.Networking.Manager ArgumentNullException.ThrowIfNull(address); // See conversation at https://github.com/jellyfin/jellyfin/pull/3515. - if ((TrustAllIpv6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6) + if ((TrustAllIPv6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6) || address.Equals(IPAddress.Loopback) || address.Equals(IPAddress.IPv6Loopback)) { @@ -995,7 +995,7 @@ namespace Jellyfin.Networking.Manager if (bindAddress is not null) { - result = NetworkExtensions.FormatIpString(bindAddress); + result = NetworkExtensions.FormatIPString(bindAddress); _logger.LogDebug("{Source}: External request received, matching external bind address found: {Result}", source, result); return true; } @@ -1015,7 +1015,7 @@ namespace Jellyfin.Networking.Manager if (bindAddress is not null) { - result = NetworkExtensions.FormatIpString(bindAddress); + result = NetworkExtensions.FormatIPString(bindAddress); _logger.LogDebug("{Source}: Internal request received, matching internal bind address found: {Result}", source, result); return true; } @@ -1049,14 +1049,14 @@ namespace Jellyfin.Networking.Manager { if (!IsInLocalNetwork(intf.Address) && intf.Subnet.Contains(source)) { - result = NetworkExtensions.FormatIpString(intf.Address); + result = NetworkExtensions.FormatIPString(intf.Address); _logger.LogDebug("{Source}: Found external interface with matching subnet, using it as bind address: {Result}", source, result); return true; } } // Fallback to first external interface. - result = NetworkExtensions.FormatIpString(extResult.First().Address); + result = NetworkExtensions.FormatIPString(extResult.First().Address); _logger.LogDebug("{Source}: Using first external interface as bind address: {Result}", source, result); return true; } diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs index 463ca7321d..b6af9baec3 100644 --- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs @@ -63,9 +63,9 @@ namespace Jellyfin.Server.Extensions /// </summary> /// <param name="appBuilder">The application builder.</param> /// <returns>The updated application builder.</returns> - public static IApplicationBuilder UseIpBasedAccessValidation(this IApplicationBuilder appBuilder) + public static IApplicationBuilder UseIPBasedAccessValidation(this IApplicationBuilder appBuilder) { - return appBuilder.UseMiddleware<IpBasedAccessValidationMiddleware>(); + return appBuilder.UseMiddleware<IPBasedAccessValidationMiddleware>(); } /// <summary> diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 1dfbb89fa0..ea3c92011f 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -271,26 +271,26 @@ namespace Jellyfin.Server.Extensions { if (IPAddress.TryParse(allowedProxies[i], out var addr)) { - AddIpAddress(config, options, addr, addr.AddressFamily == AddressFamily.InterNetwork ? 32 : 128); + AddIPAddress(config, options, addr, addr.AddressFamily == AddressFamily.InterNetwork ? 32 : 128); } else if (NetworkExtensions.TryParseToSubnet(allowedProxies[i], out var subnet)) { if (subnet != null) { - AddIpAddress(config, options, subnet.Prefix, subnet.PrefixLength); + AddIPAddress(config, options, subnet.Prefix, subnet.PrefixLength); } } else if (NetworkExtensions.TryParseHost(allowedProxies[i], out var addresses)) { foreach (var address in addresses) { - AddIpAddress(config, options, address, address.AddressFamily == AddressFamily.InterNetwork ? 32 : 128); + AddIPAddress(config, options, address, address.AddressFamily == AddressFamily.InterNetwork ? 32 : 128); } } } } - private static void AddIpAddress(NetworkConfiguration config, ForwardedHeadersOptions options, IPAddress addr, int prefixLength) + private static void AddIPAddress(NetworkConfiguration config, ForwardedHeadersOptions options, IPAddress addr, int prefixLength) { if ((!config.EnableIPv4 && addr.AddressFamily == AddressFamily.InterNetwork) || (!config.EnableIPv6 && addr.AddressFamily == AddressFamily.InterNetworkV6)) { diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 155f9fc8c1..4afaf12179 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -190,7 +190,7 @@ namespace Jellyfin.Server mainApp.UseAuthorization(); mainApp.UseLanFiltering(); - mainApp.UseIpBasedAccessValidation(); + mainApp.UseIPBasedAccessValidation(); mainApp.UseWebSocketHandler(); mainApp.UseServerStartupMessage(); diff --git a/MediaBrowser.Common/Extensions/HttpContextExtensions.cs b/MediaBrowser.Common/Extensions/HttpContextExtensions.cs index 6608704c0a..a1056b7c84 100644 --- a/MediaBrowser.Common/Extensions/HttpContextExtensions.cs +++ b/MediaBrowser.Common/Extensions/HttpContextExtensions.cs @@ -25,7 +25,7 @@ namespace MediaBrowser.Common.Extensions /// </summary> /// <param name="context">The HTTP context.</param> /// <returns>The remote caller IP address.</returns> - public static IPAddress GetNormalizedRemoteIp(this HttpContext context) + public static IPAddress GetNormalizedRemoteIP(this HttpContext context) { // Default to the loopback address if no RemoteIpAddress is specified (i.e. during integration tests) var ip = context.Connection.RemoteIpAddress ?? IPAddress.Loopback; diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs index 68974f738d..efd87a8107 100644 --- a/MediaBrowser.Common/Net/INetworkManager.cs +++ b/MediaBrowser.Common/Net/INetworkManager.cs @@ -130,10 +130,10 @@ namespace MediaBrowser.Common.Net IReadOnlyList<IPData> GetInternalBindAddresses(); /// <summary> - /// Checks if <paramref name="remoteIp"/> has access to the server. + /// Checks if <paramref name="remoteIP"/> has access to the server. /// </summary> - /// <param name="remoteIp">IP address of the client.</param> + /// <param name="remoteIP">IP address of the client.</param> /// <returns><b>True</b> if it has access, otherwise <b>false</b>.</returns> - bool HasRemoteAccess(IPAddress remoteIp); + bool HasRemoteAccess(IPAddress remoteIP); } } diff --git a/MediaBrowser.Common/Net/NetworkExtensions.cs b/MediaBrowser.Common/Net/NetworkExtensions.cs index 7c36081e60..cef4a5d965 100644 --- a/MediaBrowser.Common/Net/NetworkExtensions.cs +++ b/MediaBrowser.Common/Net/NetworkExtensions.cs @@ -126,11 +126,11 @@ namespace MediaBrowser.Common.Net /// <summary> /// Converts an IPAddress into a string. - /// Ipv6 addresses are returned in [ ], with their scope removed. + /// IPv6 addresses are returned in [ ], with their scope removed. /// </summary> /// <param name="address">Address to convert.</param> /// <returns>URI safe conversion of the address.</returns> - public static string FormatIpString(IPAddress? address) + public static string FormatIPString(IPAddress? address) { if (address is null) { @@ -252,10 +252,10 @@ namespace MediaBrowser.Common.Net /// </summary> /// <param name="host">Host name to parse.</param> /// <param name="addresses">Object representing the string, if it has successfully been parsed.</param> - /// <param name="isIpv4Enabled"><c>true</c> if IPv4 is enabled.</param> - /// <param name="isIpv6Enabled"><c>true</c> if IPv6 is enabled.</param> + /// <param name="isIPv4Enabled"><c>true</c> if IPv4 is enabled.</param> + /// <param name="isIPv6Enabled"><c>true</c> if IPv6 is enabled.</param> /// <returns><c>true</c> if the parsing is successful, <c>false</c> if not.</returns> - public static bool TryParseHost(string host, [NotNullWhen(true)] out IPAddress[] addresses, bool isIpv4Enabled = true, bool isIpv6Enabled = false) + public static bool TryParseHost(string host, [NotNullWhen(true)] out IPAddress[] addresses, bool isIPv4Enabled = true, bool isIPv6Enabled = false) { if (string.IsNullOrWhiteSpace(host)) { @@ -302,8 +302,8 @@ namespace MediaBrowser.Common.Net if (IPAddress.TryParse(host, out var address)) { - if (((address.AddressFamily == AddressFamily.InterNetwork) && (!isIpv4Enabled && isIpv6Enabled)) || - ((address.AddressFamily == AddressFamily.InterNetworkV6) && (isIpv4Enabled && !isIpv6Enabled))) + if (((address.AddressFamily == AddressFamily.InterNetwork) && (!isIPv4Enabled && isIPv6Enabled)) || + ((address.AddressFamily == AddressFamily.InterNetworkV6) && (isIPv4Enabled && !isIPv6Enabled))) { addresses = Array.Empty<IPAddress>(); return false; @@ -338,11 +338,11 @@ namespace MediaBrowser.Common.Net addressBytes.Reverse(); } - uint ipAddress = BitConverter.ToUInt32(addressBytes, 0); + uint iPAddress = BitConverter.ToUInt32(addressBytes, 0); uint ipMaskV4 = BitConverter.ToUInt32(CidrToMask(network.PrefixLength, AddressFamily.InterNetwork).GetAddressBytes(), 0); - uint broadCastIpAddress = ipAddress | ~ipMaskV4; + uint broadCastIPAddress = iPAddress | ~ipMaskV4; - return new IPAddress(BitConverter.GetBytes(broadCastIpAddress)); + return new IPAddress(BitConverter.GetBytes(broadCastIPAddress)); } } } diff --git a/MediaBrowser.Model/Dlna/UpnpDeviceInfo.cs b/MediaBrowser.Model/Dlna/UpnpDeviceInfo.cs index 987a3a908f..c7489d57ae 100644 --- a/MediaBrowser.Model/Dlna/UpnpDeviceInfo.cs +++ b/MediaBrowser.Model/Dlna/UpnpDeviceInfo.cs @@ -13,10 +13,10 @@ namespace MediaBrowser.Model.Dlna public Dictionary<string, string> Headers { get; set; } - public IPAddress LocalIpAddress { get; set; } + public IPAddress LocalIPAddress { get; set; } public int LocalPort { get; set; } - public IPAddress RemoteIpAddress { get; set; } + public IPAddress RemoteIPAddress { get; set; } } } diff --git a/RSSDP/DeviceAvailableEventArgs.cs b/RSSDP/DeviceAvailableEventArgs.cs index 9d477ea9f4..f933f258be 100644 --- a/RSSDP/DeviceAvailableEventArgs.cs +++ b/RSSDP/DeviceAvailableEventArgs.cs @@ -8,7 +8,7 @@ namespace Rssdp /// </summary> public sealed class DeviceAvailableEventArgs : EventArgs { - public IPAddress RemoteIpAddress { get; set; } + public IPAddress RemoteIPAddress { get; set; } private readonly DiscoveredSsdpDevice _DiscoveredDevice; diff --git a/RSSDP/ISsdpCommunicationsServer.cs b/RSSDP/ISsdpCommunicationsServer.cs index 571c66c107..95b0a1c704 100644 --- a/RSSDP/ISsdpCommunicationsServer.cs +++ b/RSSDP/ISsdpCommunicationsServer.cs @@ -33,13 +33,13 @@ namespace Rssdp.Infrastructure /// <summary> /// Sends a message to a particular address (uni or multicast) and port. /// </summary> - Task SendMessage(byte[] messageData, IPEndPoint destination, IPAddress fromLocalIpAddress, CancellationToken cancellationToken); + Task SendMessage(byte[] messageData, IPEndPoint destination, IPAddress fromLocalIPAddress, CancellationToken cancellationToken); /// <summary> /// Sends a message to the SSDP multicast address and port. /// </summary> - Task SendMulticastMessage(string message, IPAddress fromLocalIpAddress, CancellationToken cancellationToken); - Task SendMulticastMessage(string message, int sendCount, IPAddress fromLocalIpAddress, CancellationToken cancellationToken); + Task SendMulticastMessage(string message, IPAddress fromLocalIPAddress, CancellationToken cancellationToken); + Task SendMulticastMessage(string message, int sendCount, IPAddress fromLocalIPAddress, CancellationToken cancellationToken); /// <summary> /// Gets or sets a boolean value indicating whether or not this instance is shared amongst multiple <see cref="SsdpDeviceLocator"/> and/or <see cref="ISsdpDevicePublisher"/> instances. diff --git a/RSSDP/RequestReceivedEventArgs.cs b/RSSDP/RequestReceivedEventArgs.cs index 5cf74bd757..b8b2249e42 100644 --- a/RSSDP/RequestReceivedEventArgs.cs +++ b/RSSDP/RequestReceivedEventArgs.cs @@ -13,16 +13,16 @@ namespace Rssdp.Infrastructure private readonly IPEndPoint _ReceivedFrom; - public IPAddress LocalIpAddress { get; private set; } + public IPAddress LocalIPAddress { get; private set; } /// <summary> /// Full constructor. /// </summary> - public RequestReceivedEventArgs(HttpRequestMessage message, IPEndPoint receivedFrom, IPAddress localIpAddress) + public RequestReceivedEventArgs(HttpRequestMessage message, IPEndPoint receivedFrom, IPAddress localIPAddress) { _Message = message; _ReceivedFrom = receivedFrom; - LocalIpAddress = localIpAddress; + LocalIPAddress = localIPAddress; } /// <summary> diff --git a/RSSDP/ResponseReceivedEventArgs.cs b/RSSDP/ResponseReceivedEventArgs.cs index 93262a4608..e87ba14524 100644 --- a/RSSDP/ResponseReceivedEventArgs.cs +++ b/RSSDP/ResponseReceivedEventArgs.cs @@ -9,7 +9,7 @@ namespace Rssdp.Infrastructure /// </summary> public sealed class ResponseReceivedEventArgs : EventArgs { - public IPAddress LocalIpAddress { get; set; } + public IPAddress LocalIPAddress { get; set; } private readonly HttpResponseMessage _Message; diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs index f70a598fbe..5b8916d021 100644 --- a/RSSDP/SsdpCommunicationsServer.cs +++ b/RSSDP/SsdpCommunicationsServer.cs @@ -155,7 +155,7 @@ namespace Rssdp.Infrastructure /// <summary> /// Sends a message to a particular address (uni or multicast) and port. /// </summary> - public async Task SendMessage(byte[] messageData, IPEndPoint destination, IPAddress fromLocalIpAddress, CancellationToken cancellationToken) + public async Task SendMessage(byte[] messageData, IPEndPoint destination, IPAddress fromlocalIPAddress, CancellationToken cancellationToken) { if (messageData is null) { @@ -164,7 +164,7 @@ namespace Rssdp.Infrastructure ThrowIfDisposed(); - var sockets = GetSendSockets(fromLocalIpAddress, destination); + var sockets = GetSendSockets(fromlocalIPAddress, destination); if (sockets.Count == 0) { @@ -200,19 +200,19 @@ namespace Rssdp.Infrastructure } } - private List<Socket> GetSendSockets(IPAddress fromLocalIpAddress, IPEndPoint destination) + private List<Socket> GetSendSockets(IPAddress fromlocalIPAddress, IPEndPoint destination) { EnsureSendSocketCreated(); lock (_SendSocketSynchroniser) { - var sockets = _sendSockets.Where(s => s.AddressFamily == fromLocalIpAddress.AddressFamily); + var sockets = _sendSockets.Where(s => s.AddressFamily == fromlocalIPAddress.AddressFamily); // Send from the Any socket and the socket with the matching address - if (fromLocalIpAddress.AddressFamily == AddressFamily.InterNetwork) + if (fromlocalIPAddress.AddressFamily == AddressFamily.InterNetwork) { sockets = sockets.Where(s => ((IPEndPoint)s.LocalEndPoint).Address.Equals(IPAddress.Any) - || ((IPEndPoint)s.LocalEndPoint).Address.Equals(fromLocalIpAddress)); + || ((IPEndPoint)s.LocalEndPoint).Address.Equals(fromlocalIPAddress)); // If sending to the loopback address, filter the socket list as well if (destination.Address.Equals(IPAddress.Loopback)) @@ -221,10 +221,10 @@ namespace Rssdp.Infrastructure || ((IPEndPoint)s.LocalEndPoint).Address.Equals(IPAddress.Loopback)); } } - else if (fromLocalIpAddress.AddressFamily == AddressFamily.InterNetworkV6) + else if (fromlocalIPAddress.AddressFamily == AddressFamily.InterNetworkV6) { sockets = sockets.Where(s => ((IPEndPoint)s.LocalEndPoint).Address.Equals(IPAddress.IPv6Any) - || ((IPEndPoint)s.LocalEndPoint).Address.Equals(fromLocalIpAddress)); + || ((IPEndPoint)s.LocalEndPoint).Address.Equals(fromlocalIPAddress)); // If sending to the loopback address, filter the socket list as well if (destination.Address.Equals(IPAddress.IPv6Loopback)) @@ -238,15 +238,15 @@ namespace Rssdp.Infrastructure } } - public Task SendMulticastMessage(string message, IPAddress fromLocalIpAddress, CancellationToken cancellationToken) + public Task SendMulticastMessage(string message, IPAddress fromlocalIPAddress, CancellationToken cancellationToken) { - return SendMulticastMessage(message, SsdpConstants.UdpResendCount, fromLocalIpAddress, cancellationToken); + return SendMulticastMessage(message, SsdpConstants.UdpResendCount, fromlocalIPAddress, cancellationToken); } /// <summary> /// Sends a message to the SSDP multicast address and port. /// </summary> - public async Task SendMulticastMessage(string message, int sendCount, IPAddress fromLocalIpAddress, CancellationToken cancellationToken) + public async Task SendMulticastMessage(string message, int sendCount, IPAddress fromlocalIPAddress, CancellationToken cancellationToken) { if (message is null) { @@ -269,7 +269,7 @@ namespace Rssdp.Infrastructure new IPEndPoint( IPAddress.Parse(SsdpConstants.MulticastLocalAdminAddress), SsdpConstants.MulticastPort), - fromLocalIpAddress, + fromlocalIPAddress, cancellationToken).ConfigureAwait(false); await Task.Delay(100, cancellationToken).ConfigureAwait(false); @@ -328,14 +328,14 @@ namespace Rssdp.Infrastructure } } - private Task SendMessageIfSocketNotDisposed(byte[] messageData, IPEndPoint destination, IPAddress fromLocalIpAddress, CancellationToken cancellationToken) + private Task SendMessageIfSocketNotDisposed(byte[] messageData, IPEndPoint destination, IPAddress fromlocalIPAddress, CancellationToken cancellationToken) { var sockets = _sendSockets; if (sockets is not null) { sockets = sockets.ToList(); - var tasks = sockets.Where(s => (fromLocalIpAddress is null || fromLocalIpAddress.Equals(((IPEndPoint)s.LocalEndPoint).Address))) + var tasks = sockets.Where(s => (fromlocalIPAddress is null || fromlocalIPAddress.Equals(((IPEndPoint)s.LocalEndPoint).Address))) .Select(s => SendFromSocket(s, messageData, destination, cancellationToken)); return Task.WhenAll(tasks); } @@ -458,13 +458,13 @@ namespace Rssdp.Infrastructure } } - private void ProcessMessage(string data, IPEndPoint endPoint, IPAddress receivedOnLocalIpAddress) + private void ProcessMessage(string data, IPEndPoint endPoint, IPAddress receivedOnlocalIPAddress) { // Responses start with the HTTP version, prefixed with HTTP/ while // requests start with a method which can vary and might be one we haven't // seen/don't know. We'll check if this message is a request or a response // by checking for the HTTP/ prefix on the start of the message. - _logger.LogDebug("Received data from {From} on {Port} at {Address}:\n{Data}", endPoint.Address, endPoint.Port, receivedOnLocalIpAddress, data); + _logger.LogDebug("Received data from {From} on {Port} at {Address}:\n{Data}", endPoint.Address, endPoint.Port, receivedOnlocalIPAddress, data); if (data.StartsWith("HTTP/", StringComparison.OrdinalIgnoreCase)) { HttpResponseMessage responseMessage = null; @@ -479,7 +479,7 @@ namespace Rssdp.Infrastructure if (responseMessage is not null) { - OnResponseReceived(responseMessage, endPoint, receivedOnLocalIpAddress); + OnResponseReceived(responseMessage, endPoint, receivedOnlocalIPAddress); } } else @@ -496,12 +496,12 @@ namespace Rssdp.Infrastructure if (requestMessage is not null) { - OnRequestReceived(requestMessage, endPoint, receivedOnLocalIpAddress); + OnRequestReceived(requestMessage, endPoint, receivedOnlocalIPAddress); } } } - private void OnRequestReceived(HttpRequestMessage data, IPEndPoint remoteEndPoint, IPAddress receivedOnLocalIpAddress) + private void OnRequestReceived(HttpRequestMessage data, IPEndPoint remoteEndPoint, IPAddress receivedOnlocalIPAddress) { // SSDP specification says only * is currently used but other uri's might // be implemented in the future and should be ignored unless understood. @@ -514,18 +514,18 @@ namespace Rssdp.Infrastructure var handlers = this.RequestReceived; if (handlers is not null) { - handlers(this, new RequestReceivedEventArgs(data, remoteEndPoint, receivedOnLocalIpAddress)); + handlers(this, new RequestReceivedEventArgs(data, remoteEndPoint, receivedOnlocalIPAddress)); } } - private void OnResponseReceived(HttpResponseMessage data, IPEndPoint endPoint, IPAddress localIpAddress) + private void OnResponseReceived(HttpResponseMessage data, IPEndPoint endPoint, IPAddress localIPAddress) { var handlers = this.ResponseReceived; if (handlers is not null) { handlers(this, new ResponseReceivedEventArgs(data, endPoint) { - LocalIpAddress = localIpAddress + LocalIPAddress = localIPAddress }); } } diff --git a/RSSDP/SsdpDeviceLocator.cs b/RSSDP/SsdpDeviceLocator.cs index 25c3b4c4e8..9d756d0d4c 100644 --- a/RSSDP/SsdpDeviceLocator.cs +++ b/RSSDP/SsdpDeviceLocator.cs @@ -240,7 +240,7 @@ namespace Rssdp.Infrastructure /// Raises the <see cref="DeviceAvailable"/> event. /// </summary> /// <seealso cref="DeviceAvailable"/> - protected virtual void OnDeviceAvailable(DiscoveredSsdpDevice device, bool isNewDevice, IPAddress IpAddress) + protected virtual void OnDeviceAvailable(DiscoveredSsdpDevice device, bool isNewDevice, IPAddress IPAddress) { if (this.IsDisposed) { @@ -252,7 +252,7 @@ namespace Rssdp.Infrastructure { handlers(this, new DeviceAvailableEventArgs(device, isNewDevice) { - RemoteIpAddress = IpAddress + RemoteIPAddress = IPAddress }); } } @@ -318,7 +318,7 @@ namespace Rssdp.Infrastructure } } - private void AddOrUpdateDiscoveredDevice(DiscoveredSsdpDevice device, IPAddress IpAddress) + private void AddOrUpdateDiscoveredDevice(DiscoveredSsdpDevice device, IPAddress IPAddress) { bool isNewDevice = false; lock (_Devices) @@ -336,17 +336,17 @@ namespace Rssdp.Infrastructure } } - DeviceFound(device, isNewDevice, IpAddress); + DeviceFound(device, isNewDevice, IPAddress); } - private void DeviceFound(DiscoveredSsdpDevice device, bool isNewDevice, IPAddress IpAddress) + private void DeviceFound(DiscoveredSsdpDevice device, bool isNewDevice, IPAddress IPAddress) { if (!NotificationTypeMatchesFilter(device)) { return; } - OnDeviceAvailable(device, isNewDevice, IpAddress); + OnDeviceAvailable(device, isNewDevice, IPAddress); } private bool NotificationTypeMatchesFilter(DiscoveredSsdpDevice device) @@ -378,7 +378,7 @@ namespace Rssdp.Infrastructure return _CommunicationsServer.SendMulticastMessage(message, null, cancellationToken); } - private void ProcessSearchResponseMessage(HttpResponseMessage message, IPAddress IpAddress) + private void ProcessSearchResponseMessage(HttpResponseMessage message, IPAddress IPAddress) { if (!message.IsSuccessStatusCode) { @@ -398,11 +398,11 @@ namespace Rssdp.Infrastructure ResponseHeaders = message.Headers }; - AddOrUpdateDiscoveredDevice(device, IpAddress); + AddOrUpdateDiscoveredDevice(device, IPAddress); } } - private void ProcessNotificationMessage(HttpRequestMessage message, IPAddress IpAddress) + private void ProcessNotificationMessage(HttpRequestMessage message, IPAddress IPAddress) { if (String.Compare(message.Method.Method, "Notify", StringComparison.OrdinalIgnoreCase) != 0) { @@ -412,7 +412,7 @@ namespace Rssdp.Infrastructure var notificationType = GetFirstHeaderStringValue("NTS", message); if (String.Compare(notificationType, SsdpConstants.SsdpKeepAliveNotification, StringComparison.OrdinalIgnoreCase) == 0) { - ProcessAliveNotification(message, IpAddress); + ProcessAliveNotification(message, IPAddress); } else if (String.Compare(notificationType, SsdpConstants.SsdpByeByeNotification, StringComparison.OrdinalIgnoreCase) == 0) { @@ -420,7 +420,7 @@ namespace Rssdp.Infrastructure } } - private void ProcessAliveNotification(HttpRequestMessage message, IPAddress IpAddress) + private void ProcessAliveNotification(HttpRequestMessage message, IPAddress IPAddress) { var location = GetFirstHeaderUriValue("Location", message); if (location is not null) @@ -435,7 +435,7 @@ namespace Rssdp.Infrastructure ResponseHeaders = message.Headers }; - AddOrUpdateDiscoveredDevice(device, IpAddress); + AddOrUpdateDiscoveredDevice(device, IPAddress); } } @@ -651,7 +651,7 @@ namespace Rssdp.Infrastructure private void CommsServer_ResponseReceived(object sender, ResponseReceivedEventArgs e) { - ProcessSearchResponseMessage(e.Message, e.LocalIpAddress); + ProcessSearchResponseMessage(e.Message, e.LocalIPAddress); } private void CommsServer_RequestReceived(object sender, RequestReceivedEventArgs e) diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs index 40d93b6c0c..8b55518999 100644 --- a/RSSDP/SsdpDevicePublisher.cs +++ b/RSSDP/SsdpDevicePublisher.cs @@ -224,7 +224,7 @@ namespace Rssdp.Infrastructure string mx, string searchTarget, IPEndPoint remoteEndPoint, - IPAddress receivedOnlocalIpAddress, + IPAddress receivedOnlocalIPAddress, CancellationToken cancellationToken) { if (String.IsNullOrEmpty(searchTarget)) @@ -297,9 +297,9 @@ namespace Rssdp.Infrastructure { var root = device.ToRootDevice(); - if (!_sendOnlyMatchedHost || root.Address.Equals(receivedOnlocalIpAddress)) + if (!_sendOnlyMatchedHost || root.Address.Equals(receivedOnlocalIPAddress)) { - SendDeviceSearchResponses(device, remoteEndPoint, receivedOnlocalIpAddress, cancellationToken); + SendDeviceSearchResponses(device, remoteEndPoint, receivedOnlocalIPAddress, cancellationToken); } } } @@ -314,22 +314,22 @@ namespace Rssdp.Infrastructure private void SendDeviceSearchResponses( SsdpDevice device, IPEndPoint endPoint, - IPAddress receivedOnlocalIpAddress, + IPAddress receivedOnlocalIPAddress, CancellationToken cancellationToken) { bool isRootDevice = (device as SsdpRootDevice) is not null; if (isRootDevice) { - SendSearchResponse(SsdpConstants.UpnpDeviceTypeRootDevice, device, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), endPoint, receivedOnlocalIpAddress, cancellationToken); + SendSearchResponse(SsdpConstants.UpnpDeviceTypeRootDevice, device, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), endPoint, receivedOnlocalIPAddress, cancellationToken); if (this.SupportPnpRootDevice) { - SendSearchResponse(SsdpConstants.PnpDeviceTypeRootDevice, device, GetUsn(device.Udn, SsdpConstants.PnpDeviceTypeRootDevice), endPoint, receivedOnlocalIpAddress, cancellationToken); + SendSearchResponse(SsdpConstants.PnpDeviceTypeRootDevice, device, GetUsn(device.Udn, SsdpConstants.PnpDeviceTypeRootDevice), endPoint, receivedOnlocalIPAddress, cancellationToken); } } - SendSearchResponse(device.Udn, device, device.Udn, endPoint, receivedOnlocalIpAddress, cancellationToken); + SendSearchResponse(device.Udn, device, device.Udn, endPoint, receivedOnlocalIPAddress, cancellationToken); - SendSearchResponse(device.FullDeviceType, device, GetUsn(device.Udn, device.FullDeviceType), endPoint, receivedOnlocalIpAddress, cancellationToken); + SendSearchResponse(device.FullDeviceType, device, GetUsn(device.Udn, device.FullDeviceType), endPoint, receivedOnlocalIPAddress, cancellationToken); } private string GetUsn(string udn, string fullDeviceType) @@ -342,7 +342,7 @@ namespace Rssdp.Infrastructure SsdpDevice device, string uniqueServiceName, IPEndPoint endPoint, - IPAddress receivedOnlocalIpAddress, + IPAddress receivedOnlocalIPAddress, CancellationToken cancellationToken) { const string header = "HTTP/1.1 200 OK"; @@ -366,7 +366,7 @@ namespace Rssdp.Infrastructure await _CommsServer.SendMessage( Encoding.UTF8.GetBytes(message), endPoint, - receivedOnlocalIpAddress, + receivedOnlocalIPAddress, cancellationToken) .ConfigureAwait(false); } @@ -625,7 +625,7 @@ namespace Rssdp.Infrastructure // else if (!e.Message.Headers.Contains("MAN")) // WriteTrace("Ignoring search request - missing MAN header."); // else - ProcessSearchRequest(GetFirstHeaderValue(e.Message.Headers, "MX"), GetFirstHeaderValue(e.Message.Headers, "ST"), e.ReceivedFrom, e.LocalIpAddress, CancellationToken.None); + ProcessSearchRequest(GetFirstHeaderValue(e.Message.Headers, "MX"), GetFirstHeaderValue(e.Message.Headers, "ST"), e.ReceivedFrom, e.LocalIPAddress, CancellationToken.None); } } diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs index 10706e9c21..d51ce19d75 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs @@ -97,7 +97,7 @@ namespace Jellyfin.Networking.Tests /// Checks if IPv4 address is within a defined subnet. /// </summary> /// <param name="netMask">Network mask.</param> - /// <param name="ipAddress">IP Address.</param> + /// <param name="IPAddress">IP Address.</param> [Theory] [InlineData("192.168.5.85/24", "192.168.5.1")] [InlineData("192.168.5.85/24", "192.168.5.254")] @@ -282,7 +282,7 @@ namespace Jellyfin.Networking.Tests [InlineData("185.10.10.10", "185.10.10.10", false)] [InlineData("", "100.100.100.100", false)] - public void HasRemoteAccess_GivenWhitelist_AllowsOnlyIPsInWhitelist(string addresses, string remoteIp, bool denied) + public void HasRemoteAccess_GivenWhitelist_AllowsOnlyIPsInWhitelist(string addresses, string remoteIP, bool denied) { // 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. @@ -294,7 +294,7 @@ namespace Jellyfin.Networking.Tests }; using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); - Assert.NotEqual(nm.HasRemoteAccess(IPAddress.Parse(remoteIp)), denied); + Assert.NotEqual(nm.HasRemoteAccess(IPAddress.Parse(remoteIP)), denied); } [Theory] @@ -302,7 +302,7 @@ namespace Jellyfin.Networking.Tests [InlineData("185.10.10.10", "185.10.10.10", true)] [InlineData("", "100.100.100.100", false)] - public void HasRemoteAccess_GivenBlacklist_BlacklistTheIPs(string addresses, string remoteIp, bool denied) + public void HasRemoteAccess_GivenBlacklist_BlacklistTheIPs(string addresses, string remoteIP, bool denied) { // 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. @@ -315,7 +315,7 @@ namespace Jellyfin.Networking.Tests using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); - Assert.NotEqual(nm.HasRemoteAccess(IPAddress.Parse(remoteIp)), denied); + Assert.NotEqual(nm.HasRemoteAccess(IPAddress.Parse(remoteIP)), denied); } [Theory] From af7acc000c961312bd4a2d061dc74c64c0e3647a Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Sat, 18 Feb 2023 21:10:09 +0100 Subject: [PATCH 081/858] Fix dependency on Microsoft.AspNetCore.HttpOverrides --- Directory.Packages.props | 1 + MediaBrowser.Model/MediaBrowser.Model.csproj | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index b72bb90709..86ad36941d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -24,6 +24,7 @@ <PackageVersion Include="LrcParser" Version="2022.529.1" /> <PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" /> <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.3" /> + <PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" /> <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.3" /> <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.3" /> diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 087e6369e6..58ba83a35f 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -33,6 +33,7 @@ </PropertyGroup> <ItemGroup> + <PackageReference Include="Microsoft.AspNetCore.HttpOverrides" /> <PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" /> <PackageReference Include="MimeTypes"> @@ -58,7 +59,6 @@ <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" /> </ItemGroup> <ItemGroup> - <FrameworkReference Include="Microsoft.AspNetCore.App" /> <ProjectReference Include="../Jellyfin.Data/Jellyfin.Data.csproj" /> <ProjectReference Include="../src/Jellyfin.Extensions/Jellyfin.Extensions.csproj" /> </ItemGroup> From ed2280a0605c7e971d151b9b2239d332ee204579 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Sun, 9 Oct 2022 22:56:23 +0200 Subject: [PATCH 082/858] Overhaul content ratings --- .../Localization/LocalizationManager.cs | 18 ++++- .../Localization/Ratings/0-prefer.csv | 11 +++ .../Localization/Ratings/au.csv | 20 +++-- .../Localization/Ratings/be.csv | 17 +++-- .../Localization/Ratings/br.csv | 14 ++-- .../Localization/Ratings/ca.csv | 26 +++++-- .../Localization/Ratings/co.csv | 15 ++-- .../Localization/Ratings/de.csv | 22 +++--- .../Localization/Ratings/dk.csv | 11 ++- .../Localization/Ratings/es.csv | 30 ++++++-- .../Localization/Ratings/fi.csv | 20 ++--- .../Localization/Ratings/fr.csv | 17 +++-- .../Localization/Ratings/gb.csv | 29 ++++++-- .../Localization/Ratings/ie.csv | 15 ++-- .../Localization/Ratings/jp.csv | 15 +++- .../Localization/Ratings/kz.csv | 13 ++-- .../Localization/Ratings/mx.csv | 12 +-- .../Localization/Ratings/nl.csv | 14 ++-- .../Localization/Ratings/no.csv | 15 ++-- .../Localization/Ratings/nz.csv | 26 ++++--- .../Localization/Ratings/ro.csv | 7 +- .../Localization/Ratings/ru.csv | 11 +-- .../Localization/Ratings/se.csv | 15 ++-- .../Localization/Ratings/uk.csv | 29 ++++++-- .../Localization/Ratings/us.csv | 73 +++++++++++++------ .../Localization/LocalizationManagerTests.cs | 25 ++++--- 26 files changed, 346 insertions(+), 174 deletions(-) create mode 100644 Emby.Server.Implementations/Localization/Ratings/0-prefer.csv diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index b418c7877e..5e22e78867 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Linq; using System.Reflection; using System.Text.Json; using System.Threading.Tasks; @@ -25,7 +26,7 @@ namespace Emby.Server.Implementations.Localization private const string CulturesPath = "Emby.Server.Implementations.Localization.iso6392.txt"; private const string CountriesPath = "Emby.Server.Implementations.Localization.countries.json"; private static readonly Assembly _assembly = typeof(LocalizationManager).Assembly; - private static readonly string[] _unratedValues = { "n/a", "unrated", "not rated" }; + private static readonly string[] _unratedValues = { "n/a", "unrated", "not rated", "nr" }; private readonly IServerConfigurationManager _configurationManager; private readonly ILogger<LocalizationManager> _logger; @@ -194,6 +195,7 @@ namespace Emby.Server.Implementations.Localization { var countryCode = _configurationManager.Configuration.MetadataCountryCode; + // Fall back to US ratings if no country code is specified or country code does not exist. if (string.IsNullOrEmpty(countryCode)) { countryCode = "us"; @@ -221,12 +223,14 @@ namespace Emby.Server.Implementations.Localization { ArgumentException.ThrowIfNullOrEmpty(rating); + // Handle unrated content if (_unratedValues.Contains(rating.AsSpan(), StringComparison.OrdinalIgnoreCase)) { return null; } // Fairly common for some users to have "Rated R" in their rating field + rating = rating.Replace("Rated :", string.Empty, StringComparison.OrdinalIgnoreCase); rating = rating.Replace("Rated ", string.Empty, StringComparison.OrdinalIgnoreCase); var ratingsDictionary = GetParentalRatingsDictionary(); @@ -257,6 +261,18 @@ namespace Emby.Server.Implementations.Localization } } + // Remove prefix country code to handle "DE-18" + index = rating.IndexOf('-', StringComparison.Ordinal); + if (index != -1) + { + var trimmedRating = rating.AsSpan(index).TrimStart('-').Trim(); + + if (!trimmedRating.IsEmpty) + { + return GetRatingLevel(trimmedRating.ToString()); + } + } + // TODO: Further improve by normalizing out all spaces and dashes return null; } diff --git a/Emby.Server.Implementations/Localization/Ratings/0-prefer.csv b/Emby.Server.Implementations/Localization/Ratings/0-prefer.csv new file mode 100644 index 0000000000..36886ba760 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/0-prefer.csv @@ -0,0 +1,11 @@ +E,0 +EC,0 +T,7 +M,18 +AO,18 +UR,18 +RP,18 +X,1000 +XX,1000 +XXX,1000 +XXXX,1000 diff --git a/Emby.Server.Implementations/Localization/Ratings/au.csv b/Emby.Server.Implementations/Localization/Ratings/au.csv index 11f4ed94cd..4ab808ae9a 100644 --- a/Emby.Server.Implementations/Localization/Ratings/au.csv +++ b/Emby.Server.Implementations/Localization/Ratings/au.csv @@ -1,7 +1,13 @@ -AU-G,1 -AU-PG,5 -AU-M,6 -AU-MA15+,7 -AU-R18+,9 -AU-X18+,10 -AU-RC,11 +Exempt,0 +G,0 +7+,7 +M,15 +MA,15 +MA15+,15 +PG,16 +16+,16 +R,18 +R18+,18 +X18+,18 +18+,18 +X,1000 diff --git a/Emby.Server.Implementations/Localization/Ratings/be.csv b/Emby.Server.Implementations/Localization/Ratings/be.csv index d3937caf78..5588f63b45 100644 --- a/Emby.Server.Implementations/Localization/Ratings/be.csv +++ b/Emby.Server.Implementations/Localization/Ratings/be.csv @@ -1,6 +1,11 @@ -BE-AL,1 -BE-MG6,2 -BE-6,3 -BE-9,5 -BE-12,6 -BE-16,8 +AL,0 +KT,0 +TOUS,0 +MG6,6 +6,6 +9,9 +KNT,12 +12,12 +BE_14,14 +16,16 +18,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/br.csv b/Emby.Server.Implementations/Localization/Ratings/br.csv index e5edaf62cf..5ec1eb2627 100644 --- a/Emby.Server.Implementations/Localization/Ratings/br.csv +++ b/Emby.Server.Implementations/Localization/Ratings/br.csv @@ -1,6 +1,8 @@ -BR-L,1 -BR-10,5 -BR-12,7 -BR-14,8 -BR-16,8 -BR-18,9 +Livre,0 +L,0 +ER,9 +10,10 +12,12 +14,14 +16,16 +18,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/ca.csv b/Emby.Server.Implementations/Localization/Ratings/ca.csv index 5aef0580f8..336ee28067 100644 --- a/Emby.Server.Implementations/Localization/Ratings/ca.csv +++ b/Emby.Server.Implementations/Localization/Ratings/ca.csv @@ -1,6 +1,20 @@ -CA-G,1 -CA-PG,5 -CA-14A,7 -CA-A,8 -CA-18A,9 -CA-R,10 +E,0 +G,0 +TV-Y,0 +TV-G,0 +TV-Y7,7 +TV-Y7-FV,7 +PG,9 +TV-PG,9 +PG-13,13 +13+,13 +TV-14,14 +14A,14 +16+,16 +NC-17,17 +R,18 +TV-MA,18 +18A,18 +18+,18 +A,1000 +Prohibited,1001 diff --git a/Emby.Server.Implementations/Localization/Ratings/co.csv b/Emby.Server.Implementations/Localization/Ratings/co.csv index 9684fa0524..e1e96c5909 100644 --- a/Emby.Server.Implementations/Localization/Ratings/co.csv +++ b/Emby.Server.Implementations/Localization/Ratings/co.csv @@ -1,8 +1,7 @@ -CO-T,1 -CO-7,5 -CO-12,7 -CO-15,8 -CO-18,10 -CO-X,100 -CO-BANNED,15 -CO-E,15 +T,0 +7,7 +12,12 +15,15 +18,18 +X,1000 +Prohibited,1001 diff --git a/Emby.Server.Implementations/Localization/Ratings/de.csv b/Emby.Server.Implementations/Localization/Ratings/de.csv index f944a140d0..d633a5dab7 100644 --- a/Emby.Server.Implementations/Localization/Ratings/de.csv +++ b/Emby.Server.Implementations/Localization/Ratings/de.csv @@ -1,10 +1,12 @@ -DE-0,1 -FSK-0,1 -DE-6,5 -FSK-6,5 -DE-12,7 -FSK-12,7 -DE-16,8 -FSK-16,8 -DE-18,9 -FSK-18,9 +Educational,0 +Infoprogramm,0 +FSK-0,0 +0,0 +FSK-6,6 +6,6 +FSK-12,12 +12,12 +FSK-16,16 +16,16 +FSK-18,18 +18,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/dk.csv b/Emby.Server.Implementations/Localization/Ratings/dk.csv index 5364ae1f27..4ef63b2eac 100644 --- a/Emby.Server.Implementations/Localization/Ratings/dk.csv +++ b/Emby.Server.Implementations/Localization/Ratings/dk.csv @@ -1,4 +1,7 @@ -DA-A,1 -DA-7,5 -DA-11,6 -DA-15,8 +F,0 +A,0 +7,7 +11,11 +12,12 +15,15 +16,16 diff --git a/Emby.Server.Implementations/Localization/Ratings/es.csv b/Emby.Server.Implementations/Localization/Ratings/es.csv index 887d91ba63..0bc1d3f7d0 100644 --- a/Emby.Server.Implementations/Localization/Ratings/es.csv +++ b/Emby.Server.Implementations/Localization/Ratings/es.csv @@ -1,6 +1,24 @@ -ES-A,1 -ES-APTA,1 -ES-7,3 -ES-12,6 -ES-16,8 -ES-18,11 +A,0 +A/fig,0 +A/i,0 +A/fig/i,0 +APTA,0 +TP,0 +0+,0 +6+,6 +7/fig,7 +7/i,7 +7/i/fig,7 +7,7 +9+,9 +10,10 +12,12 +12/fig,12 +13,13 +14,14 +16,16 +16/fig,16 +18,18 +18/fig,18 +X,1000 +Banned,1001 diff --git a/Emby.Server.Implementations/Localization/Ratings/fi.csv b/Emby.Server.Implementations/Localization/Ratings/fi.csv index 782785890f..7ff92f259b 100644 --- a/Emby.Server.Implementations/Localization/Ratings/fi.csv +++ b/Emby.Server.Implementations/Localization/Ratings/fi.csv @@ -1,10 +1,10 @@ -FI-S,1 -FI-T,1 -FI-7,4 -FI-12,5 -FI-16,8 -FI-18,9 -FI-K7,4 -FI-K12,5 -FI-K16,8 -FI-K18,9 +S,0 +T,0 +K7,7 +7,7 +K12,12 +12,12 +K16,16 +16,16 +K18,18 +18,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/fr.csv b/Emby.Server.Implementations/Localization/Ratings/fr.csv index f586a3fa91..774a705891 100644 --- a/Emby.Server.Implementations/Localization/Ratings/fr.csv +++ b/Emby.Server.Implementations/Localization/Ratings/fr.csv @@ -1,5 +1,12 @@ -FR-U,1 -FR-10,5 -FR-12,7 -FR-16,9 -FR-18,10 +Public Averti,0 +Tous Publics,0 +U,0 +0+,0 +6+,6 +9+,9 +10,10 +12,12 +14+,14 +16,16 +18,18 +X,1000 diff --git a/Emby.Server.Implementations/Localization/Ratings/gb.csv b/Emby.Server.Implementations/Localization/Ratings/gb.csv index c1f7d04529..75b1c20589 100644 --- a/Emby.Server.Implementations/Localization/Ratings/gb.csv +++ b/Emby.Server.Implementations/Localization/Ratings/gb.csv @@ -1,7 +1,22 @@ -GB-U,1 -GB-PG,5 -GB-12,6 -GB-12A,7 -GB-15,8 -GB-18,9 -GB-R18,15 +All,0 +E,0 +G,0 +U,0 +0+,0 +6+,6 +7+,7 +PG,8 +9+,9 +12,12 +12+,12 +12A,12 +Teen,13 +13+,13 +14+,14 +15,15 +16,16 +Caution,18 +18,18 +Mature,1000 +Adult,1000 +R18,1000 diff --git a/Emby.Server.Implementations/Localization/Ratings/ie.csv b/Emby.Server.Implementations/Localization/Ratings/ie.csv index e42be5cd49..6ef2e50128 100644 --- a/Emby.Server.Implementations/Localization/Ratings/ie.csv +++ b/Emby.Server.Implementations/Localization/Ratings/ie.csv @@ -1,6 +1,9 @@ -IE-G,1 -IE-PG,5 -IE-12A,7 -IE-15A,8 -IE-16,9 -IE-18,10 +G,4 +PG,12 +12,12 +12A,12 +12PG,12 +15,15 +15A,15 +16,16 +18,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/jp.csv b/Emby.Server.Implementations/Localization/Ratings/jp.csv index a8fc2d1431..bfb5fdaae9 100644 --- a/Emby.Server.Implementations/Localization/Ratings/jp.csv +++ b/Emby.Server.Implementations/Localization/Ratings/jp.csv @@ -1,4 +1,11 @@ -JP-G,1 -JP-PG12,7 -JP-15+,8 -JP-18+,10 +A,0 +G,0 +B,12 +PG12,12 +C,15 +15+,15 +R15+,15 +16+,16 +D,17 +Z,18 +18+,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/kz.csv b/Emby.Server.Implementations/Localization/Ratings/kz.csv index d546bff53d..e26b32b67e 100644 --- a/Emby.Server.Implementations/Localization/Ratings/kz.csv +++ b/Emby.Server.Implementations/Localization/Ratings/kz.csv @@ -1,7 +1,6 @@ -KZ-6-,0 -KZ-6+,6 -KZ-12+,12 -KZ-14+,14 -KZ-16+,16 -KZ-18+,18 -KZ-21+,21 +K,0 +БА,12 +Б14,14 +E16,16 +E18,18 +HA,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/mx.csv b/Emby.Server.Implementations/Localization/Ratings/mx.csv index 785a8ba227..305912f239 100644 --- a/Emby.Server.Implementations/Localization/Ratings/mx.csv +++ b/Emby.Server.Implementations/Localization/Ratings/mx.csv @@ -1,6 +1,6 @@ -MX-AA,1 -MX-A,5 -MX-B,7 -MX-B-15,8 -MX-C,9 -MX-D,10 +A,0 +AA,0 +B,12 +B-15,15 +C,18 +D,1000 diff --git a/Emby.Server.Implementations/Localization/Ratings/nl.csv b/Emby.Server.Implementations/Localization/Ratings/nl.csv index 8c005092e4..44f372b2d6 100644 --- a/Emby.Server.Implementations/Localization/Ratings/nl.csv +++ b/Emby.Server.Implementations/Localization/Ratings/nl.csv @@ -1,6 +1,8 @@ -NL-AL,1 -NL-MG6,2 -NL-6,3 -NL-9,5 -NL-12,6 -NL-16,8 +AL,0 +MG6,6 +6,6 +9,9 +12,12 +14,14 +16,16 +18,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/no.csv b/Emby.Server.Implementations/Localization/Ratings/no.csv index 127407be86..c8f8e93db7 100644 --- a/Emby.Server.Implementations/Localization/Ratings/no.csv +++ b/Emby.Server.Implementations/Localization/Ratings/no.csv @@ -1,6 +1,9 @@ -NO-A,1 -NO-6,3 -NO-9,4 -NO-12,5 -NO-15,8 -NO-18,9 +A,0 +6,6 +7,7 +9,9 +11,11 +12,12 +15,15 +18,18 +Not approved,1001 diff --git a/Emby.Server.Implementations/Localization/Ratings/nz.csv b/Emby.Server.Implementations/Localization/Ratings/nz.csv index bba99b764a..f617f0c39d 100644 --- a/Emby.Server.Implementations/Localization/Ratings/nz.csv +++ b/Emby.Server.Implementations/Localization/Ratings/nz.csv @@ -1,11 +1,15 @@ -NZ-G,1 -NZ-PG,5 -NZ-M,6 -NZ-R13,7 -NZ-RP13,7 -NZ-R15,8 -NZ-RP16,9 -NZ-R16,9 -NZ-R18,10 -NZ-R,10 -NZ-MA,10 +Exempt,0 +G,0 +GY,13 +PG,13 +R13,13 +RP13,13 +R15,15 +M,16 +R16,16 +RP16,16 +GA,18 +R18,18 +MA,1000 +R,1001 +Objectionable,1001 diff --git a/Emby.Server.Implementations/Localization/Ratings/ro.csv b/Emby.Server.Implementations/Localization/Ratings/ro.csv index 4089b282f0..44c23e2486 100644 --- a/Emby.Server.Implementations/Localization/Ratings/ro.csv +++ b/Emby.Server.Implementations/Localization/Ratings/ro.csv @@ -1 +1,6 @@ -RO-AG,1 +AG,0 +AP-12,12 +N-15,15 +IM-18,18 +IM-18-XXX,1000 +IC,1001 diff --git a/Emby.Server.Implementations/Localization/Ratings/ru.csv b/Emby.Server.Implementations/Localization/Ratings/ru.csv index 1bc94affd6..8b264070ba 100644 --- a/Emby.Server.Implementations/Localization/Ratings/ru.csv +++ b/Emby.Server.Implementations/Localization/Ratings/ru.csv @@ -1,5 +1,6 @@ -RU-0+,1 -RU-6+,3 -RU-12+,7 -RU-16+,9 -RU-18+,10 +0+,0 +6+,6 +12+,12 +16+,16 +18+,18 +Refused classification,1001 diff --git a/Emby.Server.Implementations/Localization/Ratings/se.csv b/Emby.Server.Implementations/Localization/Ratings/se.csv index 1443c07df7..e129c35617 100644 --- a/Emby.Server.Implementations/Localization/Ratings/se.csv +++ b/Emby.Server.Implementations/Localization/Ratings/se.csv @@ -1,5 +1,10 @@ -SE-Btl,1 -SE-Barntillåten,1 -SE-7,3 -SE-11,5 -SE-15,8 +Alla,0 +Barntillåten,0 +Btl,0 +0+,0 +7,7 +9+,9 +10+,10 +11,11 +14,14 +15,15 diff --git a/Emby.Server.Implementations/Localization/Ratings/uk.csv b/Emby.Server.Implementations/Localization/Ratings/uk.csv index 6c8005b3f3..75b1c20589 100644 --- a/Emby.Server.Implementations/Localization/Ratings/uk.csv +++ b/Emby.Server.Implementations/Localization/Ratings/uk.csv @@ -1,7 +1,22 @@ -UK-U,1 -UK-PG,5 -UK-12,7 -UK-12A,7 -UK-15,9 -UK-18,10 -UK-R18,15 +All,0 +E,0 +G,0 +U,0 +0+,0 +6+,6 +7+,7 +PG,8 +9+,9 +12,12 +12+,12 +12A,12 +Teen,13 +13+,13 +14+,14 +15,15 +16,16 +Caution,18 +18,18 +Mature,1000 +Adult,1000 +R18,1000 diff --git a/Emby.Server.Implementations/Localization/Ratings/us.csv b/Emby.Server.Implementations/Localization/Ratings/us.csv index 34c897fe3f..d103ddf42d 100644 --- a/Emby.Server.Implementations/Localization/Ratings/us.csv +++ b/Emby.Server.Implementations/Localization/Ratings/us.csv @@ -1,23 +1,50 @@ -TV-Y,1 -APPROVED,1 -G,1 -E,1 -EC,1 -TV-G,1 -TV-Y7,3 -TV-Y7-FV,4 -PG,5 -TV-PG,5 -PG-13,7 -T,7 -TV-14,8 -R,9 -M,9 -TV-MA,9 -NC-17,10 -AO,15 -RP,15 -UR,15 -NR,15 -X,15 -XXX,100 +Approved,0 +G,0 +TV-G,0 +TV-Y,0 +TV-Y7,7 +TV-Y7-FV,7 +PG,10 +PG-13,13 +TV-PG,13 +TV-PG-D,13 +TV-PG-L,13 +TV-PG-S,13 +TV-PG-V,13 +TV-PG-DL,13 +TV-PG-DS,13 +TV-PG-DV,13 +TV-PG-LS,13 +TV-PG-LV,13 +TV-PG-SV,13 +TV-PG-DLS,13 +TV-PG-DLV,13 +TV-PG-DSV,13 +TV-PG-LSV,13 +TV-PG-DLSV,13 +TV-14,14 +TV-14-D,14 +TV-14-L,14 +TV-14-S,14 +TV-14-V,14 +TV-14-DL,14 +TV-14-DS,14 +TV-14-DV,14 +TV-14-LS,14 +TV-14-LV,14 +TV-14-SV,14 +TV-14-DLS,14 +TV-14-DLV,14 +TV-14-DSV,14 +TV-14-LSV,14 +TV-14-DLSV,14 +NC-17,17 +R,17 +TV-MA,17 +TV-MA-L,17 +TV-MA-S,17 +TV-MA-V,17 +TV-MA-LS,17 +TV-MA-LV,17 +TV-MA-SV,17 +TV-MA-LSV,17 diff --git a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs index 16eb7a75c6..256bde8194 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs @@ -83,11 +83,11 @@ namespace Jellyfin.Server.Implementations.Tests.Localization await localizationManager.LoadAll(); var ratings = localizationManager.GetParentalRatings().ToList(); - Assert.Equal(23, ratings.Count); + Assert.Equal(50, ratings.Count); var tvma = ratings.FirstOrDefault(x => x.Name.Equals("TV-MA", StringComparison.Ordinal)); Assert.NotNull(tvma); - Assert.Equal(9, tvma!.Value); + Assert.Equal(17, tvma!.Value); } [Fact] @@ -100,21 +100,21 @@ namespace Jellyfin.Server.Implementations.Tests.Localization await localizationManager.LoadAll(); var ratings = localizationManager.GetParentalRatings().ToList(); - Assert.Equal(10, ratings.Count); + Assert.Equal(12, ratings.Count); var fsk = ratings.FirstOrDefault(x => x.Name.Equals("FSK-12", StringComparison.Ordinal)); Assert.NotNull(fsk); - Assert.Equal(7, fsk!.Value); + Assert.Equal(12, fsk!.Value); } [Theory] - [InlineData("CA-R", "CA", 10)] - [InlineData("FSK-16", "DE", 8)] - [InlineData("FSK-18", "DE", 9)] - [InlineData("FSK-18", "US", 9)] - [InlineData("TV-MA", "US", 9)] - [InlineData("XXX", "asdf", 100)] - [InlineData("Germany: FSK-18", "DE", 9)] + [InlineData("CA-R", "CA", 18)] + [InlineData("FSK-16", "DE", 16)] + [InlineData("FSK-18", "DE", 18)] + [InlineData("FSK-18", "US", 18)] + [InlineData("TV-MA", "US", 17)] + [InlineData("XXX", "asdf", 1000)] + [InlineData("Germany: FSK-18", "DE", 18)] public async Task GetRatingLevel_GivenValidString_Success(string value, string countryCode, int expectedLevel) { var localizationManager = Setup(new ServerConfiguration() @@ -135,6 +135,9 @@ namespace Jellyfin.Server.Implementations.Tests.Localization UICulture = "de-DE" }); await localizationManager.LoadAll(); + Assert.Null(localizationManager.GetRatingLevel("NR")); + Assert.Null(localizationManager.GetRatingLevel("unrated")); + Assert.Null(localizationManager.GetRatingLevel("Not Rated")); Assert.Null(localizationManager.GetRatingLevel("n/a")); } From c8d80450e09c2a0c475e71c8f1d312b3a8ccff26 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Sun, 27 Nov 2022 15:37:34 +0100 Subject: [PATCH 083/858] Recursively update rating --- .../Controllers/ItemUpdateController.cs | 47 +++++++++++++++++-- MediaBrowser.Controller/Entities/BaseItem.cs | 4 +- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs index 230fbfb2cd..57d446dbd0 100644 --- a/Jellyfin.Api/Controllers/ItemUpdateController.cs +++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs @@ -98,7 +98,7 @@ public class ItemUpdateController : BaseJellyfinApiController }).ToList()); } - UpdateItem(request, item); + await UpdateItem(request, item).ConfigureAwait(false); item.OnMetadataChanged(); @@ -224,7 +224,7 @@ public class ItemUpdateController : BaseJellyfinApiController return NoContent(); } - private void UpdateItem(BaseItemDto request, BaseItem item) + private async Task UpdateItem(BaseItemDto request, BaseItem item) { item.Name = request.Name; item.ForcedSortName = request.ForcedSortName; @@ -266,9 +266,50 @@ public class ItemUpdateController : BaseJellyfinApiController item.EndDate = request.EndDate.HasValue ? NormalizeDateTime(request.EndDate.Value) : null; item.PremiereDate = request.PremiereDate.HasValue ? NormalizeDateTime(request.PremiereDate.Value) : null; item.ProductionYear = request.ProductionYear; - item.OfficialRating = string.IsNullOrWhiteSpace(request.OfficialRating) ? null : request.OfficialRating; + + request.OfficialRating = string.IsNullOrWhiteSpace(request.OfficialRating) ? null : request.OfficialRating; + item.OfficialRating = request.OfficialRating; item.CustomRating = request.CustomRating; + if (item is Series rseries) + { + foreach (Season season in rseries.Children) + { + season.OfficialRating = request.OfficialRating; + season.CustomRating = request.CustomRating; + season.OnMetadataChanged(); + await season.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); + + foreach (Episode ep in season.Children) + { + ep.OfficialRating = request.OfficialRating; + ep.CustomRating = request.CustomRating; + ep.OnMetadataChanged(); + await ep.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); + } + } + } + else if (item is Season season) + { + foreach (Episode ep in season.Children) + { + ep.OfficialRating = request.OfficialRating; + ep.CustomRating = request.CustomRating; + ep.OnMetadataChanged(); + await ep.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); + } + } + else if (item is MusicAlbum album) + { + foreach (BaseItem track in album.Children) + { + track.OfficialRating = request.OfficialRating; + track.CustomRating = request.CustomRating; + track.OnMetadataChanged(); + await track.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); + } + } + if (request.ProductionLocations is not null) { item.ProductionLocations = request.ProductionLocations; diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 3d683052fe..cf369e84b2 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -554,7 +554,7 @@ namespace MediaBrowser.Controller.Entities public string OfficialRating { get; set; } [JsonIgnore] - public int InheritedParentalRatingValue { get; set; } + public int? InheritedParentalRatingValue { get; set; } /// <summary> /// Gets or sets the critic rating. @@ -2517,7 +2517,7 @@ namespace MediaBrowser.Controller.Entities var item = this; - var inheritedParentalRatingValue = item.GetInheritedParentalRatingValue() ?? 0; + var inheritedParentalRatingValue = item.GetInheritedParentalRatingValue() ?? null; if (inheritedParentalRatingValue != item.InheritedParentalRatingValue) { item.InheritedParentalRatingValue = inheritedParentalRatingValue; From a6cfe75d6e422eb1343a1f4bc2410fabb425266b Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Mon, 28 Nov 2022 13:06:55 +0100 Subject: [PATCH 084/858] Add database migration for rating schema change --- Jellyfin.Server/Migrations/MigrationRunner.cs | 3 +- .../Routines/MigrateRatingLevels.cs | 85 +++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs index 8a6ab79323..2b15a6a1b5 100644 --- a/Jellyfin.Server/Migrations/MigrationRunner.cs +++ b/Jellyfin.Server/Migrations/MigrationRunner.cs @@ -39,7 +39,8 @@ namespace Jellyfin.Server.Migrations typeof(Routines.ReaddDefaultPluginRepository), typeof(Routines.MigrateDisplayPreferencesDb), typeof(Routines.RemoveDownloadImagesInAdvance), - typeof(Routines.MigrateAuthenticationDb) + typeof(Routines.MigrateAuthenticationDb), + typeof(Routines.MigrateRatingLevels) }; /// <summary> diff --git a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs new file mode 100644 index 0000000000..5b27681f61 --- /dev/null +++ b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs @@ -0,0 +1,85 @@ +using System; +using System.Globalization; +using System.IO; + +using MediaBrowser.Controller; +using Microsoft.Extensions.Logging; +using SQLitePCL.pretty; + +namespace Jellyfin.Server.Migrations.Routines +{ + /// <summary> + /// Remove duplicate entries which were caused by a bug where a file was considered to be an "Extra" to itself. + /// </summary> + internal class MigrateRatingLevels : IMigrationRoutine + { + private const string DbFilename = "library.db"; + private readonly ILogger<MigrateRatingLevels> _logger; + private readonly IServerApplicationPaths _paths; + + public MigrateRatingLevels(ILogger<MigrateRatingLevels> logger, IServerApplicationPaths paths) + { + _logger = logger; + _paths = paths; + } + + /// <inheritdoc/> + public Guid Id => Guid.Parse("{67445D54-B895-4B24-9F4C-35CE0690EA07}"); + + /// <inheritdoc/> + public string Name => "RemoveDuplicateExtras"; + + /// <inheritdoc/> + public bool PerformOnNewInstall => false; + + /// <inheritdoc/> + public void Perform() + { + var dataPath = _paths.DataPath; + var dbPath = Path.Combine(dataPath, DbFilename); + using (var connection = SQLite3.Open( + dbPath, + ConnectionFlags.ReadWrite, + null)) + { + // Back up the database before deleting any entries + for (int i = 1; ; i++) + { + var bakPath = string.Format(CultureInfo.InvariantCulture, "{0}.bak{1}", dbPath, i); + if (!File.Exists(bakPath)) + { + try + { + File.Copy(dbPath, bakPath); + _logger.LogInformation("Library database backed up to {BackupPath}", bakPath); + break; + } + catch (Exception ex) + { + _logger.LogError(ex, "Cannot make a backup of {Library} at path {BackupPath}", DbFilename, bakPath); + throw; + } + } + } + + // Migrate parental rating levels to new schema + _logger.LogInformation("Migrating parental rating levels."); + connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = NULL WHERE OfficialRating = 'NR'"); + connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = NULL WHERE InheritedParentalRatingValue = ''"); + connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = NULL WHERE InheritedParentalRatingValue = 0"); + connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 1000 WHERE InheritedParentalRatingValue = 100"); + connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 1000 WHERE InheritedParentalRatingValue = 15"); + connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 18 WHERE InheritedParentalRatingValue = 10"); + connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 18 WHERE InheritedParentalRatingValue = 9"); + connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 16 WHERE InheritedParentalRatingValue = 8"); + connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 12 WHERE InheritedParentalRatingValue = 7"); + connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 12 WHERE InheritedParentalRatingValue = 6"); + connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 12 WHERE InheritedParentalRatingValue = 5"); + connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 7 WHERE InheritedParentalRatingValue = 4"); + connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 6 WHERE InheritedParentalRatingValue = 3"); + connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 6 WHERE InheritedParentalRatingValue = 2"); + connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 0 WHERE InheritedParentalRatingValue = 1"); + } + } + } +} From 2e315b7f085ca0b9a2f61a7ac71a46ca84750f9f Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Mon, 28 Nov 2022 13:07:57 +0100 Subject: [PATCH 085/858] Properly build where clause for rating checks --- .../Data/SqliteItemRepository.cs | 137 +++++++++++++----- .../Localization/LocalizationManager.cs | 2 +- .../Controllers/ItemUpdateController.cs | 2 +- Jellyfin.Api/Controllers/ItemsController.cs | 7 + 4 files changed, 109 insertions(+), 39 deletions(-) diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 90f03995e8..797990d296 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -4020,34 +4020,116 @@ namespace Emby.Server.Implementations.Data whereClauses.Add(clause); } - if (query.MinParentalRating.HasValue) + var ratingClause = "("; + if (query.HasParentalRating.HasValue && query.HasParentalRating.Value) { - whereClauses.Add("InheritedParentalRatingValue>=@MinParentalRating"); - if (statement is not null) + ratingClause += "InheritedParentalRatingValue not null"; + if (query.MinParentalRating.HasValue) + { + ratingClause += " AND InheritedParentalRatingValue >= @MinParentalRating"; + if (statement is not null) + { + statement.TryBind("@MinParentalRating", query.MinParentalRating.Value); + } + } + + if (query.MaxParentalRating.HasValue) + { + ratingClause += " AND InheritedParentalRatingValue <= @MaxParentalRating"; + if (statement is not null) + { + statement.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); + } + } + } + else if (query.BlockUnratedItems.Length > 0) + { + var paramName = "@UnratedType"; + var index = 0; + string blockedUnratedItems = string.Join(',', query.BlockUnratedItems.Select(_ => paramName + index++)); + ratingClause += "(InheritedParentalRatingValue is null AND UnratedType not in (" + blockedUnratedItems + "))"; + + if (statement != null) + { + for (var ind = 0; ind < query.BlockUnratedItems.Length; ind++) + { + statement.TryBind(paramName + ind, query.BlockUnratedItems[ind].ToString()); + } + } + + if (query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue) + { + ratingClause += " OR ("; + } + + if (query.MinParentalRating.HasValue) + { + ratingClause += "InheritedParentalRatingValue >= @MinParentalRating"; + if (statement != null) + { + statement.TryBind("@MinParentalRating", query.MinParentalRating.Value); + } + } + + if (query.MaxParentalRating.HasValue) + { + if (query.MinParentalRating.HasValue) + { + ratingClause += " AND "; + } + + ratingClause += "InheritedParentalRatingValue <= @MaxParentalRating"; + if (statement != null) + { + statement.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); + } + } + + if (query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue) + { + ratingClause += ")"; + } + + if (!(query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue)) + { + ratingClause += " OR InheritedParentalRatingValue not null"; + } + } + else if (query.MinParentalRating.HasValue) + { + ratingClause += "InheritedParentalRatingValue is null OR (InheritedParentalRatingValue >= @MinParentalRating"; + if (statement != null) { statement.TryBind("@MinParentalRating", query.MinParentalRating.Value); } - } - if (query.MaxParentalRating.HasValue) + if (query.MaxParentalRating.HasValue) + { + ratingClause += " AND InheritedParentalRatingValue <= @MaxParentalRating"; + if (statement != null) + { + statement.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); + } + } + + ratingClause += ")"; + } + else if (query.MaxParentalRating.HasValue) { - whereClauses.Add("InheritedParentalRatingValue<=@MaxParentalRating"); - if (statement is not null) + ratingClause += "InheritedParentalRatingValue is null OR InheritedParentalRatingValue <= @MaxParentalRating"; + if (statement != null) { statement.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); } } - - if (query.HasParentalRating.HasValue) + else if (query.HasParentalRating.HasValue && !query.HasParentalRating.Value) { - if (query.HasParentalRating.Value) - { - whereClauses.Add("InheritedParentalRatingValue > 0"); - } - else - { - whereClauses.Add("InheritedParentalRatingValue = 0"); - } + ratingClause += "InheritedParentalRatingValue is null"; + } + + if (!string.Equals(ratingClause, "(", StringComparison.OrdinalIgnoreCase)) + { + whereClauses.Add(ratingClause + ")"); } if (query.HasOfficialRating.HasValue) @@ -4312,7 +4394,7 @@ namespace Emby.Server.Implementations.Data } // TODO this seems to be an idea for a better schema where ProviderIds are their own table - // buut this is not implemented + // but this is not implemented // hasProviderIds.Add("(COALESCE((select value from ProviderIds where ItemId=Guid and Name = '" + pair.Key + "'), '') <> " + paramName + ")"); // TODO this is a really BAD way to do it since the pair: @@ -4440,25 +4522,6 @@ namespace Emby.Server.Implementations.Data } } - if (query.BlockUnratedItems.Length == 1) - { - whereClauses.Add("(InheritedParentalRatingValue > 0 or UnratedType <> @UnratedType)"); - if (statement is not null) - { - statement.TryBind("@UnratedType", query.BlockUnratedItems[0].ToString()); - } - } - - if (query.BlockUnratedItems.Length > 1) - { - var inClause = string.Join(',', query.BlockUnratedItems.Select(i => "'" + i.ToString() + "'")); - whereClauses.Add( - string.Format( - CultureInfo.InvariantCulture, - "(InheritedParentalRatingValue > 0 or UnratedType not in ({0}))", - inClause)); - } - if (query.ExcludeInheritedTags.Length > 0) { var paramName = "@ExcludeInheritedTags"; diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index 5e22e78867..0e19e80a88 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -273,7 +273,7 @@ namespace Emby.Server.Implementations.Localization } } - // TODO: Further improve by normalizing out all spaces and dashes + // TODO: Further improve when necessary return null; } diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs index 57d446dbd0..9c71482413 100644 --- a/Jellyfin.Api/Controllers/ItemUpdateController.cs +++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs @@ -147,7 +147,7 @@ public class ItemUpdateController : BaseJellyfinApiController var info = new MetadataEditorInfo { - ParentalRatingOptions = _localizationManager.GetParentalRatings().ToArray(), + ParentalRatingOptions = _localizationManager.GetParentalRatings().ToList(), ExternalIdInfos = _providerManager.GetExternalIdInfos(item).ToArray(), Countries = _localizationManager.GetCountries().ToArray(), Cultures = _localizationManager.GetCultures().ToArray() diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 728e628109..377526729a 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -411,6 +411,13 @@ public class ItemsController : BaseJellyfinApiController query.SeriesStatuses = seriesStatus; } + // Exclude Blocked Unrated Items + var blockedUnratedItems = user?.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems); + if (blockedUnratedItems is not null) + { + query.BlockUnratedItems = blockedUnratedItems; + } + // ExcludeLocationTypes if (excludeLocationTypes.Any(t => t == LocationType.Virtual)) { From e014522dff2e9ba83d27b9d874559c9cd52fd5f5 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Mon, 28 Nov 2022 18:06:56 +0100 Subject: [PATCH 086/858] Add default for MaxParentalRating to UserPolicy --- MediaBrowser.Model/Users/UserPolicy.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs index cc7d57eb0a..80f5e2c37e 100644 --- a/MediaBrowser.Model/Users/UserPolicy.cs +++ b/MediaBrowser.Model/Users/UserPolicy.cs @@ -46,6 +46,7 @@ namespace MediaBrowser.Model.Users LoginAttemptsBeforeLockout = -1; MaxActiveSessions = 0; + MaxParentalRating = null; EnableAllChannels = true; EnabledChannels = Array.Empty<Guid>(); From 4ed97a459339f79b0864bf4db24ffe84f9b22768 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Tue, 29 Nov 2022 18:00:56 +0100 Subject: [PATCH 087/858] Properly check for item visibility in UserLibraryController --- .../Controllers/UserLibraryController.cs | 159 +++++++++++++++--- 1 file changed, 139 insertions(+), 20 deletions(-) diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs index 1233995b4c..2c4fe91862 100644 --- a/Jellyfin.Api/Controllers/UserLibraryController.cs +++ b/Jellyfin.Api/Controllers/UserLibraryController.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Extensions; using Jellyfin.Api.ModelBinders; +using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; @@ -72,7 +73,7 @@ public class UserLibraryController : BaseJellyfinApiController /// <param name="userId">User id.</param> /// <param name="itemId">Item id.</param> /// <response code="200">Item returned.</response> - /// <returns>An <see cref="OkResult"/> containing the d item.</returns> + /// <returns>An <see cref="OkResult"/> containing the item.</returns> [HttpGet("Users/{userId}/Items/{itemId}")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task<ActionResult<BaseItemDto>> GetItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) @@ -86,11 +87,19 @@ public class UserLibraryController : BaseJellyfinApiController var item = itemId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); + if (item is null) { return NotFound(); } + if (item is not UserRootFolder + // Check the item is visible for the user + && !item.IsVisible(user)) + { + return Unauthorized($"{user.Username} is not permitted to access item {item.Name}."); + } + await RefreshItemOnDemandIfNeeded(item).ConfigureAwait(false); var dtoOptions = new DtoOptions().AddClientFields(User); @@ -139,11 +148,19 @@ public class UserLibraryController : BaseJellyfinApiController var item = itemId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); + if (item is null) { return NotFound(); } + if (item is not UserRootFolder + // Check the item is visible for the user + && !item.IsVisible(user)) + { + return Unauthorized($"{user.Username} is not permitted to access item {item.Name}."); + } + var items = await _libraryManager.GetIntros(item, user).ConfigureAwait(false); var dtoOptions = new DtoOptions().AddClientFields(User); var dtos = items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)).ToArray(); @@ -162,7 +179,29 @@ public class UserLibraryController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<UserItemDataDto> MarkFavoriteItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) { - return MarkFavorite(userId, itemId, true); + var user = _userManager.GetUserById(userId); + if (user is null) + { + return NotFound(); + } + + var item = itemId.Equals(default) + ? _libraryManager.GetUserRootFolder() + : _libraryManager.GetItemById(itemId); + + if (item is null) + { + return NotFound(); + } + + if (item is not UserRootFolder + // Check the item is visible for the user + && !item.IsVisible(user)) + { + return Unauthorized($"{user.Username} is not permitted to access item {item.Name}."); + } + + return MarkFavorite(user, item, true); } /// <summary> @@ -176,7 +215,29 @@ public class UserLibraryController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<UserItemDataDto> UnmarkFavoriteItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) { - return MarkFavorite(userId, itemId, false); + var user = _userManager.GetUserById(userId); + if (user is null) + { + return NotFound(); + } + + var item = itemId.Equals(default) + ? _libraryManager.GetUserRootFolder() + : _libraryManager.GetItemById(itemId); + + if (item is null) + { + return NotFound(); + } + + if (item is not UserRootFolder + // Check the item is visible for the user + && !item.IsVisible(user)) + { + return Unauthorized($"{user.Username} is not permitted to access item {item.Name}."); + } + + return MarkFavorite(user, item, false); } /// <summary> @@ -190,7 +251,29 @@ public class UserLibraryController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<UserItemDataDto> DeleteUserItemRating([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) { - return UpdateUserItemRatingInternal(userId, itemId, null); + var user = _userManager.GetUserById(userId); + if (user is null) + { + return NotFound(); + } + + var item = itemId.Equals(default) + ? _libraryManager.GetUserRootFolder() + : _libraryManager.GetItemById(itemId); + + if (item is null) + { + return NotFound(); + } + + if (item is not UserRootFolder + // Check the item is visible for the user + && !item.IsVisible(user)) + { + return Unauthorized($"{user.Username} is not permitted to access item {item.Name}."); + } + + return UpdateUserItemRatingInternal(user, item, null); } /// <summary> @@ -205,7 +288,29 @@ public class UserLibraryController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<UserItemDataDto> UpdateUserItemRating([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId, [FromQuery] bool? likes) { - return UpdateUserItemRatingInternal(userId, itemId, likes); + var user = _userManager.GetUserById(userId); + if (user is null) + { + return NotFound(); + } + + var item = itemId.Equals(default) + ? _libraryManager.GetUserRootFolder() + : _libraryManager.GetItemById(itemId); + + if (item is null) + { + return NotFound(); + } + + if (item is not UserRootFolder + // Check the item is visible for the user + && !item.IsVisible(user)) + { + return Unauthorized($"{user.Username} is not permitted to access item {item.Name}."); + } + + return UpdateUserItemRatingInternal(user, item, likes); } /// <summary> @@ -228,13 +333,20 @@ public class UserLibraryController : BaseJellyfinApiController var item = itemId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); + if (item is null) { return NotFound(); } - var dtoOptions = new DtoOptions().AddClientFields(User); + if (item is not UserRootFolder + // Check the item is visible for the user + && !item.IsVisible(user)) + { + return Unauthorized($"{user.Username} is not permitted to access item {item.Name}."); + } + var dtoOptions = new DtoOptions().AddClientFields(User); if (item is IHasTrailers hasTrailers) { var trailers = hasTrailers.LocalTrailers; @@ -266,11 +378,19 @@ public class UserLibraryController : BaseJellyfinApiController var item = itemId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); + if (item is null) { return NotFound(); } + if (item is not UserRootFolder + // Check the item is visible for the user + && !item.IsVisible(user)) + { + return Unauthorized($"{user.Username} is not permitted to access item {item.Name}."); + } + var dtoOptions = new DtoOptions().AddClientFields(User); return Ok(item @@ -385,15 +505,11 @@ public class UserLibraryController : BaseJellyfinApiController /// <summary> /// Marks the favorite. /// </summary> - /// <param name="userId">The user id.</param> - /// <param name="itemId">The item id.</param> + /// <param name="user">The user.</param> + /// <param name="item">The item.</param> /// <param name="isFavorite">if set to <c>true</c> [is favorite].</param> - private UserItemDataDto MarkFavorite(Guid userId, Guid itemId, bool isFavorite) + private UserItemDataDto MarkFavorite(User user, BaseItem item, bool isFavorite) { - var user = _userManager.GetUserById(userId); - - var item = itemId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); - // Get the user data for this item var data = _userDataRepository.GetUserData(user, item); @@ -408,15 +524,11 @@ public class UserLibraryController : BaseJellyfinApiController /// <summary> /// Updates the user item rating. /// </summary> - /// <param name="userId">The user id.</param> - /// <param name="itemId">The item id.</param> + /// <param name="user">The user.</param> + /// <param name="item">The item.</param> /// <param name="likes">if set to <c>true</c> [likes].</param> - private UserItemDataDto UpdateUserItemRatingInternal(Guid userId, Guid itemId, bool? likes) + private UserItemDataDto UpdateUserItemRatingInternal(User user, BaseItem item, bool? likes) { - var user = _userManager.GetUserById(userId); - - var item = itemId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); - // Get the user data for this item var data = _userDataRepository.GetUserData(user, item); @@ -455,6 +567,13 @@ public class UserLibraryController : BaseJellyfinApiController return NotFound(); } + if (item is not UserRootFolder + // Check the item is visible for the user + && !item.IsVisible(user)) + { + return Unauthorized($"{user.Username} is not permitted to access item {item.Name}."); + } + var result = await _lyricManager.GetLyrics(item).ConfigureAwait(false); if (result is not null) { From 9d21f078c71373d956893d2d298cc4d7fcc0adf1 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Tue, 29 Nov 2022 22:04:28 +0100 Subject: [PATCH 088/858] Add default rating selections --- .../Localization/LocalizationManager.cs | 59 +++++++++++++++++-- MediaBrowser.Model/Entities/ParentalRating.cs | 4 +- .../Localization/LocalizationManagerTests.cs | 4 +- 3 files changed, 57 insertions(+), 10 deletions(-) diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index 0e19e80a88..ca7c2c21f1 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -87,12 +87,10 @@ namespace Emby.Server.Implementations.Localization var name = parts[0]; dict.Add(name, new ParentalRating(name, value)); } -#if DEBUG else { _logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode); } -#endif } _allParentalRatings[countryCode] = dict; @@ -185,7 +183,56 @@ namespace Emby.Server.Implementations.Localization /// <inheritdoc /> public IEnumerable<ParentalRating> GetParentalRatings() - => GetParentalRatingsDictionary().Values; + { + var ratings = GetParentalRatingsDictionary().Values.ToList(); + + // Add common ratings to ensure them being available for selection. + // Based on the US rating system due to it being the main source of rating in the metadata providers + // Minimum rating possible + if (!ratings.Any(x => x.Value == 0)) + { + ratings.Add(new ParentalRating("Approved", 0)); + } + + // Matches PG (this has differnet age restrictions depending on country) + if (!ratings.Any(x => x.Value == 10)) + { + ratings.Add(new ParentalRating("10", 10)); + } + + // Matches PG-13 + if (!ratings.Any(x => x.Value == 13)) + { + ratings.Add(new ParentalRating("13", 13)); + } + + // Matches TV-14 + if (!ratings.Any(x => x.Value == 14)) + { + ratings.Add(new ParentalRating("14", 14)); + } + + // Catchall if max rating of country is less than 21 + // Using 21 instead of 18 to be sure to allow access to all rated content except adult and banned + if (!ratings.Any(x => x.Value >= 21)) + { + ratings.Add(new ParentalRating("21", 21)); + } + + // A lot of countries don't excplicitly have a seperate rating for adult content + if (!ratings.Any(x => x.Value == 1000)) + { + ratings.Add(new ParentalRating("XXX", 1000)); + } + + // A lot of countries don't excplicitly have a seperate rating for banned content + if (!ratings.Any(x => x.Value == 1001)) + { + ratings.Add(new ParentalRating("Banned", 1001)); + } + + return ratings.OrderBy(r => r.Value); + } /// <summary> /// Gets the parental ratings dictionary. @@ -207,15 +254,15 @@ namespace Emby.Server.Implementations.Localization } /// <summary> - /// Gets the ratings. + /// Gets the ratings for a country. /// </summary> /// <param name="countryCode">The country code.</param> /// <returns>The ratings.</returns> private Dictionary<string, ParentalRating>? GetRatings(string countryCode) { - _allParentalRatings.TryGetValue(countryCode, out var value); + _allParentalRatings.TryGetValue(countryCode, out var countryValue); - return value; + return countryValue; } /// <inheritdoc /> diff --git a/MediaBrowser.Model/Entities/ParentalRating.cs b/MediaBrowser.Model/Entities/ParentalRating.cs index 17b2868a31..c92640818c 100644 --- a/MediaBrowser.Model/Entities/ParentalRating.cs +++ b/MediaBrowser.Model/Entities/ParentalRating.cs @@ -12,7 +12,7 @@ namespace MediaBrowser.Model.Entities { } - public ParentalRating(string name, int value) + public ParentalRating(string name, int? value) { Name = name; Value = value; @@ -28,6 +28,6 @@ namespace MediaBrowser.Model.Entities /// Gets or sets the value. /// </summary> /// <value>The value.</value> - public int Value { get; set; } + public int? Value { get; set; } } } diff --git a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs index 256bde8194..ab3682ccf6 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs @@ -83,7 +83,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization await localizationManager.LoadAll(); var ratings = localizationManager.GetParentalRatings().ToList(); - Assert.Equal(50, ratings.Count); + Assert.Equal(53, ratings.Count); var tvma = ratings.FirstOrDefault(x => x.Name.Equals("TV-MA", StringComparison.Ordinal)); Assert.NotNull(tvma); @@ -100,7 +100,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization await localizationManager.LoadAll(); var ratings = localizationManager.GetParentalRatings().ToList(); - Assert.Equal(12, ratings.Count); + Assert.Equal(18, ratings.Count); var fsk = ratings.FirstOrDefault(x => x.Name.Equals("FSK-12", StringComparison.Ordinal)); Assert.NotNull(fsk); From 07dc163844091e3335081578ed5cc7fd427c5c0d Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Wed, 30 Nov 2022 11:55:40 +0100 Subject: [PATCH 089/858] Fix playlist parental control and no parental control skipping forbidden unrated items --- Emby.Server.Implementations/Dto/DtoService.cs | 7 ++++--- MediaBrowser.Controller/Entities/BaseItem.cs | 15 +++++---------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 5103b1fbfb..e928f1ff3a 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -83,13 +83,14 @@ namespace Emby.Server.Implementations.Dto /// <inheritdoc /> public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null) { - var returnItems = new BaseItemDto[items.Count]; + var accessibleItems = user is null ? items : items.Where(x => x.IsVisible(user)).ToList(); + var returnItems = new BaseItemDto[accessibleItems.Count]; var programTuples = new List<(BaseItem, BaseItemDto)>(); var channelTuples = new List<(BaseItemDto, LiveTvChannel)>(); - for (int index = 0; index < items.Count; index++) + for (int index = 0; index < accessibleItems.Count; index++) { - var item = items[index]; + var item = accessibleItems[index]; var dto = GetBaseItemDtoInternal(item, options, user, owner); if (item is LiveTvChannel tvChannel) diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index cf369e84b2..b8601cccd9 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1534,12 +1534,6 @@ namespace MediaBrowser.Controller.Entities } var maxAllowedRating = user.MaxParentalAgeRating; - - if (maxAllowedRating is null) - { - return true; - } - var rating = CustomRatingForComparison; if (string.IsNullOrEmpty(rating)) @@ -1549,12 +1543,13 @@ namespace MediaBrowser.Controller.Entities if (string.IsNullOrEmpty(rating)) { + Logger.LogDebug("{0} has no parental rating set.", Name); return !GetBlockUnratedValue(user); } var value = LocalizationManager.GetRatingLevel(rating); - // Could not determine the integer value + // Could not determine rating level if (!value.HasValue) { var isAllowed = !GetBlockUnratedValue(user); @@ -1567,7 +1562,7 @@ namespace MediaBrowser.Controller.Entities return isAllowed; } - return value.Value <= maxAllowedRating.Value; + return !maxAllowedRating.HasValue || value.Value <= maxAllowedRating.Value; } public int? GetInheritedParentalRatingValue() @@ -1627,10 +1622,10 @@ namespace MediaBrowser.Controller.Entities } /// <summary> - /// Gets the block unrated value. + /// Gets a bool indicating if access to the unrated item is blocked or not. /// </summary> /// <param name="user">The configuration.</param> - /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns> + /// <returns><c>true</c> if blocked, <c>false</c> otherwise.</returns> protected virtual bool GetBlockUnratedValue(User user) { // Don't block plain folders that are unrated. Let the media underneath get blocked From 15efb9935c2f786c12343f158ef2c9db542e71ba Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Thu, 8 Dec 2022 16:15:26 +0100 Subject: [PATCH 090/858] Fix typo and migration description --- Emby.Server.Implementations/Localization/LocalizationManager.cs | 2 +- Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index ca7c2c21f1..8faebadd61 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -194,7 +194,7 @@ namespace Emby.Server.Implementations.Localization ratings.Add(new ParentalRating("Approved", 0)); } - // Matches PG (this has differnet age restrictions depending on country) + // Matches PG (this has different age restrictions depending on country) if (!ratings.Any(x => x.Value == 10)) { ratings.Add(new ParentalRating("10", 10)); diff --git a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs index 5b27681f61..21f1872bc5 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs @@ -9,7 +9,7 @@ using SQLitePCL.pretty; namespace Jellyfin.Server.Migrations.Routines { /// <summary> - /// Remove duplicate entries which were caused by a bug where a file was considered to be an "Extra" to itself. + /// Migrate rating levels to new rating level system. /// </summary> internal class MigrateRatingLevels : IMigrationRoutine { From 5cdb0c7932303d2d9a59d7d21860172fd97fe031 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Fri, 27 Jan 2023 15:49:08 +0100 Subject: [PATCH 091/858] Apply review suggestions --- .../Data/SqliteItemRepository.cs | 208 ++++-------------- .../Localization/LocalizationManager.cs | 21 +- .../Localization/Ratings/be.csv | 2 +- .../Routines/MigrateRatingLevels.cs | 2 +- 4 files changed, 46 insertions(+), 187 deletions(-) diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 797990d296..055131c8e6 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -3202,7 +3202,8 @@ namespace Emby.Server.Implementations.Data return IsAlphaNumeric(value); } - private List<string> GetWhereClauses(InternalItemsQuery query, IStatement statement) +#nullable enable + private List<string> GetWhereClauses(InternalItemsQuery query, IStatement? statement) { if (query.IsResumable ?? false) { @@ -3622,12 +3623,8 @@ namespace Emby.Server.Implementations.Data clauseBuilder.Append("(guid in (select itemid from People where Name = (select Name from TypedBaseItems where guid=") .Append(paramName) .Append("))) OR "); - - if (statement is not null) - { - query.PersonIds[i].TryWriteBytes(idBytes); - statement.TryBind(paramName, idBytes); - } + query.PersonIds[i].TryWriteBytes(idBytes); + statement?.TryBind(paramName, idBytes); } // Remove last " OR " @@ -3677,7 +3674,6 @@ namespace Emby.Server.Implementations.Data if (statement is not null) { nameContains = FixUnicodeChars(nameContains); - statement.TryBind("@NameContains", "%" + GetCleanValue(nameContains) + "%"); } } @@ -3803,13 +3799,8 @@ namespace Emby.Server.Implementations.Data foreach (var artistId in query.ArtistIds) { var paramName = "@ArtistIds" + index; - clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))"); - if (statement is not null) - { - statement.TryBind(paramName, artistId); - } - + statement?.TryBind(paramName, artistId); index++; } @@ -3824,13 +3815,8 @@ namespace Emby.Server.Implementations.Data foreach (var artistId in query.AlbumArtistIds) { var paramName = "@ArtistIds" + index; - clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=1))"); - if (statement is not null) - { - statement.TryBind(paramName, artistId); - } - + statement?.TryBind(paramName, artistId); index++; } @@ -3845,13 +3831,8 @@ namespace Emby.Server.Implementations.Data foreach (var artistId in query.ContributingArtistIds) { var paramName = "@ArtistIds" + index; - clauses.Add("((select CleanName from TypedBaseItems where guid=" + paramName + ") in (select CleanValue from ItemValues where ItemId=Guid and Type=0) AND (select CleanName from TypedBaseItems where guid=" + paramName + ") not in (select CleanValue from ItemValues where ItemId=Guid and Type=1))"); - if (statement is not null) - { - statement.TryBind(paramName, artistId); - } - + statement?.TryBind(paramName, artistId); index++; } @@ -3866,13 +3847,8 @@ namespace Emby.Server.Implementations.Data foreach (var albumId in query.AlbumIds) { var paramName = "@AlbumIds" + index; - clauses.Add("Album in (select Name from typedbaseitems where guid=" + paramName + ")"); - if (statement is not null) - { - statement.TryBind(paramName, albumId); - } - + statement?.TryBind(paramName, albumId); index++; } @@ -3887,13 +3863,8 @@ namespace Emby.Server.Implementations.Data foreach (var artistId in query.ExcludeArtistIds) { var paramName = "@ExcludeArtistId" + index; - clauses.Add("(guid not in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))"); - if (statement is not null) - { - statement.TryBind(paramName, artistId); - } - + statement?.TryBind(paramName, artistId); index++; } @@ -3908,13 +3879,8 @@ namespace Emby.Server.Implementations.Data foreach (var genreId in query.GenreIds) { var paramName = "@GenreId" + index; - clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=2))"); - if (statement is not null) - { - statement.TryBind(paramName, genreId); - } - + statement?.TryBind(paramName, genreId); index++; } @@ -3929,11 +3895,7 @@ namespace Emby.Server.Implementations.Data foreach (var item in query.Genres) { clauses.Add("@Genre" + index + " in (select CleanValue from ItemValues where ItemId=Guid and Type=2)"); - if (statement is not null) - { - statement.TryBind("@Genre" + index, GetCleanValue(item)); - } - + statement?.TryBind("@Genre" + index, GetCleanValue(item)); index++; } @@ -3948,11 +3910,7 @@ namespace Emby.Server.Implementations.Data foreach (var item in tags) { clauses.Add("@Tag" + index + " in (select CleanValue from ItemValues where ItemId=Guid and Type=4)"); - if (statement is not null) - { - statement.TryBind("@Tag" + index, GetCleanValue(item)); - } - + statement?.TryBind("@Tag" + index, GetCleanValue(item)); index++; } @@ -3967,11 +3925,7 @@ namespace Emby.Server.Implementations.Data foreach (var item in excludeTags) { clauses.Add("@ExcludeTag" + index + " not in (select CleanValue from ItemValues where ItemId=Guid and Type=4)"); - if (statement is not null) - { - statement.TryBind("@ExcludeTag" + index, GetCleanValue(item)); - } - + statement?.TryBind("@ExcludeTag" + index, GetCleanValue(item)); index++; } @@ -3986,14 +3940,8 @@ namespace Emby.Server.Implementations.Data foreach (var studioId in query.StudioIds) { var paramName = "@StudioId" + index; - clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=3))"); - - if (statement is not null) - { - statement.TryBind(paramName, studioId); - } - + statement?.TryBind(paramName, studioId); index++; } @@ -4008,11 +3956,7 @@ namespace Emby.Server.Implementations.Data foreach (var item in query.OfficialRatings) { clauses.Add("OfficialRating=@OfficialRating" + index); - if (statement is not null) - { - statement.TryBind("@OfficialRating" + index, item); - } - + statement?.TryBind("@OfficialRating" + index, item); index++; } @@ -4021,25 +3965,19 @@ namespace Emby.Server.Implementations.Data } var ratingClause = "("; - if (query.HasParentalRating.HasValue && query.HasParentalRating.Value) + if (query.HasParentalRating ?? false) { ratingClause += "InheritedParentalRatingValue not null"; if (query.MinParentalRating.HasValue) { ratingClause += " AND InheritedParentalRatingValue >= @MinParentalRating"; - if (statement is not null) - { - statement.TryBind("@MinParentalRating", query.MinParentalRating.Value); - } + statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value); } if (query.MaxParentalRating.HasValue) { ratingClause += " AND InheritedParentalRatingValue <= @MaxParentalRating"; - if (statement is not null) - { - statement.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); - } + statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); } } else if (query.BlockUnratedItems.Length > 0) @@ -4049,7 +3987,7 @@ namespace Emby.Server.Implementations.Data string blockedUnratedItems = string.Join(',', query.BlockUnratedItems.Select(_ => paramName + index++)); ratingClause += "(InheritedParentalRatingValue is null AND UnratedType not in (" + blockedUnratedItems + "))"; - if (statement != null) + if (statement is not null) { for (var ind = 0; ind < query.BlockUnratedItems.Length; ind++) { @@ -4065,10 +4003,7 @@ namespace Emby.Server.Implementations.Data if (query.MinParentalRating.HasValue) { ratingClause += "InheritedParentalRatingValue >= @MinParentalRating"; - if (statement != null) - { - statement.TryBind("@MinParentalRating", query.MinParentalRating.Value); - } + statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value); } if (query.MaxParentalRating.HasValue) @@ -4079,10 +4014,7 @@ namespace Emby.Server.Implementations.Data } ratingClause += "InheritedParentalRatingValue <= @MaxParentalRating"; - if (statement != null) - { - statement.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); - } + statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); } if (query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue) @@ -4098,18 +4030,12 @@ namespace Emby.Server.Implementations.Data else if (query.MinParentalRating.HasValue) { ratingClause += "InheritedParentalRatingValue is null OR (InheritedParentalRatingValue >= @MinParentalRating"; - if (statement != null) - { - statement.TryBind("@MinParentalRating", query.MinParentalRating.Value); - } + statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value); if (query.MaxParentalRating.HasValue) { ratingClause += " AND InheritedParentalRatingValue <= @MaxParentalRating"; - if (statement != null) - { - statement.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); - } + statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); } ratingClause += ")"; @@ -4117,12 +4043,9 @@ namespace Emby.Server.Implementations.Data else if (query.MaxParentalRating.HasValue) { ratingClause += "InheritedParentalRatingValue is null OR InheritedParentalRatingValue <= @MaxParentalRating"; - if (statement != null) - { - statement.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); - } + statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); } - else if (query.HasParentalRating.HasValue && !query.HasParentalRating.Value) + else if (!query.HasParentalRating ?? false) { ratingClause += "InheritedParentalRatingValue is null"; } @@ -4171,37 +4094,25 @@ namespace Emby.Server.Implementations.Data if (!string.IsNullOrWhiteSpace(query.HasNoAudioTrackWithLanguage)) { whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Audio' and MediaStreams.Language=@HasNoAudioTrackWithLanguage limit 1) is null)"); - if (statement is not null) - { - statement.TryBind("@HasNoAudioTrackWithLanguage", query.HasNoAudioTrackWithLanguage); - } + statement?.TryBind("@HasNoAudioTrackWithLanguage", query.HasNoAudioTrackWithLanguage); } if (!string.IsNullOrWhiteSpace(query.HasNoInternalSubtitleTrackWithLanguage)) { whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' and MediaStreams.IsExternal=0 and MediaStreams.Language=@HasNoInternalSubtitleTrackWithLanguage limit 1) is null)"); - if (statement is not null) - { - statement.TryBind("@HasNoInternalSubtitleTrackWithLanguage", query.HasNoInternalSubtitleTrackWithLanguage); - } + statement?.TryBind("@HasNoInternalSubtitleTrackWithLanguage", query.HasNoInternalSubtitleTrackWithLanguage); } if (!string.IsNullOrWhiteSpace(query.HasNoExternalSubtitleTrackWithLanguage)) { whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' and MediaStreams.IsExternal=1 and MediaStreams.Language=@HasNoExternalSubtitleTrackWithLanguage limit 1) is null)"); - if (statement is not null) - { - statement.TryBind("@HasNoExternalSubtitleTrackWithLanguage", query.HasNoExternalSubtitleTrackWithLanguage); - } + statement?.TryBind("@HasNoExternalSubtitleTrackWithLanguage", query.HasNoExternalSubtitleTrackWithLanguage); } if (!string.IsNullOrWhiteSpace(query.HasNoSubtitleTrackWithLanguage)) { whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' and MediaStreams.Language=@HasNoSubtitleTrackWithLanguage limit 1) is null)"); - if (statement is not null) - { - statement.TryBind("@HasNoSubtitleTrackWithLanguage", query.HasNoSubtitleTrackWithLanguage); - } + statement?.TryBind("@HasNoSubtitleTrackWithLanguage", query.HasNoSubtitleTrackWithLanguage); } if (query.HasSubtitles.HasValue) @@ -4251,15 +4162,11 @@ namespace Emby.Server.Implementations.Data if (query.Years.Length == 1) { whereClauses.Add("ProductionYear=@Years"); - if (statement is not null) - { - statement.TryBind("@Years", query.Years[0].ToString(CultureInfo.InvariantCulture)); - } + statement?.TryBind("@Years", query.Years[0].ToString(CultureInfo.InvariantCulture)); } else if (query.Years.Length > 1) { var val = string.Join(',', query.Years); - whereClauses.Add("ProductionYear in (" + val + ")"); } @@ -4267,10 +4174,7 @@ namespace Emby.Server.Implementations.Data if (isVirtualItem.HasValue) { whereClauses.Add("IsVirtualItem=@IsVirtualItem"); - if (statement is not null) - { - statement.TryBind("@IsVirtualItem", isVirtualItem.Value); - } + statement?.TryBind("@IsVirtualItem", isVirtualItem.Value); } if (query.IsSpecialSeason.HasValue) @@ -4301,31 +4205,22 @@ namespace Emby.Server.Implementations.Data if (queryMediaTypes.Length == 1) { whereClauses.Add("MediaType=@MediaTypes"); - if (statement is not null) - { - statement.TryBind("@MediaTypes", queryMediaTypes[0]); - } + statement?.TryBind("@MediaTypes", queryMediaTypes[0]); } else if (queryMediaTypes.Length > 1) { var val = string.Join(',', queryMediaTypes.Select(i => "'" + i + "'")); - whereClauses.Add("MediaType in (" + val + ")"); } if (query.ItemIds.Length > 0) { var includeIds = new List<string>(); - var index = 0; foreach (var id in query.ItemIds) { includeIds.Add("Guid = @IncludeId" + index); - if (statement is not null) - { - statement.TryBind("@IncludeId" + index, id); - } - + statement?.TryBind("@IncludeId" + index, id); index++; } @@ -4335,16 +4230,11 @@ namespace Emby.Server.Implementations.Data if (query.ExcludeItemIds.Length > 0) { var excludeIds = new List<string>(); - var index = 0; foreach (var id in query.ExcludeItemIds) { excludeIds.Add("Guid <> @ExcludeId" + index); - if (statement is not null) - { - statement.TryBind("@ExcludeId" + index, id); - } - + statement?.TryBind("@ExcludeId" + index, id); index++; } @@ -4365,11 +4255,7 @@ namespace Emby.Server.Implementations.Data var paramName = "@ExcludeProviderId" + index; excludeIds.Add("(ProviderIds is null or ProviderIds not like " + paramName + ")"); - if (statement is not null) - { - statement.TryBind(paramName, "%" + pair.Key + "=" + pair.Value + "%"); - } - + statement?.TryBind(paramName, "%" + pair.Key + "=" + pair.Value + "%"); index++; break; @@ -4408,11 +4294,7 @@ namespace Emby.Server.Implementations.Data hasProviderIds.Add("ProviderIds like " + paramName); // this replaces the placeholder with a value, here: %key=val% - if (statement is not null) - { - statement.TryBind(paramName, "%" + pair.Key + "=" + pair.Value + "%"); - } - + statement?.TryBind(paramName, "%" + pair.Key + "=" + pair.Value + "%"); index++; break; @@ -4489,11 +4371,7 @@ namespace Emby.Server.Implementations.Data if (query.AncestorIds.Length == 1) { whereClauses.Add("Guid in (select itemId from AncestorIds where AncestorId=@AncestorId)"); - - if (statement is not null) - { - statement.TryBind("@AncestorId", query.AncestorIds[0]); - } + statement?.TryBind("@AncestorId", query.AncestorIds[0]); } if (query.AncestorIds.Length > 1) @@ -4506,20 +4384,13 @@ namespace Emby.Server.Implementations.Data { var inClause = "select guid from TypedBaseItems where PresentationUniqueKey=@AncestorWithPresentationUniqueKey"; whereClauses.Add(string.Format(CultureInfo.InvariantCulture, "Guid in (select itemId from AncestorIds where AncestorId in ({0}))", inClause)); - if (statement is not null) - { - statement.TryBind("@AncestorWithPresentationUniqueKey", query.AncestorWithPresentationUniqueKey); - } + statement?.TryBind("@AncestorWithPresentationUniqueKey", query.AncestorWithPresentationUniqueKey); } if (!string.IsNullOrWhiteSpace(query.SeriesPresentationUniqueKey)) { whereClauses.Add("SeriesPresentationUniqueKey=@SeriesPresentationUniqueKey"); - - if (statement is not null) - { - statement.TryBind("@SeriesPresentationUniqueKey", query.SeriesPresentationUniqueKey); - } + statement?.TryBind("@SeriesPresentationUniqueKey", query.SeriesPresentationUniqueKey); } if (query.ExcludeInheritedTags.Length > 0) @@ -4668,6 +4539,7 @@ namespace Emby.Server.Implementations.Data return whereClauses; } +#nullable disable /// <summary> /// Formats a where clause for the specified provider. diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index 8faebadd61..6e2a33fd56 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -297,30 +297,17 @@ namespace Emby.Server.Implementations.Localization } // Try splitting by : to handle "Germany: FSK 18" - var index = rating.IndexOf(':', StringComparison.Ordinal); - if (index != -1) + if (rating.Contains(':', StringComparison.OrdinalIgnoreCase)) { - var trimmedRating = rating.AsSpan(index).TrimStart(':').Trim(); - - if (!trimmedRating.IsEmpty) - { - return GetRatingLevel(trimmedRating.ToString()); - } + return GetRatingLevel(rating.AsSpan().RightPart(':').ToString()); } // Remove prefix country code to handle "DE-18" - index = rating.IndexOf('-', StringComparison.Ordinal); - if (index != -1) + if (rating.Contains('-', StringComparison.OrdinalIgnoreCase)) { - var trimmedRating = rating.AsSpan(index).TrimStart('-').Trim(); - - if (!trimmedRating.IsEmpty) - { - return GetRatingLevel(trimmedRating.ToString()); - } + return GetRatingLevel(rating.AsSpan().RightPart('-').ToString()); } - // TODO: Further improve when necessary return null; } diff --git a/Emby.Server.Implementations/Localization/Ratings/be.csv b/Emby.Server.Implementations/Localization/Ratings/be.csv index 5588f63b45..d171a71328 100644 --- a/Emby.Server.Implementations/Localization/Ratings/be.csv +++ b/Emby.Server.Implementations/Localization/Ratings/be.csv @@ -6,6 +6,6 @@ MG6,6 9,9 KNT,12 12,12 -BE_14,14 +14,14 16,16 18,18 diff --git a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs index 21f1872bc5..f30eb84213 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs @@ -27,7 +27,7 @@ namespace Jellyfin.Server.Migrations.Routines public Guid Id => Guid.Parse("{67445D54-B895-4B24-9F4C-35CE0690EA07}"); /// <inheritdoc/> - public string Name => "RemoveDuplicateExtras"; + public string Name => "MigrateRatingLevels"; /// <inheritdoc/> public bool PerformOnNewInstall => false; From f0251f86cb7d88495de0000d0ebca01fd9b8bbbe Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Mon, 20 Feb 2023 11:49:40 +0100 Subject: [PATCH 092/858] Move MigrateRatingLevels migration to preStartup --- Jellyfin.Server/Migrations/MigrationRunner.cs | 6 +++--- .../MigrateRatingLevels.cs | 13 +++++++------ 2 files changed, 10 insertions(+), 9 deletions(-) rename Jellyfin.Server/Migrations/{Routines => PreStartupRoutines}/MigrateRatingLevels.cs (90%) diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs index 2b15a6a1b5..d4bf81f10b 100644 --- a/Jellyfin.Server/Migrations/MigrationRunner.cs +++ b/Jellyfin.Server/Migrations/MigrationRunner.cs @@ -22,7 +22,8 @@ namespace Jellyfin.Server.Migrations private static readonly Type[] _preStartupMigrationTypes = { typeof(PreStartupRoutines.CreateNetworkConfiguration), - typeof(PreStartupRoutines.MigrateMusicBrainzTimeout) + typeof(PreStartupRoutines.MigrateMusicBrainzTimeout), + typeof(PreStartupRoutines.MigrateRatingLevels) }; /// <summary> @@ -39,8 +40,7 @@ namespace Jellyfin.Server.Migrations typeof(Routines.ReaddDefaultPluginRepository), typeof(Routines.MigrateDisplayPreferencesDb), typeof(Routines.RemoveDownloadImagesInAdvance), - typeof(Routines.MigrateAuthenticationDb), - typeof(Routines.MigrateRatingLevels) + typeof(Routines.MigrateAuthenticationDb) }; /// <summary> diff --git a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateRatingLevels.cs similarity index 90% rename from Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs rename to Jellyfin.Server/Migrations/PreStartupRoutines/MigrateRatingLevels.cs index f30eb84213..465bbd7fe1 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs +++ b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateRatingLevels.cs @@ -2,11 +2,12 @@ using System; using System.Globalization; using System.IO; +using Emby.Server.Implementations; using MediaBrowser.Controller; using Microsoft.Extensions.Logging; using SQLitePCL.pretty; -namespace Jellyfin.Server.Migrations.Routines +namespace Jellyfin.Server.Migrations.PreStartupRoutines { /// <summary> /// Migrate rating levels to new rating level system. @@ -15,12 +16,12 @@ namespace Jellyfin.Server.Migrations.Routines { private const string DbFilename = "library.db"; private readonly ILogger<MigrateRatingLevels> _logger; - private readonly IServerApplicationPaths _paths; + private readonly IServerApplicationPaths _applicationPaths; - public MigrateRatingLevels(ILogger<MigrateRatingLevels> logger, IServerApplicationPaths paths) + public MigrateRatingLevels(ServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory) { - _logger = logger; - _paths = paths; + _applicationPaths = applicationPaths; + _logger = loggerFactory.CreateLogger<MigrateRatingLevels>(); } /// <inheritdoc/> @@ -35,7 +36,7 @@ namespace Jellyfin.Server.Migrations.Routines /// <inheritdoc/> public void Perform() { - var dataPath = _paths.DataPath; + var dataPath = _applicationPaths.DataPath; var dbPath = Path.Combine(dataPath, DbFilename); using (var connection = SQLite3.Open( dbPath, From 5f938de337537ac1e03633c82d2a27a34808d9d0 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Mon, 20 Feb 2023 11:49:46 +0100 Subject: [PATCH 093/858] Build ratingClause with StringBuilder --- .../Data/SqliteItemRepository.cs | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 055131c8e6..ecc2a2c910 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -3964,19 +3964,19 @@ namespace Emby.Server.Implementations.Data whereClauses.Add(clause); } - var ratingClause = "("; + var ratingClauseBuilder = new StringBuilder("("); if (query.HasParentalRating ?? false) { - ratingClause += "InheritedParentalRatingValue not null"; + ratingClauseBuilder.Append("InheritedParentalRatingValue not null"); if (query.MinParentalRating.HasValue) { - ratingClause += " AND InheritedParentalRatingValue >= @MinParentalRating"; + ratingClauseBuilder.Append(" AND InheritedParentalRatingValue >= @MinParentalRating"); statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value); } if (query.MaxParentalRating.HasValue) { - ratingClause += " AND InheritedParentalRatingValue <= @MaxParentalRating"; + ratingClauseBuilder.Append(" AND InheritedParentalRatingValue <= @MaxParentalRating"); statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); } } @@ -3985,7 +3985,7 @@ namespace Emby.Server.Implementations.Data var paramName = "@UnratedType"; var index = 0; string blockedUnratedItems = string.Join(',', query.BlockUnratedItems.Select(_ => paramName + index++)); - ratingClause += "(InheritedParentalRatingValue is null AND UnratedType not in (" + blockedUnratedItems + "))"; + ratingClauseBuilder.Append("(InheritedParentalRatingValue is null AND UnratedType not in (" + blockedUnratedItems + "))"); if (statement is not null) { @@ -3997,12 +3997,12 @@ namespace Emby.Server.Implementations.Data if (query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue) { - ratingClause += " OR ("; + ratingClauseBuilder.Append(" OR ("); } if (query.MinParentalRating.HasValue) { - ratingClause += "InheritedParentalRatingValue >= @MinParentalRating"; + ratingClauseBuilder.Append("InheritedParentalRatingValue >= @MinParentalRating"); statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value); } @@ -4010,49 +4010,50 @@ namespace Emby.Server.Implementations.Data { if (query.MinParentalRating.HasValue) { - ratingClause += " AND "; + ratingClauseBuilder.Append(" AND "); } - ratingClause += "InheritedParentalRatingValue <= @MaxParentalRating"; + ratingClauseBuilder.Append("InheritedParentalRatingValue <= @MaxParentalRating"); statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); } if (query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue) { - ratingClause += ")"; + ratingClauseBuilder.Append(")"); } if (!(query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue)) { - ratingClause += " OR InheritedParentalRatingValue not null"; + ratingClauseBuilder.Append(" OR InheritedParentalRatingValue not null"); } } else if (query.MinParentalRating.HasValue) { - ratingClause += "InheritedParentalRatingValue is null OR (InheritedParentalRatingValue >= @MinParentalRating"; + ratingClauseBuilder.Append("InheritedParentalRatingValue is null OR (InheritedParentalRatingValue >= @MinParentalRating"); statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value); if (query.MaxParentalRating.HasValue) { - ratingClause += " AND InheritedParentalRatingValue <= @MaxParentalRating"; + ratingClauseBuilder.Append(" AND InheritedParentalRatingValue <= @MaxParentalRating"); statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); } - ratingClause += ")"; + ratingClauseBuilder.Append(")"); } else if (query.MaxParentalRating.HasValue) { - ratingClause += "InheritedParentalRatingValue is null OR InheritedParentalRatingValue <= @MaxParentalRating"; + ratingClauseBuilder.Append("InheritedParentalRatingValue is null OR InheritedParentalRatingValue <= @MaxParentalRating"); statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); } else if (!query.HasParentalRating ?? false) { - ratingClause += "InheritedParentalRatingValue is null"; + ratingClauseBuilder.Append("InheritedParentalRatingValue is null"); } - if (!string.Equals(ratingClause, "(", StringComparison.OrdinalIgnoreCase)) + var ratingClauseString = ratingClauseBuilder.ToString(); + if (!string.Equals(ratingClauseString, "(", StringComparison.OrdinalIgnoreCase)) { - whereClauses.Add(ratingClause + ")"); + whereClauses.Add(ratingClauseString + ")"); } if (query.HasOfficialRating.HasValue) From a5f16136eb171b17b1e1ed661e9aeb017522ce89 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Mon, 20 Feb 2023 16:58:22 +0100 Subject: [PATCH 094/858] Apply review suggestions --- MediaBrowser.Common/Net/INetworkManager.cs | 1 - MediaBrowser.Model/Net/IPData.cs | 121 ++++++++++----------- MediaBrowser.Model/Net/ISocketFactory.cs | 53 +++++---- RSSDP/SsdpDeviceLocator.cs | 28 +---- RSSDP/SsdpDevicePublisher.cs | 27 +---- 5 files changed, 92 insertions(+), 138 deletions(-) diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs index efd87a8107..1a3176b581 100644 --- a/MediaBrowser.Common/Net/INetworkManager.cs +++ b/MediaBrowser.Common/Net/INetworkManager.cs @@ -76,7 +76,6 @@ namespace MediaBrowser.Common.Net /// <summary> /// Retrieves the bind address to use in system URLs. (Server Discovery, PlayTo, LiveTV, SystemInfo) /// If no bind addresses are specified, an internal interface address is selected. - /// (See <see cref="GetBindAddress(IPAddress, out int?, bool)"/>. /// </summary> /// <param name="source">IP address of the request.</param> /// <param name="port">Optional port returned, if it's part of an override.</param> diff --git a/MediaBrowser.Model/Net/IPData.cs b/MediaBrowser.Model/Net/IPData.cs index 16d74dcddd..985b16c6e4 100644 --- a/MediaBrowser.Model/Net/IPData.cs +++ b/MediaBrowser.Model/Net/IPData.cs @@ -2,73 +2,72 @@ using System.Net; using System.Net.Sockets; using Microsoft.AspNetCore.HttpOverrides; -namespace MediaBrowser.Model.Net +namespace MediaBrowser.Model.Net; + +/// <summary> +/// Base network object class. +/// </summary> +public class IPData { /// <summary> - /// Base network object class. + /// Initializes a new instance of the <see cref="IPData"/> class. /// </summary> - public class IPData + /// <param name="address">The <see cref="IPAddress"/>.</param> + /// <param name="subnet">The <see cref="IPNetwork"/>.</param> + /// <param name="name">The interface name.</param> + public IPData(IPAddress address, IPNetwork? subnet, string name) { - /// <summary> - /// Initializes a new instance of the <see cref="IPData"/> class. - /// </summary> - /// <param name="address">The <see cref="IPAddress"/>.</param> - /// <param name="subnet">The <see cref="IPNetwork"/>.</param> - /// <param name="name">The interface name.</param> - public IPData(IPAddress address, IPNetwork? subnet, string name) + Address = address; + Subnet = subnet ?? (address.AddressFamily == AddressFamily.InterNetwork ? new IPNetwork(address, 32) : new IPNetwork(address, 128)); + Name = name; + } + + /// <summary> + /// Initializes a new instance of the <see cref="IPData"/> class. + /// </summary> + /// <param name="address">The <see cref="IPAddress"/>.</param> + /// <param name="subnet">The <see cref="IPNetwork"/>.</param> + public IPData(IPAddress address, IPNetwork? subnet) + : this(address, subnet, string.Empty) + { + } + + /// <summary> + /// Gets or sets the object's IP address. + /// </summary> + public IPAddress Address { get; set; } + + /// <summary> + /// Gets or sets the object's IP address. + /// </summary> + public IPNetwork Subnet { get; set; } + + /// <summary> + /// Gets or sets the interface index. + /// </summary> + public int Index { get; set; } + + /// <summary> + /// Gets or sets the interface name. + /// </summary> + public string Name { get; set; } + + /// <summary> + /// Gets the AddressFamily of the object. + /// </summary> + public AddressFamily AddressFamily + { + get { - Address = address; - Subnet = subnet ?? (address.AddressFamily == AddressFamily.InterNetwork ? new IPNetwork(address, 32) : new IPNetwork(address, 128)); - Name = name; - } - - /// <summary> - /// Initializes a new instance of the <see cref="IPData"/> class. - /// </summary> - /// <param name="address">The <see cref="IPAddress"/>.</param> - /// <param name="subnet">The <see cref="IPNetwork"/>.</param> - public IPData(IPAddress address, IPNetwork? subnet) - : this(address, subnet, string.Empty) - { - } - - /// <summary> - /// Gets or sets the object's IP address. - /// </summary> - public IPAddress Address { get; set; } - - /// <summary> - /// Gets or sets the object's IP address. - /// </summary> - public IPNetwork Subnet { get; set; } - - /// <summary> - /// Gets or sets the interface index. - /// </summary> - public int Index { get; set; } - - /// <summary> - /// Gets or sets the interface name. - /// </summary> - public string Name { get; set; } - - /// <summary> - /// Gets the AddressFamily of the object. - /// </summary> - public AddressFamily AddressFamily - { - get + if (Address.Equals(IPAddress.None)) { - if (Address.Equals(IPAddress.None)) - { - return Subnet.Prefix.AddressFamily.Equals(IPAddress.None) - ? AddressFamily.Unspecified - : Subnet.Prefix.AddressFamily; - } - else - { - return Address.AddressFamily; - } + return Subnet.Prefix.AddressFamily.Equals(IPAddress.None) + ? AddressFamily.Unspecified + : Subnet.Prefix.AddressFamily; + } + else + { + return Address.AddressFamily; } } } diff --git a/MediaBrowser.Model/Net/ISocketFactory.cs b/MediaBrowser.Model/Net/ISocketFactory.cs index 49a88c2277..128034eb8f 100644 --- a/MediaBrowser.Model/Net/ISocketFactory.cs +++ b/MediaBrowser.Model/Net/ISocketFactory.cs @@ -1,36 +1,35 @@ using System.Net; using System.Net.Sockets; -namespace MediaBrowser.Model.Net +namespace MediaBrowser.Model.Net; + +/// <summary> +/// Implemented by components that can create specific socket configurations. +/// </summary> +public interface ISocketFactory { /// <summary> - /// Implemented by components that can create specific socket configurations. + /// Creates a new unicast socket using the specified local port number. /// </summary> - public interface ISocketFactory - { - /// <summary> - /// Creates a new unicast socket using the specified local port number. - /// </summary> - /// <param name="localPort">The local port to bind to.</param> - /// <returns>A new unicast socket using the specified local port number.</returns> - Socket CreateUdpBroadcastSocket(int localPort); + /// <param name="localPort">The local port to bind to.</param> + /// <returns>A new unicast socket using the specified local port number.</returns> + Socket CreateUdpBroadcastSocket(int localPort); - /// <summary> - /// Creates a new unicast socket using the specified local port number. - /// </summary> - /// <param name="bindInterface">The bind interface.</param> - /// <param name="localPort">The local port to bind to.</param> - /// <returns>A new unicast socket using the specified local port number.</returns> - Socket CreateSsdpUdpSocket(IPData bindInterface, int localPort); + /// <summary> + /// Creates a new unicast socket using the specified local port number. + /// </summary> + /// <param name="bindInterface">The bind interface.</param> + /// <param name="localPort">The local port to bind to.</param> + /// <returns>A new unicast socket using the specified local port number.</returns> + Socket CreateSsdpUdpSocket(IPData bindInterface, int localPort); - /// <summary> - /// Creates a new multicast socket using the specified multicast IP address, multicast time to live and local port. - /// </summary> - /// <param name="multicastAddress">The multicast IP address to bind to.</param> - /// <param name="bindInterface">The bind interface.</param> - /// <param name="multicastTimeToLive">The multicast time to live value. Actually a maximum number of network hops for UDP packets.</param> - /// <param name="localPort">The local port to bind to.</param> - /// <returns>A new multicast socket using the specfied bind interface, multicast address, multicast time to live and port.</returns> - Socket CreateUdpMulticastSocket(IPAddress multicastAddress, IPData bindInterface, int multicastTimeToLive, int localPort); - } + /// <summary> + /// Creates a new multicast socket using the specified multicast IP address, multicast time to live and local port. + /// </summary> + /// <param name="multicastAddress">The multicast IP address to bind to.</param> + /// <param name="bindInterface">The bind interface.</param> + /// <param name="multicastTimeToLive">The multicast time to live value. Actually a maximum number of network hops for UDP packets.</param> + /// <param name="localPort">The local port to bind to.</param> + /// <returns>A new multicast socket using the specfied bind interface, multicast address, multicast time to live and port.</returns> + Socket CreateUdpMulticastSocket(IPAddress multicastAddress, IPData bindInterface, int multicastTimeToLive, int localPort); } diff --git a/RSSDP/SsdpDeviceLocator.cs b/RSSDP/SsdpDeviceLocator.cs index 9d756d0d4c..ffe97754c7 100644 --- a/RSSDP/SsdpDeviceLocator.cs +++ b/RSSDP/SsdpDeviceLocator.cs @@ -34,30 +34,9 @@ namespace Rssdp.Infrastructure string osName, string osVersion) { - if (communicationsServer is null) - { - throw new ArgumentNullException(nameof(communicationsServer)); - } - - if (osName is null) - { - throw new ArgumentNullException(nameof(osName)); - } - - if (osName.Length == 0) - { - throw new ArgumentException("osName cannot be an empty string.", nameof(osName)); - } - - if (osVersion is null) - { - throw new ArgumentNullException(nameof(osVersion)); - } - - if (osVersion.Length == 0) - { - throw new ArgumentException("osVersion cannot be an empty string.", nameof(osName)); - } + ArgumentNullException.ThrowIfNull(communicationsServer); + ArgumentNullException.ThrowIfNullOrEmpty(osName); + ArgumentNullException.ThrowIfNullOrEmpty(osVersion); _OSName = osName; _OSVersion = osVersion; @@ -363,7 +342,6 @@ namespace Rssdp.Infrastructure var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); values["HOST"] = "239.255.255.250:1900"; - values["USER-AGENT"] = "UPnP/1.0 DLNADOC/1.50 Platinum/1.0.4.2"; values["USER-AGENT"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion); values["MAN"] = "\"ssdp:discover\""; diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs index 8b55518999..e443c62855 100644 --- a/RSSDP/SsdpDevicePublisher.cs +++ b/RSSDP/SsdpDevicePublisher.cs @@ -40,30 +40,9 @@ namespace Rssdp.Infrastructure string osVersion, bool sendOnlyMatchedHost) { - if (communicationsServer is null) - { - throw new ArgumentNullException(nameof(communicationsServer)); - } - - if (osName is null) - { - throw new ArgumentNullException(nameof(osName)); - } - - if (osName.Length == 0) - { - throw new ArgumentException("osName cannot be an empty string.", nameof(osName)); - } - - if (osVersion is null) - { - throw new ArgumentNullException(nameof(osVersion)); - } - - if (osVersion.Length == 0) - { - throw new ArgumentException("osVersion cannot be an empty string.", nameof(osName)); - } + ArgumentNullException.ThrowIfNull(communicationsServer); + ArgumentNullException.ThrowIfNullOrEmpty(osName); + ArgumentNullException.ThrowIfNullOrEmpty(osVersion); _SupportPnpRootDevice = true; _Devices = new List<SsdpRootDevice>(); From a5bfeb28aa2c92e6e58f5f00e5651807794ac8ef Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Mon, 20 Feb 2023 21:51:15 +0100 Subject: [PATCH 095/858] Apply review suggestions --- Jellyfin.Networking/Manager/NetworkManager.cs | 141 ++++++++++-------- MediaBrowser.Common/Net/INetworkManager.cs | 6 +- MediaBrowser.Common/Net/NetworkExtensions.cs | 105 ++++++------- RSSDP/SsdpCommunicationsServer.cs | 2 +- .../NetworkParseTests.cs | 4 +- 5 files changed, 139 insertions(+), 119 deletions(-) diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index dd90a5b516..f0f95f5fc8 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -26,11 +26,6 @@ namespace Jellyfin.Networking.Manager /// </summary> private readonly object _initLock; - /// <summary> - /// List of all interface MAC addresses. - /// </summary> - private readonly List<PhysicalAddress> _macAddresses; - private readonly ILogger<NetworkManager> _logger; private readonly IConfigurationManager _configurationManager; @@ -40,30 +35,35 @@ namespace Jellyfin.Networking.Manager /// <summary> /// Holds the published server URLs and the IPs to use them on. /// </summary> - private readonly Dictionary<IPData, string> _publishedServerUrls; + private IReadOnlyDictionary<IPData, string> _publishedServerUrls; - private List<IPNetwork> _remoteAddressFilter; + private IReadOnlyList<IPNetwork> _remoteAddressFilter; /// <summary> /// Used to stop "event-racing conditions". /// </summary> private bool _eventfire; + /// <summary> + /// List of all interface MAC addresses. + /// </summary> + private IReadOnlyList<PhysicalAddress> _macAddresses; + /// <summary> /// Dictionary containing interface addresses and their subnets. /// </summary> - private List<IPData> _interfaces; + private IReadOnlyList<IPData> _interfaces; /// <summary> /// Unfiltered user defined LAN subnets (<see cref="NetworkConfiguration.LocalNetworkSubnets"/>) /// or internal interface network subnets if undefined by user. /// </summary> - private List<IPNetwork> _lanSubnets; + private IReadOnlyList<IPNetwork> _lanSubnets; /// <summary> /// User defined list of subnets to excluded from the LAN. /// </summary> - private List<IPNetwork> _excludedSubnets; + private IReadOnlyList<IPNetwork> _excludedSubnets; /// <summary> /// True if this object is disposed. @@ -127,7 +127,7 @@ namespace Jellyfin.Networking.Manager /// <summary> /// Gets the Published server override list. /// </summary> - public Dictionary<IPData, string> PublishedServerUrls => _publishedServerUrls; + public IReadOnlyDictionary<IPData, string> PublishedServerUrls => _publishedServerUrls; /// <inheritdoc/> public void Dispose() @@ -206,8 +206,8 @@ namespace Jellyfin.Networking.Manager { _logger.LogDebug("Refreshing interfaces."); - _interfaces.Clear(); - _macAddresses.Clear(); + var interfaces = new List<IPData>(); + var macAddresses = new List<PhysicalAddress>(); try { @@ -224,7 +224,7 @@ namespace Jellyfin.Networking.Manager // Populate MAC list if (adapter.NetworkInterfaceType != NetworkInterfaceType.Loopback && PhysicalAddress.None.Equals(mac)) { - _macAddresses.Add(mac); + macAddresses.Add(mac); } // Populate interface list @@ -236,7 +236,7 @@ namespace Jellyfin.Networking.Manager interfaceObject.Index = ipProperties.GetIPv4Properties().Index; interfaceObject.Name = adapter.Name; - _interfaces.Add(interfaceObject); + interfaces.Add(interfaceObject); } else if (IsIPv6Enabled && info.Address.AddressFamily == AddressFamily.InterNetworkV6) { @@ -244,7 +244,7 @@ namespace Jellyfin.Networking.Manager interfaceObject.Index = ipProperties.GetIPv6Properties().Index; interfaceObject.Name = adapter.Name; - _interfaces.Add(interfaceObject); + interfaces.Add(interfaceObject); } } } @@ -265,23 +265,26 @@ namespace Jellyfin.Networking.Manager } // If no interfaces are found, fallback to loopback interfaces. - if (_interfaces.Count == 0) + if (interfaces.Count == 0) { _logger.LogWarning("No interface information available. Using loopback interface(s)."); if (IsIPv4Enabled && !IsIPv6Enabled) { - _interfaces.Add(new IPData(IPAddress.Loopback, new IPNetwork(IPAddress.Loopback, 8), "lo")); + interfaces.Add(new IPData(IPAddress.Loopback, new IPNetwork(IPAddress.Loopback, 8), "lo")); } if (!IsIPv4Enabled && IsIPv6Enabled) { - _interfaces.Add(new IPData(IPAddress.IPv6Loopback, new IPNetwork(IPAddress.IPv6Loopback, 128), "lo")); + interfaces.Add(new IPData(IPAddress.IPv6Loopback, new IPNetwork(IPAddress.IPv6Loopback, 128), "lo")); } } - _logger.LogDebug("Discovered {NumberOfInterfaces} interfaces.", _interfaces.Count); - _logger.LogDebug("Interfaces addresses: {Addresses}", _interfaces.OrderByDescending(s => s.AddressFamily == AddressFamily.InterNetwork).Select(s => s.Address.ToString())); + _logger.LogDebug("Discovered {NumberOfInterfaces} interfaces.", interfaces.Count); + _logger.LogDebug("Interfaces addresses: {Addresses}", interfaces.OrderByDescending(s => s.AddressFamily == AddressFamily.InterNetwork).Select(s => s.Address.ToString())); + + _macAddresses = macAddresses; + _interfaces = interfaces; } } @@ -297,36 +300,37 @@ namespace Jellyfin.Networking.Manager // Get configuration options var subnets = config.LocalNetworkSubnets; - if (!NetworkExtensions.TryParseToSubnets(subnets, out _lanSubnets, false)) - { - _lanSubnets.Clear(); - } - - if (!NetworkExtensions.TryParseToSubnets(subnets, out _excludedSubnets, true)) - { - _excludedSubnets.Clear(); - } - // If no LAN addresses are specified, all private subnets and Loopback are deemed to be the LAN - if (_lanSubnets.Count == 0) + if (!NetworkExtensions.TryParseToSubnets(subnets, out var lanSubnets, false) || lanSubnets.Count == 0) { _logger.LogDebug("Using LAN interface addresses as user provided no LAN details."); + var fallbackLanSubnets = new List<IPNetwork>(); if (IsIPv6Enabled) { - _lanSubnets.Add(new IPNetwork(IPAddress.IPv6Loopback, 128)); // RFC 4291 (Loopback) - _lanSubnets.Add(new IPNetwork(IPAddress.Parse("fe80::"), 10)); // RFC 4291 (Site local) - _lanSubnets.Add(new IPNetwork(IPAddress.Parse("fc00::"), 7)); // RFC 4193 (Unique local) + fallbackLanSubnets.Add(new IPNetwork(IPAddress.IPv6Loopback, 128)); // RFC 4291 (Loopback) + fallbackLanSubnets.Add(new IPNetwork(IPAddress.Parse("fe80::"), 10)); // RFC 4291 (Site local) + fallbackLanSubnets.Add(new IPNetwork(IPAddress.Parse("fc00::"), 7)); // RFC 4193 (Unique local) } if (IsIPv4Enabled) { - _lanSubnets.Add(new IPNetwork(IPAddress.Loopback, 8)); // RFC 5735 (Loopback) - _lanSubnets.Add(new IPNetwork(IPAddress.Parse("10.0.0.0"), 8)); // RFC 1918 (private) - _lanSubnets.Add(new IPNetwork(IPAddress.Parse("172.16.0.0"), 12)); // RFC 1918 (private) - _lanSubnets.Add(new IPNetwork(IPAddress.Parse("192.168.0.0"), 16)); // RFC 1918 (private) + fallbackLanSubnets.Add(new IPNetwork(IPAddress.Loopback, 8)); // RFC 5735 (Loopback) + fallbackLanSubnets.Add(new IPNetwork(IPAddress.Parse("10.0.0.0"), 8)); // RFC 1918 (private) + fallbackLanSubnets.Add(new IPNetwork(IPAddress.Parse("172.16.0.0"), 12)); // RFC 1918 (private) + fallbackLanSubnets.Add(new IPNetwork(IPAddress.Parse("192.168.0.0"), 16)); // RFC 1918 (private) } + + _lanSubnets = fallbackLanSubnets; } + else + { + _lanSubnets = lanSubnets; + } + + _excludedSubnets = NetworkExtensions.TryParseToSubnets(subnets, out var excludedSubnets, true) + ? excludedSubnets + : new List<IPNetwork>(); _logger.LogInformation("Defined LAN addresses: {0}", _lanSubnets.Select(s => s.Prefix + "/" + s.PrefixLength)); _logger.LogInformation("Defined LAN exclusions: {0}", _excludedSubnets.Select(s => s.Prefix + "/" + s.PrefixLength)); @@ -342,26 +346,27 @@ namespace Jellyfin.Networking.Manager lock (_initLock) { // Respect explicit bind addresses + var interfaces = _interfaces.ToList(); var localNetworkAddresses = config.LocalNetworkAddresses; if (localNetworkAddresses.Length > 0 && !string.IsNullOrWhiteSpace(localNetworkAddresses[0])) { var bindAddresses = localNetworkAddresses.Select(p => NetworkExtensions.TryParseToSubnet(p, out var network) ? network.Prefix - : (_interfaces.Where(x => x.Name.Equals(p, StringComparison.OrdinalIgnoreCase)) + : (interfaces.Where(x => x.Name.Equals(p, StringComparison.OrdinalIgnoreCase)) .Select(x => x.Address) .FirstOrDefault() ?? IPAddress.None)) .Where(x => x != IPAddress.None) .ToHashSet(); - _interfaces = _interfaces.Where(x => bindAddresses.Contains(x.Address)).ToList(); + interfaces = interfaces.Where(x => bindAddresses.Contains(x.Address)).ToList(); if (bindAddresses.Contains(IPAddress.Loopback)) { - _interfaces.Add(new IPData(IPAddress.Loopback, new IPNetwork(IPAddress.Loopback, 8), "lo")); + interfaces.Add(new IPData(IPAddress.Loopback, new IPNetwork(IPAddress.Loopback, 8), "lo")); } if (bindAddresses.Contains(IPAddress.IPv6Loopback)) { - _interfaces.Add(new IPData(IPAddress.IPv6Loopback, new IPNetwork(IPAddress.IPv6Loopback, 128), "lo")); + interfaces.Add(new IPData(IPAddress.IPv6Loopback, new IPNetwork(IPAddress.IPv6Loopback, 128), "lo")); } } @@ -377,7 +382,7 @@ namespace Jellyfin.Networking.Manager { foreach (var virtualInterfacePrefix in virtualInterfacePrefixes) { - _interfaces.RemoveAll(x => x.Name.StartsWith(virtualInterfacePrefix, StringComparison.OrdinalIgnoreCase)); + interfaces.RemoveAll(x => x.Name.StartsWith(virtualInterfacePrefix, StringComparison.OrdinalIgnoreCase)); } } } @@ -385,16 +390,17 @@ namespace Jellyfin.Networking.Manager // Remove all IPv4 interfaces if IPv4 is disabled if (!IsIPv4Enabled) { - _interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetwork); + interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetwork); } // Remove all IPv6 interfaces if IPv6 is disabled if (!IsIPv6Enabled) { - _interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetworkV6); + interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetworkV6); } - _logger.LogInformation("Using bind addresses: {0}", _interfaces.OrderByDescending(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => x.Address)); + _logger.LogInformation("Using bind addresses: {0}", interfaces.OrderByDescending(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => x.Address)); + _interfaces = interfaces; } } @@ -410,10 +416,11 @@ namespace Jellyfin.Networking.Manager if (remoteIPFilter.Any() && !string.IsNullOrWhiteSpace(remoteIPFilter.First())) { // Parse all IPs with netmask to a subnet + var remoteAddressFilter = new List<IPNetwork>(); var remoteFilteredSubnets = remoteIPFilter.Where(x => x.Contains('/', StringComparison.OrdinalIgnoreCase)).ToArray(); - if (!NetworkExtensions.TryParseToSubnets(remoteFilteredSubnets, out _remoteAddressFilter, false)) + if (NetworkExtensions.TryParseToSubnets(remoteFilteredSubnets, out var remoteAddressFilterResult, false)) { - _remoteAddressFilter.Clear(); + remoteAddressFilter = remoteAddressFilterResult.ToList(); } // Parse everything else as an IP and construct subnet with a single IP @@ -422,9 +429,11 @@ namespace Jellyfin.Networking.Manager { if (IPAddress.TryParse(ip, out var ipp)) { - _remoteAddressFilter.Add(new IPNetwork(ipp, ipp.AddressFamily == AddressFamily.InterNetwork ? 32 : 128)); + remoteAddressFilter.Add(new IPNetwork(ipp, ipp.AddressFamily == AddressFamily.InterNetwork ? 32 : 128)); } } + + _remoteAddressFilter = remoteAddressFilter; } } } @@ -440,8 +449,8 @@ namespace Jellyfin.Networking.Manager { lock (_initLock) { - _publishedServerUrls.Clear(); - string[] overrides = config.PublishedServerUriBySubnet; + var publishedServerUrls = new Dictionary<IPData, string>(); + var overrides = config.PublishedServerUriBySubnet; foreach (var entry in overrides) { @@ -456,31 +465,31 @@ namespace Jellyfin.Networking.Manager var identifier = parts[0]; if (string.Equals(identifier, "all", StringComparison.OrdinalIgnoreCase)) { - _publishedServerUrls[new IPData(IPAddress.Broadcast, null)] = replacement; + publishedServerUrls[new IPData(IPAddress.Broadcast, null)] = replacement; } else if (string.Equals(identifier, "external", StringComparison.OrdinalIgnoreCase)) { - _publishedServerUrls[new IPData(IPAddress.Any, new IPNetwork(IPAddress.Any, 0))] = replacement; - _publishedServerUrls[new IPData(IPAddress.IPv6Any, new IPNetwork(IPAddress.IPv6Any, 0))] = replacement; + publishedServerUrls[new IPData(IPAddress.Any, new IPNetwork(IPAddress.Any, 0))] = replacement; + publishedServerUrls[new IPData(IPAddress.IPv6Any, new IPNetwork(IPAddress.IPv6Any, 0))] = replacement; } else if (string.Equals(identifier, "internal", StringComparison.OrdinalIgnoreCase)) { foreach (var lan in _lanSubnets) { var lanPrefix = lan.Prefix; - _publishedServerUrls[new IPData(lanPrefix, new IPNetwork(lanPrefix, lan.PrefixLength))] = replacement; + publishedServerUrls[new IPData(lanPrefix, new IPNetwork(lanPrefix, lan.PrefixLength))] = replacement; } } else if (NetworkExtensions.TryParseToSubnet(identifier, out var result) && result is not null) { var data = new IPData(result.Prefix, result); - _publishedServerUrls[data] = replacement; + publishedServerUrls[data] = replacement; } else if (TryParseInterface(identifier, out var ifaces)) { foreach (var iface in ifaces) { - _publishedServerUrls[iface] = replacement; + publishedServerUrls[iface] = replacement; } } else @@ -488,6 +497,8 @@ namespace Jellyfin.Networking.Manager _logger.LogError("Unable to parse bind override: {Entry}", entry); } } + + _publishedServerUrls = publishedServerUrls; } } @@ -520,6 +531,7 @@ namespace Jellyfin.Networking.Manager { // Format is <IPAddress>,<Index>,<Name>: <next interface>. Set index to -ve to simulate a gateway. var interfaceList = MockNetworkSettings.Split('|'); + var interfaces = new List<IPData>(); foreach (var details in interfaceList) { var parts = details.Split(','); @@ -531,7 +543,7 @@ namespace Jellyfin.Networking.Manager { var data = new IPData(address, subnet, parts[2]); data.Index = index; - _interfaces.Add(data); + interfaces.Add(data); } } else @@ -539,6 +551,8 @@ namespace Jellyfin.Networking.Manager _logger.LogWarning("Could not parse mock interface settings: {Part}", details); } } + + _interfaces = interfaces; } EnforceBindSettings(config); @@ -565,11 +579,12 @@ namespace Jellyfin.Networking.Manager } /// <inheritdoc/> - public bool TryParseInterface(string intf, out List<IPData> result) + public bool TryParseInterface(string intf, out IReadOnlyList<IPData> result) { - result = new List<IPData>(); + var resultList = new List<IPData>(); if (string.IsNullOrEmpty(intf) || _interfaces is null) { + result = resultList.AsReadOnly(); return false; } @@ -585,13 +600,15 @@ namespace Jellyfin.Networking.Manager if ((IsIPv4Enabled && iface.Address.AddressFamily == AddressFamily.InterNetwork) || (IsIPv6Enabled && iface.Address.AddressFamily == AddressFamily.InterNetworkV6)) { - result.Add(iface); + resultList.Add(iface); } } + result = resultList.AsReadOnly(); return true; } + result = resultList.AsReadOnly(); return false; } diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs index 1a3176b581..a92b751f2a 100644 --- a/MediaBrowser.Common/Net/INetworkManager.cs +++ b/MediaBrowser.Common/Net/INetworkManager.cs @@ -30,7 +30,7 @@ namespace MediaBrowser.Common.Net /// <summary> /// Calculates the list of interfaces to use for Kestrel. /// </summary> - /// <returns>A List{IPData} object containing all the interfaces to bind. + /// <returns>A IReadOnlyList{IPData} 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> @@ -39,7 +39,7 @@ namespace MediaBrowser.Common.Net /// <summary> /// Returns a list containing the loopback interfaces. /// </summary> - /// <returns>List{IPData}.</returns> + /// <returns>IReadOnlyList{IPData}.</returns> IReadOnlyList<IPData> GetLoopbacks(); /// <summary> @@ -120,7 +120,7 @@ namespace MediaBrowser.Common.Net /// <param name="intf">Interface name.</param> /// <param name="result">Resulting object's IP addresses, if successful.</param> /// <returns>Success of the operation.</returns> - bool TryParseInterface(string intf, out List<IPData> result); + bool TryParseInterface(string intf, out IReadOnlyList<IPData> result); /// <summary> /// Returns all internal (LAN) bind interface addresses. diff --git a/MediaBrowser.Common/Net/NetworkExtensions.cs b/MediaBrowser.Common/Net/NetworkExtensions.cs index cef4a5d965..227f0483f4 100644 --- a/MediaBrowser.Common/Net/NetworkExtensions.cs +++ b/MediaBrowser.Common/Net/NetworkExtensions.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Net; using System.Net.Sockets; using System.Text.RegularExpressions; +using Jellyfin.Extensions; using Microsoft.AspNetCore.HttpOverrides; namespace MediaBrowser.Common.Net @@ -193,71 +194,64 @@ namespace MediaBrowser.Common.Net /// <param name="result">An <see cref="IPNetwork"/>.</param> /// <param name="negated">Boolean signaling if negated or not negated values should be parsed.</param> /// <returns><c>True</c> if parsing was successful.</returns> - public static bool TryParseToSubnet(string value, out IPNetwork result, bool negated = false) + public static bool TryParseToSubnet(ReadOnlySpan<char> value, out IPNetwork result, bool negated = false) { result = new IPNetwork(IPAddress.None, 32); - - if (string.IsNullOrEmpty(value)) + var splitString = value.Trim().Split('/'); + if (splitString.MoveNext()) { - return false; - } - - var splitString = value.Trim().Split("/"); - var ipBlock = splitString[0]; - - var address = IPAddress.None; - if (negated && ipBlock.StartsWith('!') && IPAddress.TryParse(ipBlock[1..], out var tmpAddress)) - { - address = tmpAddress; - } - else if (!negated && IPAddress.TryParse(ipBlock, out tmpAddress)) - { - address = tmpAddress; - } - - if (address != IPAddress.None && address is not null) - { - if (splitString.Length > 1) + var ipBlock = splitString.Current; + var address = IPAddress.None; + if (negated && ipBlock.StartsWith("!") && IPAddress.TryParse(ipBlock[1..], out var tmpAddress)) { - var subnetBlock = splitString[1]; - if (int.TryParse(subnetBlock, out var netmask)) + address = tmpAddress; + } + else if (!negated && IPAddress.TryParse(ipBlock, out tmpAddress)) + { + address = tmpAddress; + } + + if (address != IPAddress.None) + { + if (splitString.MoveNext()) { - result = new IPNetwork(address, netmask); + var subnetBlock = splitString.Current; + if (int.TryParse(subnetBlock, out var netmask)) + { + result = new IPNetwork(address, netmask); + } + else if (IPAddress.TryParse(subnetBlock, out var netmaskAddress)) + { + result = new IPNetwork(address, NetworkExtensions.MaskToCidr(netmaskAddress)); + } } - else if (IPAddress.TryParse(subnetBlock, out var netmaskAddress)) + else if (address.AddressFamily == AddressFamily.InterNetwork) { - result = new IPNetwork(address, NetworkExtensions.MaskToCidr(netmaskAddress)); + result = new IPNetwork(address, 32); + } + else if (address.AddressFamily == AddressFamily.InterNetworkV6) + { + result = new IPNetwork(address, 128); } - } - else if (address.AddressFamily == AddressFamily.InterNetwork) - { - result = new IPNetwork(address, 32); - } - else if (address.AddressFamily == AddressFamily.InterNetworkV6) - { - result = new IPNetwork(address, 128); - } - } - if (!result.Prefix.Equals(IPAddress.None)) - { - return true; + return true; + } } return false; } /// <summary> - /// Attempts to parse a host string. + /// Attempts to parse a host span. /// </summary> /// <param name="host">Host name to parse.</param> - /// <param name="addresses">Object representing the string, if it has successfully been parsed.</param> + /// <param name="addresses">Object representing the span, if it has successfully been parsed.</param> /// <param name="isIPv4Enabled"><c>true</c> if IPv4 is enabled.</param> /// <param name="isIPv6Enabled"><c>true</c> if IPv6 is enabled.</param> /// <returns><c>true</c> if the parsing is successful, <c>false</c> if not.</returns> - public static bool TryParseHost(string host, [NotNullWhen(true)] out IPAddress[] addresses, bool isIPv4Enabled = true, bool isIPv6Enabled = false) + public static bool TryParseHost(ReadOnlySpan<char> host, [NotNullWhen(true)] out IPAddress[] addresses, bool isIPv4Enabled = true, bool isIPv6Enabled = false) { - if (string.IsNullOrWhiteSpace(host)) + if (host.IsEmpty) { addresses = Array.Empty<IPAddress>(); return false; @@ -268,19 +262,24 @@ namespace MediaBrowser.Common.Net // See if it's an IPv6 with port address e.g. [::1] or [::1]:120. if (host[0] == '[') { - int i = host.IndexOf(']', StringComparison.Ordinal); + int i = host.IndexOf("]", StringComparison.Ordinal); if (i != -1) { - return TryParseHost(host.Remove(i)[1..], out addresses); + return TryParseHost(host[1..(i - 1)], out addresses); } addresses = Array.Empty<IPAddress>(); return false; } - var hosts = host.Split(':'); + var hosts = new List<string>(); + var splitSpan = host.Split(':'); + while (splitSpan.MoveNext()) + { + hosts.Add(splitSpan.Current.ToString()); + } - if (hosts.Length <= 2) + if (hosts.Count <= 2) { // Is hostname or hostname:port if (_fqdnRegex.IsMatch(hosts[0])) @@ -315,10 +314,14 @@ namespace MediaBrowser.Common.Net return true; } } - else if (hosts.Length <= 9 && IPAddress.TryParse(host.Split('/')[0], out var address)) // 8 octets + port + else if (hosts.Count <= 9) // 8 octets + port { - addresses = new[] { address }; - return true; + splitSpan = host.Split('/'); + if (splitSpan.MoveNext() && IPAddress.TryParse(splitSpan.Current, out var address)) + { + addresses = new[] { address }; + return true; + } } addresses = Array.Empty<IPAddress>(); diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs index 5b8916d021..0dce6c3bfa 100644 --- a/RSSDP/SsdpCommunicationsServer.cs +++ b/RSSDP/SsdpCommunicationsServer.cs @@ -428,7 +428,7 @@ namespace Rssdp.Infrastructure if (result.ReceivedBytes > 0) { var remoteEndpoint = (IPEndPoint)result.RemoteEndPoint; - var localEndpointAdapter = _networkManager.GetAllBindInterfaces().Where(a => a.Index == result.PacketInformation.Interface).First(); + var localEndpointAdapter = _networkManager.GetAllBindInterfaces().First(a => a.Index == result.PacketInformation.Interface); ProcessMessage( UTF8Encoding.UTF8.GetString(receiveBuffer, 0, result.ReceivedBytes), diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs index d51ce19d75..8b7df0470d 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs @@ -203,7 +203,7 @@ namespace Jellyfin.Networking.Tests using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); NetworkManager.MockNetworkSettings = string.Empty; - _ = nm.TryParseInterface(result, out List<IPData>? resultObj); + _ = nm.TryParseInterface(result, out IReadOnlyList<IPData>? resultObj); // Check to see if dns resolution is working. If not, skip test. _ = NetworkExtensions.TryParseHost(source, out var host); @@ -266,7 +266,7 @@ namespace Jellyfin.Networking.Tests using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); NetworkManager.MockNetworkSettings = string.Empty; - if (nm.TryParseInterface(result, out List<IPData>? resultObj) && resultObj is not null) + if (nm.TryParseInterface(result, out IReadOnlyList<IPData>? resultObj) && resultObj is not null) { // Parse out IPAddresses so we can do a string comparison (ignore subnet masks). result = resultObj.First().Address.ToString(); From 6300d01fcceba56932741251443f5b2aa4f76de2 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Mon, 20 Feb 2023 21:58:31 +0100 Subject: [PATCH 096/858] Apply review suggestion --- Emby.Server.Implementations/Data/SqliteItemRepository.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index ecc2a2c910..0aa943270e 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -3623,8 +3623,12 @@ namespace Emby.Server.Implementations.Data clauseBuilder.Append("(guid in (select itemid from People where Name = (select Name from TypedBaseItems where guid=") .Append(paramName) .Append("))) OR "); - query.PersonIds[i].TryWriteBytes(idBytes); - statement?.TryBind(paramName, idBytes); + + if (statement is not null) + { + query.PersonIds[i].TryWriteBytes(idBytes); + statement.TryBind(paramName, idBytes); + } } // Remove last " OR " From b6ce9206703543b1b55b1da88208758cd56e41a5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 22 Feb 2023 04:30:10 +0000 Subject: [PATCH 097/858] chore(deps): update dependency autofixture.automoq to v4.18.0 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 5060b3de56..6e997d7064 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,7 +6,7 @@ <!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.--> <ItemGroup Label="Package Dependencies"> - <PackageVersion Include="AutoFixture.AutoMoq" Version="4.17.0" /> + <PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.0" /> <PackageVersion Include="AutoFixture.Xunit2" Version="4.17.0" /> <PackageVersion Include="AutoFixture" Version="4.18.0" /> <PackageVersion Include="BDInfo" Version="0.7.6.2" /> From eb3d187f27c34112689d37c50a1f734835a33ef3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 22 Feb 2023 05:48:43 -0700 Subject: [PATCH 098/858] chore(deps): update dependency autofixture.xunit2 to v4.18.0 (#9372) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 6e997d7064..c2a4d5ff3d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,7 +7,7 @@ <ItemGroup Label="Package Dependencies"> <PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.0" /> - <PackageVersion Include="AutoFixture.Xunit2" Version="4.17.0" /> + <PackageVersion Include="AutoFixture.Xunit2" Version="4.18.0" /> <PackageVersion Include="AutoFixture" Version="4.18.0" /> <PackageVersion Include="BDInfo" Version="0.7.6.2" /> <PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.2.0" /> From 7af6694594cfc71644b336a2bba459c2f439369b Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Thu, 23 Feb 2023 13:55:27 +0100 Subject: [PATCH 099/858] Fix AutoDiscovery socket creation --- .../EntryPoints/UdpServerEntryPoint.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs index 8fb1f93228..2839e163e3 100644 --- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs +++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs @@ -82,7 +82,9 @@ namespace Emby.Server.Implementations.EntryPoints if (_enableMultiSocketBinding) { // Add global broadcast socket - _udpServers.Add(new UdpServer(_logger, _appHost, _config, IPAddress.Broadcast, PortNumber)); + var server = new UdpServer(_logger, _appHost, _config, IPAddress.Broadcast, PortNumber); + server.Start(_cancellationTokenSource.Token); + _udpServers.Add(server); // Add bind address specific broadcast sockets // IPv6 is currently unsupported @@ -90,9 +92,9 @@ namespace Emby.Server.Implementations.EntryPoints foreach (var intf in validInterfaces) { var broadcastAddress = NetworkExtensions.GetBroadcastAddress(intf.Subnet); - _logger.LogDebug("Binding UDP server to {Address} on port {PortNumber}", broadcastAddress.ToString(), PortNumber); + _logger.LogDebug("Binding UDP server to {Address} on port {PortNumber}", broadcastAddress, PortNumber); - var server = new UdpServer(_logger, _appHost, _config, broadcastAddress, PortNumber); + server = new UdpServer(_logger, _appHost, _config, broadcastAddress, PortNumber); server.Start(_cancellationTokenSource.Token); _udpServers.Add(server); } From ab24c0e2cf0e678146474db10ecb8e5f86764a10 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Thu, 23 Feb 2023 19:09:16 +0100 Subject: [PATCH 100/858] Enable nullable for more files --- Jellyfin.Api/Controllers/GenresController.cs | 9 +++------ MediaBrowser.Controller/Subtitles/ISubtitleManager.cs | 2 -- .../Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs | 2 -- .../Plugins/Tmdb/Movies/TmdbMovieProvider.cs | 2 -- .../Plugins/Tmdb/People/TmdbPersonProvider.cs | 2 -- .../Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs | 4 +--- .../Plugins/Tmdb/TV/TmdbEpisodeProvider.cs | 4 +--- .../Plugins/Tmdb/TV/TmdbSeriesProvider.cs | 2 -- MediaBrowser.Providers/Subtitles/SubtitleManager.cs | 8 +++----- MediaBrowser.Providers/TV/SeriesMetadataService.cs | 2 -- 10 files changed, 8 insertions(+), 29 deletions(-) diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs index eb03b514c7..da60f2c60b 100644 --- a/Jellyfin.Api/Controllers/GenresController.cs +++ b/Jellyfin.Api/Controllers/GenresController.cs @@ -172,12 +172,9 @@ public class GenresController : BaseJellyfinApiController item ??= new Genre(); - if (userId.Value.Equals(default)) - { - return _dtoService.GetBaseItemDto(item, dtoOptions); - } - - var user = _userManager.GetUserById(userId.Value); + var user = userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); return _dtoService.GetBaseItemDto(item, dtoOptions, user); } diff --git a/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs index 841b320376..b86e482435 100644 --- a/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs +++ b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs index 02601d3f56..655fa5a16d 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Collections.Generic; using System.Globalization; diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs index 9eced93fa5..753a15c6ee 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Collections.Generic; using System.Globalization; diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs index b3709baf58..44d5bab76d 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Collections.Generic; using System.Globalization; diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs index 5259faf76f..28da62111e 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Collections.Generic; using System.Globalization; @@ -63,7 +61,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV var seriesTmdbId = Convert.ToInt32(series?.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture); - if (seriesTmdbId <= 0) + if (series == null || seriesTmdbId <= 0) { return Enumerable.Empty<RemoteImageInfo>(); } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs index 35e304a2ac..66decde842 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Collections.Generic; using System.Globalization; @@ -87,7 +85,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV return metadataResult; } - info.SeriesProviderIds.TryGetValue(MetadataProvider.Tmdb.ToString(), out string tmdbId); + info.SeriesProviderIds.TryGetValue(MetadataProvider.Tmdb.ToString(), out string? tmdbId); var seriesTmdbId = Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture); if (seriesTmdbId <= 0) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs index 9590882105..046b63faf8 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Collections.Generic; using System.Globalization; diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs index d89fb814d8..0c01c50317 100644 --- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs +++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -56,7 +54,7 @@ namespace MediaBrowser.Providers.Subtitles } /// <inheritdoc /> - public event EventHandler<SubtitleDownloadFailureEventArgs> SubtitleDownloadFailure; + public event EventHandler<SubtitleDownloadFailureEventArgs>? SubtitleDownloadFailure; /// <inheritdoc /> public async Task<RemoteSubtitleInfo[]> SearchSubtitles(SubtitleSearchRequest request, CancellationToken cancellationToken) @@ -235,7 +233,7 @@ namespace MediaBrowser.Providers.Subtitles private async Task TrySaveToFiles(Stream stream, List<string> savePaths) { - List<Exception> exs = null; + List<Exception>? exs = null; foreach (var savePath in savePaths) { @@ -245,7 +243,7 @@ namespace MediaBrowser.Providers.Subtitles try { - Directory.CreateDirectory(Path.GetDirectoryName(savePath)); + Directory.CreateDirectory(Path.GetDirectoryName(savePath) ?? throw new InvalidOperationException("Path can't be a root directory.")); var fileOptions = AsyncFile.WriteOptions; fileOptions.Mode = FileMode.CreateNew; diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs index a261d7cdb5..97f9383971 100644 --- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs +++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System.Collections.Generic; From a328dba3b16adb1707a8b7fb504cc47eabed18d8 Mon Sep 17 00:00:00 2001 From: Pranav Avva <pranav.avva@gmail.com> Date: Wed, 22 Feb 2023 20:38:26 +0000 Subject: [PATCH 101/858] Translated using Weblate (Hindi) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hi/ --- Emby.Server.Implementations/Localization/Core/hi.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/hi.json b/Emby.Server.Implementations/Localization/Core/hi.json index 182b43ffca..a0e2f04a16 100644 --- a/Emby.Server.Implementations/Localization/Core/hi.json +++ b/Emby.Server.Implementations/Localization/Core/hi.json @@ -67,5 +67,11 @@ "Plugin": "प्लग-इन", "Playlists": "प्लेलिस्ट", "Photos": "तस्वीरें", - "External": "बाहरी" + "External": "बाहरी", + "PluginUpdatedWithName": "{0} अपडेट हुए", + "ScheduledTaskStartedWithName": "{0} शुरू हुए", + "Songs": "गाने", + "UserStartedPlayingItemWithValues": "{0} {2} पर {1} खेल रहे हैं", + "UserStoppedPlayingItemWithValues": "{0} ने {2} पर {1} खेलना खत्म किया", + "StartupEmbyServerIsLoading": "जेलीफ़िन सर्वर लोड हो रहा है। कृपया शीघ्र ही पुन: प्रयास करें।" } From f94abc1eb7352404e0250060ed0c57ad369d2457 Mon Sep 17 00:00:00 2001 From: Nick <20588554+nicknsy@users.noreply.github.com> Date: Fri, 24 Feb 2023 06:06:01 -0800 Subject: [PATCH 102/858] Copy IsAutomated option when making MetadataRefreshOptions copy. (#9385) --- MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs index 8a37094620..9e91a8bcd7 100644 --- a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs +++ b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs @@ -26,6 +26,7 @@ namespace MediaBrowser.Controller.Providers ReplaceAllMetadata = copy.ReplaceAllMetadata; EnableRemoteContentProbe = copy.EnableRemoteContentProbe; + IsAutomated = copy.IsAutomated; ImageRefreshMode = copy.ImageRefreshMode; ReplaceAllImages = copy.ReplaceAllImages; ReplaceImages = copy.ReplaceImages; From b3273f0f9ac73c466dee47240f00a2b5890b5595 Mon Sep 17 00:00:00 2001 From: Cody Robibero <cody@robibe.ro> Date: Fri, 24 Feb 2023 08:06:19 -0700 Subject: [PATCH 103/858] Simplify audio transcode channel lookup --- .../MediaEncoding/EncodingHelper.cs | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index f4684a2218..a4714e7bf8 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -71,6 +71,21 @@ namespace MediaBrowser.Controller.MediaEncoding "m4v", }; + // Set max transcoding channels for encoders that can't handle more than a set amount of channels + // AAC, FLAC, ALAC, libopus, libvorbis encoders all support at least 8 channels + private static readonly Dictionary<string, int> _audioTranscodeChannelLookup = new(StringComparer.OrdinalIgnoreCase) + { + { "wmav2", 2 }, + { "libmp3lame", 2 }, + { "libfdk_aac", 6 }, + { "aac_at", 6 }, + { "ac3", 6 }, + { "eac3", 6 }, + { "dts", 6 }, + { "mlp", 6 }, + { "truehd", 6 }, + }; + public EncodingHelper( IApplicationPaths appPaths, IMediaEncoder mediaEncoder, @@ -2231,25 +2246,14 @@ namespace MediaBrowser.Controller.MediaEncoding if (isTranscodingAudio) { - // Set max transcoding channels for encoders that can't handle more than a set amount of channels - // AAC, FLAC, ALAC, libopus, libvorbis encoders all support at least 8 channels - int transcoderChannelLimit = GetAudioEncoder(state) switch + var audioEncoder = GetAudioEncoder(state); + if (!_audioTranscodeChannelLookup.TryGetValue(audioEncoder, out var transcoderChannelLimit)) { - string audioEncoder when audioEncoder.Equals("wmav2", StringComparison.OrdinalIgnoreCase) - || audioEncoder.Equals("libmp3lame", StringComparison.OrdinalIgnoreCase) => 2, - string audioEncoder when audioEncoder.Equals("libfdk_aac", StringComparison.OrdinalIgnoreCase) - || audioEncoder.Equals("aac_at", StringComparison.OrdinalIgnoreCase) - || audioEncoder.Equals("ac3", StringComparison.OrdinalIgnoreCase) - || audioEncoder.Equals("eac3", StringComparison.OrdinalIgnoreCase) - || audioEncoder.Equals("dts", StringComparison.OrdinalIgnoreCase) - || audioEncoder.Equals("mlp", StringComparison.OrdinalIgnoreCase) - || audioEncoder.Equals("truehd", StringComparison.OrdinalIgnoreCase) => 6, - // Set default max transcoding channels to 8 to prevent encoding errors due to asking for too many channels - _ => 8, - }; + // Set default max transcoding channels to 8 to prevent encoding errors due to asking for too many channels. + transcoderChannelLimit = 8; + } // Set resultChannels to minimum between resultChannels, TranscodingMaxAudioChannels, transcoderChannelLimit - resultChannels = transcoderChannelLimit < resultChannels ? transcoderChannelLimit : resultChannels ?? transcoderChannelLimit; if (request.TranscodingMaxAudioChannels < resultChannels) From eaeb65f94d94bab40126c12e8fd89c3c5c6b35d8 Mon Sep 17 00:00:00 2001 From: Bond-009 <bond.009@outlook.com> Date: Fri, 24 Feb 2023 16:22:30 +0100 Subject: [PATCH 104/858] Update MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs Co-authored-by: Cody Robibero <cody@robibe.ro> --- .../Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs index 28da62111e..abef732bbb 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs @@ -61,7 +61,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV var seriesTmdbId = Convert.ToInt32(series?.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture); - if (series == null || seriesTmdbId <= 0) + if (series is null || seriesTmdbId <= 0) { return Enumerable.Empty<RemoteImageInfo>(); } From 49eb04899c1e4f0864674b475a87a26e6dddbd58 Mon Sep 17 00:00:00 2001 From: Cody Robibero <cody@robibe.ro> Date: Fri, 24 Feb 2023 08:53:08 -0700 Subject: [PATCH 105/858] Update MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs Co-authored-by: Shadowghost <Shadowghost@users.noreply.github.com> --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index a4714e7bf8..e8d011a153 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -81,7 +81,7 @@ namespace MediaBrowser.Controller.MediaEncoding { "aac_at", 6 }, { "ac3", 6 }, { "eac3", 6 }, - { "dts", 6 }, + { "dca", 6 }, { "mlp", 6 }, { "truehd", 6 }, }; From c29e8ffe1d1ffe99c895ad228b6cfc5c18fb3b98 Mon Sep 17 00:00:00 2001 From: ipitio <21136719+ipitio@users.noreply.github.com> Date: Fri, 24 Feb 2023 11:45:56 -0500 Subject: [PATCH 106/858] Update MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs Co-authored-by: Cody Robibero <cody@robibe.ro> --- MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs index 48ee78a7cc..0524999c79 100644 --- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs +++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs @@ -235,7 +235,7 @@ namespace MediaBrowser.Controller.Net catch (Exception ex) { // TODO Investigate and properly fix. - Logger.LogError(ex, "Object Disposed Exception"); + Logger.LogError(ex, "Error disposing websocket"); } lock (_activeConnections) From f8f8505286b7b6865b46eb4a500904a79077ff86 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 24 Feb 2023 14:48:33 -0700 Subject: [PATCH 107/858] chore(deps): update github/codeql-action digest to 32dc499 (#9392) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 41d59e2435..6d87af538c 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2 + uses: github/codeql-action/init@32dc499307d133bb5085bae78498c0ac2cf762d5 # v2 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2 + uses: github/codeql-action/autobuild@32dc499307d133bb5085bae78498c0ac2cf762d5 # v2 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2 + uses: github/codeql-action/analyze@32dc499307d133bb5085bae78498c0ac2cf762d5 # v2 From e35119987a80f5048572889f4a0153eba148a1d1 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Sat, 25 Feb 2023 17:20:53 +0100 Subject: [PATCH 108/858] Enable nullable for more files --- .../Tmdb/BoxSets/TmdbBoxSetImageProvider.cs | 2 - .../Tmdb/BoxSets/TmdbBoxSetProvider.cs | 2 - .../Plugins/Tmdb/Movies/TmdbMovieProvider.cs | 55 ++++++++++--------- .../Plugins/Tmdb/People/TmdbPersonProvider.cs | 4 ++ .../Plugins/Tmdb/TV/TmdbSeriesProvider.cs | 9 ++- .../Plugins/Tmdb/TmdbClientManager.cs | 52 +++++++++--------- .../Plugins/Tmdb/TmdbUtils.cs | 4 +- 7 files changed, 69 insertions(+), 59 deletions(-) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs index eee3658de5..ef32b0a074 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Collections.Generic; using System.Globalization; diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs index 1cce7fc35a..e6c7dba250 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Collections.Generic; using System.Globalization; diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs index 753a15c6ee..fc72023666 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs @@ -62,32 +62,35 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies cancellationToken) .ConfigureAwait(false); - var remoteResult = new RemoteSearchResult + if (movie is not null) { - Name = movie.Title ?? movie.OriginalTitle, - SearchProviderName = Name, - ImageUrl = _tmdbClientManager.GetPosterUrl(movie.PosterPath), - Overview = movie.Overview - }; + var remoteResult = new RemoteSearchResult + { + Name = movie.Title ?? movie.OriginalTitle, + SearchProviderName = Name, + ImageUrl = _tmdbClientManager.GetPosterUrl(movie.PosterPath), + Overview = movie.Overview + }; - if (movie.ReleaseDate is not null) - { - var releaseDate = movie.ReleaseDate.Value.ToUniversalTime(); - remoteResult.PremiereDate = releaseDate; - remoteResult.ProductionYear = releaseDate.Year; + if (movie.ReleaseDate is not null) + { + var releaseDate = movie.ReleaseDate.Value.ToUniversalTime(); + remoteResult.PremiereDate = releaseDate; + remoteResult.ProductionYear = releaseDate.Year; + } + + remoteResult.SetProviderId(MetadataProvider.Tmdb, movie.Id.ToString(CultureInfo.InvariantCulture)); + + if (!string.IsNullOrWhiteSpace(movie.ImdbId)) + { + remoteResult.SetProviderId(MetadataProvider.Imdb, movie.ImdbId); + } + + return new[] { remoteResult }; } - - remoteResult.SetProviderId(MetadataProvider.Tmdb, movie.Id.ToString(CultureInfo.InvariantCulture)); - - if (!string.IsNullOrWhiteSpace(movie.ImdbId)) - { - remoteResult.SetProviderId(MetadataProvider.Imdb, movie.ImdbId); - } - - return new[] { remoteResult }; } - IReadOnlyList<SearchMovie> movieResults; + IReadOnlyList<SearchMovie>? movieResults = null; if (searchInfo.TryGetProviderId(MetadataProvider.Imdb, out id)) { var result = await _tmdbClientManager.FindByExternalIdAsync( @@ -95,18 +98,20 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies FindExternalSource.Imdb, TmdbUtils.GetImageLanguagesParam(searchInfo.MetadataLanguage), cancellationToken).ConfigureAwait(false); - movieResults = result.MovieResults; + movieResults = result?.MovieResults; } - else if (searchInfo.TryGetProviderId(MetadataProvider.Tvdb, out id)) + + if (movieResults is null && searchInfo.TryGetProviderId(MetadataProvider.Tvdb, out id)) { var result = await _tmdbClientManager.FindByExternalIdAsync( id, FindExternalSource.TvDb, TmdbUtils.GetImageLanguagesParam(searchInfo.MetadataLanguage), cancellationToken).ConfigureAwait(false); - movieResults = result.MovieResults; + movieResults = result?.MovieResults; } - else + + if (movieResults is null) { movieResults = await _tmdbClientManager .SearchMovieAsync(searchInfo.Name, searchInfo.Year ?? 0, searchInfo.MetadataLanguage, cancellationToken) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs index 44d5bab76d..c03a1ca3b8 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs @@ -105,6 +105,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People if (personTmdbId > 0) { var person = await _tmdbClientManager.GetPersonAsync(personTmdbId, info.MetadataLanguage, cancellationToken).ConfigureAwait(false); + if (person is null) + { + return result; + } result.HasMetadata = true; diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs index 046b63faf8..09d1a739d2 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs @@ -209,7 +209,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV } } - if (string.IsNullOrEmpty(tmdbId)) + if (!int.TryParse(tmdbId, CultureInfo.InvariantCulture, out int tmdbIdInt)) { return result; } @@ -217,9 +217,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV cancellationToken.ThrowIfCancellationRequested(); var tvShow = await _tmdbClientManager - .GetSeriesAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken) + .GetSeriesAsync(tmdbIdInt, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken) .ConfigureAwait(false); + if (tvShow is null) + { + return result; + } + result = new MetadataResult<Series> { Item = MapTvShowToSeries(tvShow, info.MetadataCountryCode), diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs index c7441bf357..500ebaf71c 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs @@ -1,6 +1,4 @@ -#nullable disable - -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.Threading; @@ -50,10 +48,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// <param name="imageLanguages">A comma-separated list of image languages.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>The TMDb movie or null if not found.</returns> - public async Task<Movie> GetMovieAsync(int tmdbId, string language, string imageLanguages, CancellationToken cancellationToken) + public async Task<Movie?> GetMovieAsync(int tmdbId, string? language, string? imageLanguages, CancellationToken cancellationToken) { var key = $"movie-{tmdbId.ToString(CultureInfo.InvariantCulture)}-{language}"; - if (_memoryCache.TryGetValue(key, out Movie movie)) + if (_memoryCache.TryGetValue(key, out Movie? movie)) { return movie; } @@ -89,10 +87,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// <param name="imageLanguages">A comma-separated list of image languages.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>The TMDb collection or null if not found.</returns> - public async Task<Collection> GetCollectionAsync(int tmdbId, string language, string imageLanguages, CancellationToken cancellationToken) + public async Task<Collection?> GetCollectionAsync(int tmdbId, string? language, string? imageLanguages, CancellationToken cancellationToken) { var key = $"collection-{tmdbId.ToString(CultureInfo.InvariantCulture)}-{language}"; - if (_memoryCache.TryGetValue(key, out Collection collection)) + if (_memoryCache.TryGetValue(key, out Collection? collection)) { return collection; } @@ -122,10 +120,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// <param name="imageLanguages">A comma-separated list of image languages.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>The TMDb tv show information or null if not found.</returns> - public async Task<TvShow> GetSeriesAsync(int tmdbId, string language, string imageLanguages, CancellationToken cancellationToken) + public async Task<TvShow?> GetSeriesAsync(int tmdbId, string? language, string? imageLanguages, CancellationToken cancellationToken) { var key = $"series-{tmdbId.ToString(CultureInfo.InvariantCulture)}-{language}"; - if (_memoryCache.TryGetValue(key, out TvShow series)) + if (_memoryCache.TryGetValue(key, out TvShow? series)) { return series; } @@ -162,7 +160,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// <param name="imageLanguages">A comma-separated list of image languages.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>The TMDb tv show episode group information or null if not found.</returns> - private async Task<TvGroupCollection> GetSeriesGroupAsync(int tvShowId, string displayOrder, string language, string imageLanguages, CancellationToken cancellationToken) + private async Task<TvGroupCollection?> GetSeriesGroupAsync(int tvShowId, string displayOrder, string? language, string? imageLanguages, CancellationToken cancellationToken) { TvGroupType? groupType = string.Equals(displayOrder, "originalAirDate", StringComparison.Ordinal) ? TvGroupType.OriginalAirDate : @@ -180,7 +178,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb } var key = $"group-{tvShowId.ToString(CultureInfo.InvariantCulture)}-{displayOrder}-{language}"; - if (_memoryCache.TryGetValue(key, out TvGroupCollection group)) + if (_memoryCache.TryGetValue(key, out TvGroupCollection? group)) { return group; } @@ -217,10 +215,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// <param name="imageLanguages">A comma-separated list of image languages.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>The TMDb tv season information or null if not found.</returns> - public async Task<TvSeason> GetSeasonAsync(int tvShowId, int seasonNumber, string language, string imageLanguages, CancellationToken cancellationToken) + public async Task<TvSeason?> GetSeasonAsync(int tvShowId, int seasonNumber, string? language, string? imageLanguages, CancellationToken cancellationToken) { var key = $"season-{tvShowId.ToString(CultureInfo.InvariantCulture)}-s{seasonNumber.ToString(CultureInfo.InvariantCulture)}-{language}"; - if (_memoryCache.TryGetValue(key, out TvSeason season)) + if (_memoryCache.TryGetValue(key, out TvSeason? season)) { return season; } @@ -254,10 +252,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// <param name="imageLanguages">A comma-separated list of image languages.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>The TMDb tv episode information or null if not found.</returns> - public async Task<TvEpisode> GetEpisodeAsync(int tvShowId, int seasonNumber, int episodeNumber, string displayOrder, string language, string imageLanguages, CancellationToken cancellationToken) + public async Task<TvEpisode?> GetEpisodeAsync(int tvShowId, int seasonNumber, int episodeNumber, string displayOrder, string? language, string? imageLanguages, CancellationToken cancellationToken) { var key = $"episode-{tvShowId.ToString(CultureInfo.InvariantCulture)}-s{seasonNumber.ToString(CultureInfo.InvariantCulture)}e{episodeNumber.ToString(CultureInfo.InvariantCulture)}-{displayOrder}-{language}"; - if (_memoryCache.TryGetValue(key, out TvEpisode episode)) + if (_memoryCache.TryGetValue(key, out TvEpisode? episode)) { return episode; } @@ -301,10 +299,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// <param name="language">The episode's language.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>The TMDb person information or null if not found.</returns> - public async Task<Person> GetPersonAsync(int personTmdbId, string language, CancellationToken cancellationToken) + public async Task<Person?> GetPersonAsync(int personTmdbId, string language, CancellationToken cancellationToken) { var key = $"person-{personTmdbId.ToString(CultureInfo.InvariantCulture)}-{language}"; - if (_memoryCache.TryGetValue(key, out Person person)) + if (_memoryCache.TryGetValue(key, out Person? person)) { return person; } @@ -333,14 +331,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// <param name="language">The item's language.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>The TMDb item or null if not found.</returns> - public async Task<FindContainer> FindByExternalIdAsync( + public async Task<FindContainer?> FindByExternalIdAsync( string externalId, FindExternalSource source, string language, CancellationToken cancellationToken) { var key = $"find-{source.ToString()}-{externalId.ToString(CultureInfo.InvariantCulture)}-{language}"; - if (_memoryCache.TryGetValue(key, out FindContainer result)) + if (_memoryCache.TryGetValue(key, out FindContainer? result)) { return result; } @@ -372,7 +370,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb public async Task<IReadOnlyList<SearchTv>> SearchSeriesAsync(string name, string language, int year = 0, CancellationToken cancellationToken = default) { var key = $"searchseries-{name}-{language}"; - if (_memoryCache.TryGetValue(key, out SearchContainer<SearchTv> series)) + if (_memoryCache.TryGetValue(key, out SearchContainer<SearchTv>? series) && series is not null) { return series.Results; } @@ -400,7 +398,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb public async Task<IReadOnlyList<SearchPerson>> SearchPersonAsync(string name, CancellationToken cancellationToken) { var key = $"searchperson-{name}"; - if (_memoryCache.TryGetValue(key, out SearchContainer<SearchPerson> person)) + if (_memoryCache.TryGetValue(key, out SearchContainer<SearchPerson>? person) && person is not null) { return person.Results; } @@ -442,7 +440,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb public async Task<IReadOnlyList<SearchMovie>> SearchMovieAsync(string name, int year, string language, CancellationToken cancellationToken) { var key = $"moviesearch-{name}-{year.ToString(CultureInfo.InvariantCulture)}-{language}"; - if (_memoryCache.TryGetValue(key, out SearchContainer<SearchMovie> movies)) + if (_memoryCache.TryGetValue(key, out SearchContainer<SearchMovie>? movies) && movies is not null) { return movies.Results; } @@ -471,7 +469,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb public async Task<IReadOnlyList<SearchCollection>> SearchCollectionAsync(string name, string language, CancellationToken cancellationToken) { var key = $"collectionsearch-{name}-{language}"; - if (_memoryCache.TryGetValue(key, out SearchContainer<SearchCollection> collections)) + if (_memoryCache.TryGetValue(key, out SearchContainer<SearchCollection>? collections) && collections is not null) { return collections.Results; } @@ -496,7 +494,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// <param name="size">The image size to fetch.</param> /// <param name="path">The relative URL of the image.</param> /// <returns>The absolute URL.</returns> - private string GetUrl(string size, string path) + private string? GetUrl(string? size, string path) { if (string.IsNullOrEmpty(path)) { @@ -511,7 +509,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// </summary> /// <param name="posterPath">The relative URL of the poster.</param> /// <returns>The absolute URL.</returns> - public string GetPosterUrl(string posterPath) + public string? GetPosterUrl(string posterPath) { return GetUrl(Plugin.Instance.Configuration.PosterSize, posterPath); } @@ -521,7 +519,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// </summary> /// <param name="actorProfilePath">The relative URL of the profile image.</param> /// <returns>The absolute URL.</returns> - public string GetProfileUrl(string actorProfilePath) + public string? GetProfileUrl(string actorProfilePath) { return GetUrl(Plugin.Instance.Configuration.ProfileSize, actorProfilePath); } @@ -579,7 +577,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// <param name="type">The type of the image.</param> /// <param name="requestLanguage">The requested language.</param> /// <returns>The remote images.</returns> - private IEnumerable<RemoteImageInfo> ConvertToRemoteImageInfo(IReadOnlyList<ImageData> images, string size, ImageType type, string requestLanguage) + private IEnumerable<RemoteImageInfo> ConvertToRemoteImageInfo(IReadOnlyList<ImageData> images, string? size, ImageType type, string requestLanguage) { // sizes provided are for original resolution, don't store them when downloading scaled images var scaleImage = !string.Equals(size, "original", StringComparison.OrdinalIgnoreCase); diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs index 44c2c81f44..b326d22c87 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Text.RegularExpressions; using MediaBrowser.Model.Entities; using TMDbLib.Objects.General; @@ -128,7 +129,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// </summary> /// <param name="language">The language code.</param> /// <returns>The normalized language code.</returns> - public static string NormalizeLanguage(string language) + [return: NotNullIfNotNull(nameof(language))] + public static string? NormalizeLanguage(string? language) { if (string.IsNullOrEmpty(language)) { From da25c3ad7b5007c6237777e0d5f2b116789d2e23 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 25 Feb 2023 09:29:23 -0700 Subject: [PATCH 109/858] chore(deps): update dependency efcoresecondlevelcacheinterceptor to v3.8.5 (#9393) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index c2a4d5ff3d..93aad5d918 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -17,7 +17,7 @@ <PackageVersion Include="Diacritics" Version="3.3.14" /> <PackageVersion Include="DiscUtils.Udf" Version="0.16.13" /> <PackageVersion Include="DotNet.Glob" Version="3.1.3" /> - <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.8.3" /> + <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.8.5" /> <PackageVersion Include="FsCheck.Xunit" Version="2.16.5" /> <PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" /> <PackageVersion Include="libse" Version="3.6.10" /> From cb2d72d7ec840fb56ce7249bc21ffad565ea5335 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 26 Feb 2023 07:32:12 -0700 Subject: [PATCH 110/858] chore(deps): update peter-evans/find-comment digest to 034abe9 (#9401) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/openapi.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index 5ba53af865..aa2e0417fe 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -103,7 +103,7 @@ jobs: body="${body//$'\r'/'%0D'}" echo ::set-output name=body::$body - name: Find difference comment - uses: peter-evans/find-comment@85a676a52594b4481e0532825a2d8906ef96dac2 # v2 + uses: peter-evans/find-comment@034abe94d3191f9c89d870519735beae326f2bdb # v2 id: find-comment with: issue-number: ${{ github.event.pull_request.number }} From edc627fd5b5a5bd19c843dd9e2970b1ebce3fbfd Mon Sep 17 00:00:00 2001 From: Nyanmisaka <nst799610810@gmail.com> Date: Sun, 26 Feb 2023 22:33:27 +0800 Subject: [PATCH 111/858] Improve the Vulkan based subtitle burn-in performance (#9402) https://gitlab.freedesktop.org/mesa/mesa/-/issues/850 Currently Mesa RADV does not support a dedicated transfer queue. Signed-off-by: nyanmisaka <nst799610810@gmail.com> --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index e8d011a153..e02a932b12 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -4232,12 +4232,12 @@ namespace MediaBrowser.Controller.MediaEncoding subFilters.Add(subTextSubtitlesFilter); } - subFilters.Add("hwupload=derive_device=vulkan:extra_hw_frames=16"); + // prefer vaapi hwupload to vulkan hwupload, + // Mesa RADV does not support a dedicated transfer queue. + subFilters.Add("hwupload=derive_device=vaapi,format=vaapi,hwmap=derive_device=vulkan"); overlayFilters.Add("overlay_vulkan=eof_action=endall:shortest=1:repeatlast=0"); - - // explicitly sync using libplacebo. - overlayFilters.Add("libplacebo=format=nv12:upscaler=none:downscaler=none"); + overlayFilters.Add("scale_vulkan=format=nv12"); // OUTPUT vaapi(nv12/bgra) surface(vram) // reverse-mapping via vaapi-vulkan interop. From 3e74377036ff0aea33b09381fb5e15efb0a6ded8 Mon Sep 17 00:00:00 2001 From: knackebrot <knackebrot@tfwno.gf> Date: Tue, 12 Jul 2022 15:54:17 +0200 Subject: [PATCH 112/858] Calculate output bitrate from output channel count --- Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 2 +- Jellyfin.Api/Helpers/StreamingHelpers.cs | 2 +- .../MediaEncoding/EncodingHelper.cs | 53 +++++++++---------- 3 files changed, 27 insertions(+), 30 deletions(-) diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index 245239233c..555babc0e1 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -223,7 +223,7 @@ public class DynamicHlsHelper sdrVideoUrl += "&AllowVideoStreamCopy=false"; var sdrOutputVideoBitrate = _encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec); - var sdrOutputAudioBitrate = _encodingHelper.GetAudioBitrateParam(state.VideoRequest, state.AudioStream) ?? 0; + var sdrOutputAudioBitrate = _encodingHelper.GetAudioBitrateParam(state.VideoRequest, state.AudioStream, state.OutputAudioChannels) ?? 0; var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate; var sdrPlaylist = AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup); diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index 9b5a14c4de..6ce98d2317 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -183,7 +183,7 @@ public static class StreamingHelpers state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.'); - state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, streamingRequest.AudioCodec, state.AudioStream); + state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, streamingRequest.AudioCodec, state.AudioStream, state.OutputAudioChannels); state.OutputAudioCodec = streamingRequest.AudioCodec; diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index e02a932b12..648358b598 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -2111,50 +2111,47 @@ namespace MediaBrowser.Controller.MediaEncoding return Convert.ToInt32(scaleFactor * bitrate); } - public int? GetAudioBitrateParam(BaseEncodingJobOptions request, MediaStream audioStream) + public int? GetAudioBitrateParam(BaseEncodingJobOptions request, MediaStream audioStream, int? outputAudioChannels) { - return GetAudioBitrateParam(request.AudioBitRate, request.AudioCodec, audioStream); + return GetAudioBitrateParam(request.AudioBitRate, request.AudioCodec, audioStream, outputAudioChannels); } - public int? GetAudioBitrateParam(int? audioBitRate, string audioCodec, MediaStream audioStream) + public int? GetAudioBitrateParam(int? audioBitRate, string audioCodec, MediaStream audioStream, int? outputAudioChannels) { if (audioStream is null) { return null; } - if (audioBitRate.HasValue && string.IsNullOrEmpty(audioCodec)) + var inputChannels = audioStream.Channels ?? 0; + var outputChannels = outputAudioChannels ?? 0; + + if (audioBitRate.HasValue && (string.IsNullOrEmpty(audioCodec) + || string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase) + || string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase) + || string.Equals(audioCodec, "opus", StringComparison.OrdinalIgnoreCase) + || string.Equals(audioCodec, "vorbis", StringComparison.OrdinalIgnoreCase) + || string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase) + || string.Equals(audioCodec, "eac3", StringComparison.OrdinalIgnoreCase))) { - return Math.Min(384000, audioBitRate.Value); + return (inputChannels, outputChannels) switch + { + (>= 6, >= 6 or 0) => Math.Min(640000, audioBitRate.Value), + (> 0, > 0) => Math.Min(outputChannels * 128000, audioBitRate.Value), + (> 0, _) => Math.Min(inputChannels * 128000, audioBitRate.Value), + (_, _) => Math.Min(384000, audioBitRate.Value) + }; } - if (audioBitRate.HasValue && !string.IsNullOrEmpty(audioCodec)) + if (audioBitRate.HasValue && (string.Equals(audioCodec, "flac", StringComparison.OrdinalIgnoreCase) + || string.Equals(audioCodec, "alac", StringComparison.OrdinalIgnoreCase))) { - if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase) - || string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase) - || string.Equals(audioCodec, "opus", StringComparison.OrdinalIgnoreCase) - || string.Equals(audioCodec, "vorbis", StringComparison.OrdinalIgnoreCase) - || string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase) - || string.Equals(audioCodec, "eac3", StringComparison.OrdinalIgnoreCase)) + if ((audioStream.Channels ?? 0) >= 6) { - if ((audioStream.Channels ?? 0) >= 6) - { - return Math.Min(640000, audioBitRate.Value); - } - - return Math.Min(384000, audioBitRate.Value); + return Math.Min(3584000, audioBitRate.Value); } - if (string.Equals(audioCodec, "flac", StringComparison.OrdinalIgnoreCase) - || string.Equals(audioCodec, "alac", StringComparison.OrdinalIgnoreCase)) - { - if ((audioStream.Channels ?? 0) >= 6) - { - return Math.Min(3584000, audioBitRate.Value); - } - - return Math.Min(1536000, audioBitRate.Value); - } + return Math.Min(1536000, audioBitRate.Value); } // Empty bitrate area is not allow on iOS From aa99aaebc4e37ca1e16c11f72dd4a57038200179 Mon Sep 17 00:00:00 2001 From: knackebrot <knackebrot@tfwno.gf> Date: Mon, 7 Nov 2022 00:15:04 +0100 Subject: [PATCH 113/858] Add audio vbr calculation --- .../Controllers/DynamicHlsController.cs | 29 +++++++-- .../MediaEncoding/EncodingHelper.cs | 64 ++++++++++++++++++- .../Configuration/EncodingOptions.cs | 6 ++ 3 files changed, 91 insertions(+), 8 deletions(-) diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 4d8b4de24f..7b1830761c 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1685,14 +1685,25 @@ public class DynamicHlsController : BaseJellyfinApiController audioTranscodeParams += "-acodec " + audioCodec; - if (state.OutputAudioBitrate.HasValue) + var audioBitrate = state.OutputAudioBitrate; + var audioChannels = state.OutputAudioChannels; + + if (audioBitrate.HasValue) { - audioTranscodeParams += " -ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture); + string vbrParam; + if (_encodingOptions.EnableAudioVbr && (vbrParam = _encodingHelper.GetAudioVbrModeParam(state.OutputAudioCodec, audioBitrate.Value / audioChannels ?? 2)) != null) + { + audioTranscodeParams += vbrParam; + } + else + { + audioTranscodeParams += " -ab " + audioBitrate.Value.ToString(CultureInfo.InvariantCulture); + } } - if (state.OutputAudioChannels.HasValue) + if (audioChannels.HasValue) { - audioTranscodeParams += " -ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture); + audioTranscodeParams += " -ac " + audioChannels.Value.ToString(CultureInfo.InvariantCulture); } if (state.OutputAudioSampleRate.HasValue) @@ -1747,7 +1758,15 @@ public class DynamicHlsController : BaseJellyfinApiController if (bitrate.HasValue) { - args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture); + string vbrParam; + if (_encodingOptions.EnableAudioVbr && (vbrParam = _encodingHelper.GetAudioVbrModeParam(state.OutputAudioCodec, bitrate.Value / channels ?? 2)) != null) + { + args += vbrParam; + } + else + { + args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture); + } } if (state.OutputAudioSampleRate.HasValue) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 648358b598..551160934d 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -2160,6 +2160,47 @@ namespace MediaBrowser.Controller.MediaEncoding return 128000; } + public string GetAudioVbrModeParam(string encoder, int bitratePerChannel) + { + if (encoder == "libfdk_aac") + { + return " -vbr:a " + bitratePerChannel switch + { + < 32000 => "1", + < 48000 => "2", + < 64000 => "3", + < 96000 => "4", + _ => "5" + }; + } + + if (encoder == "libmp3lame") + { + return " -qscale:a " + bitratePerChannel switch + { + < 48000 => "8", + < 64000 => "6", + < 88000 => "4", + < 112000 => "2", + _ => "0" + }; + } + + if (encoder == "libvorbis") + { + return " -qscale:a " + bitratePerChannel switch + { + < 40000 => "0", + < 56000 => "2", + < 80000 => "4", + < 112000 => "6", + _ => "8" + }; + } + + return null; + } + public string GetAudioFilterParam(EncodingJobInfo state, EncodingOptions encodingOptions) { var channels = state.OutputAudioChannels; @@ -5801,7 +5842,15 @@ namespace MediaBrowser.Controller.MediaEncoding if (bitrate.HasValue) { - args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture); + string vbrParam; + if (encodingOptions.EnableAudioVbr && (vbrParam = GetAudioVbrModeParam(codec, bitrate.Value / channels ?? 2)) != null) + { + args += vbrParam; + } + else + { + args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture); + } } if (state.OutputAudioSampleRate.HasValue) @@ -5819,13 +5868,22 @@ namespace MediaBrowser.Controller.MediaEncoding var audioTranscodeParams = new List<string>(); var bitrate = state.OutputAudioBitrate; + var channels = state.OutputAudioChannels; if (bitrate.HasValue) { - audioTranscodeParams.Add("-ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture)); + string vbrParam; + if (encodingOptions.EnableAudioVbr && (vbrParam = GetAudioVbrModeParam(state.OutputAudioCodec, bitrate.Value / channels ?? 2)) != null) + { + audioTranscodeParams.Add(vbrParam); + } + else + { + audioTranscodeParams.Add("-ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture)); + } } - if (state.OutputAudioChannels.HasValue) + if (channels.HasValue) { audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture)); } diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index 0ff95a2e1f..b43e0f024b 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -14,6 +14,7 @@ public class EncodingOptions public EncodingOptions() { EnableFallbackFont = false; + EnableAudioVbr = false; DownMixAudioBoost = 2; DownMixStereoAlgorithm = DownMixStereoAlgorithms.None; MaxMuxingQueueSize = 2048; @@ -70,6 +71,11 @@ public class EncodingOptions /// </summary> public bool EnableFallbackFont { get; set; } + /// <summary> + /// Gets or sets a value indicating whether audio VBR is enabled. + /// </summary> + public bool EnableAudioVbr { get; set; } + /// <summary> /// Gets or sets the audio boost applied when downmixing audio. /// </summary> From 2e3b4bda7bad0b7d72f8b7bbd190d7c9cdb84061 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Fri, 24 Feb 2023 14:31:57 +0100 Subject: [PATCH 114/858] Take channels into account when calculating fallback audio bitrate --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 551160934d..bbb18e737c 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -2146,7 +2146,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (audioBitRate.HasValue && (string.Equals(audioCodec, "flac", StringComparison.OrdinalIgnoreCase) || string.Equals(audioCodec, "alac", StringComparison.OrdinalIgnoreCase))) { - if ((audioStream.Channels ?? 0) >= 6) + if (inputChannels >= 6) { return Math.Min(3584000, audioBitRate.Value); } @@ -2155,9 +2155,9 @@ namespace MediaBrowser.Controller.MediaEncoding } // Empty bitrate area is not allow on iOS - // Default audio bitrate to 128K if it is not being requested + // Default audio bitrate to 128K per channel if it is not being requested // https://ffmpeg.org/ffmpeg-codecs.html#toc-Codec-Options - return 128000; + return 128000 * (outputAudioChannels ?? audioStream.Channels ?? 1); } public string GetAudioVbrModeParam(string encoder, int bitratePerChannel) From f3840e0fdbc85d9009666b51b07bd3a21786cb39 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Fri, 24 Feb 2023 15:04:52 +0100 Subject: [PATCH 115/858] Fix encoder checks for DTS and TrueHD --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 5 +++++ MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index bbb18e737c..bcb16eb38e 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -614,6 +614,11 @@ namespace MediaBrowser.Controller.MediaEncoding return "flac"; } + if (string.Equals(codec, "dts", StringComparison.OrdinalIgnoreCase)) + { + return "dca"; + } + return codec.ToLowerInvariant(); } diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index 540d50bf15..3980353d15 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -25,11 +25,12 @@ namespace MediaBrowser.MediaEncoding.Encoder "mpeg2video", "mpeg4", "msmpeg4", - "dts", + "dca", "ac3", "aac", "mp3", "flac", + "truehd", "h264_qsv", "hevc_qsv", "mpeg2_qsv", @@ -59,10 +60,12 @@ namespace MediaBrowser.MediaEncoding.Encoder "aac_at", "libfdk_aac", "ac3", + "dca", "libmp3lame", "libopus", "libvorbis", "flac", + "truehd", "srt", "h264_amf", "hevc_amf", From 4a1498f614ca3f51908e8e7ead0ea921222f0f2b Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Fri, 24 Feb 2023 15:21:22 +0100 Subject: [PATCH 116/858] Add DTS and TrueHD bitrate limits, enforce bitrate limits if no bitrate is requested --- .../MediaEncoding/EncodingHelper.cs | 45 ++++++++++++++----- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index bcb16eb38e..c08e3a076c 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -2130,37 +2130,60 @@ namespace MediaBrowser.Controller.MediaEncoding var inputChannels = audioStream.Channels ?? 0; var outputChannels = outputAudioChannels ?? 0; + var bitrate = audioBitRate.HasValue ? audioBitRate.Value : int.MaxValue; - if (audioBitRate.HasValue && (string.IsNullOrEmpty(audioCodec) + if (string.IsNullOrEmpty(audioCodec) || string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase) || string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase) || string.Equals(audioCodec, "opus", StringComparison.OrdinalIgnoreCase) || string.Equals(audioCodec, "vorbis", StringComparison.OrdinalIgnoreCase) || string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase) - || string.Equals(audioCodec, "eac3", StringComparison.OrdinalIgnoreCase))) + || string.Equals(audioCodec, "eac3", StringComparison.OrdinalIgnoreCase)) { return (inputChannels, outputChannels) switch { - (>= 6, >= 6 or 0) => Math.Min(640000, audioBitRate.Value), - (> 0, > 0) => Math.Min(outputChannels * 128000, audioBitRate.Value), - (> 0, _) => Math.Min(inputChannels * 128000, audioBitRate.Value), - (_, _) => Math.Min(384000, audioBitRate.Value) + (>= 6, >= 6 or 0) => Math.Min(640000, bitrate), + (> 0, > 0) => Math.Min(outputChannels * 128000, bitrate), + (> 0, _) => Math.Min(inputChannels * 128000, bitrate), + (_, _) => Math.Min(384000, bitrate) }; } - if (audioBitRate.HasValue && (string.Equals(audioCodec, "flac", StringComparison.OrdinalIgnoreCase) - || string.Equals(audioCodec, "alac", StringComparison.OrdinalIgnoreCase))) + if (string.Equals(audioCodec, "flac", StringComparison.OrdinalIgnoreCase) + || string.Equals(audioCodec, "alac", StringComparison.OrdinalIgnoreCase)) { if (inputChannels >= 6) { - return Math.Min(3584000, audioBitRate.Value); + return Math.Min(3584000, bitrate); } - return Math.Min(1536000, audioBitRate.Value); + return Math.Min(1536000, bitrate); + } + + if (string.Equals(audioCodec, "dts", StringComparison.OrdinalIgnoreCase) + || string.Equals(audioCodec, "dca", StringComparison.OrdinalIgnoreCase)) + { + return (inputChannels, outputChannels) switch + { + (>= 6, >= 6 or 0) => Math.Min(768000, bitrate), + (> 0, > 0) => Math.Min(outputChannels * 136000, bitrate), + (> 0, _) => Math.Min(inputChannels * 136000, bitrate), + (_, _) => Math.Min(672000, bitrate) + }; + } + + if (string.Equals(audioCodec, "truehd", StringComparison.OrdinalIgnoreCase)) + { + return (inputChannels, outputChannels) switch + { + (> 0, > 0) => Math.Min(outputChannels * 768000, bitrate), + (> 0, _) => Math.Min(inputChannels * 768000, bitrate), + (_, _) => Math.Min(768000, bitrate) + }; } // Empty bitrate area is not allow on iOS - // Default audio bitrate to 128K per channel if it is not being requested + // Default audio bitrate to 128K per channel if we don't have codec specific defaults // https://ffmpeg.org/ffmpeg-codecs.html#toc-Codec-Options return 128000 * (outputAudioChannels ?? audioStream.Channels ?? 1); } From 9880a2b3e1ae0b88b5f5545681bd7394f3d35e84 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Mon, 27 Feb 2023 00:08:25 +0100 Subject: [PATCH 117/858] Enforce HLS codec restrictions --- MediaBrowser.Model/Dlna/StreamBuilder.cs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index ab81bfb34c..b3982fefa5 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -799,6 +799,14 @@ namespace MediaBrowser.Model.Dlna { // Prefer matching video codecs var videoCodecs = ContainerProfile.SplitValue(videoCodec); + + // Enforce HLS video codec restrictions + if (string.Equals(playlistItem.SubProtocol, "hls", StringComparison.OrdinalIgnoreCase)) + { + var supportedHlsVideoCodecs = new List<string> { "h264", "hevc" }; + videoCodecs = videoCodecs.Where(codec => supportedHlsVideoCodecs.Contains(codec)).ToArray(); + } + var directVideoCodec = ContainerProfile.ContainsContainer(videoCodecs, videoStream?.Codec) ? videoStream?.Codec : null; if (directVideoCodec is not null) { @@ -834,6 +842,22 @@ namespace MediaBrowser.Model.Dlna // Prefer matching audio codecs, could do better here var audioCodecs = ContainerProfile.SplitValue(audioCodec); + + // Enforce HLS audio codec restrictions + if (string.Equals(playlistItem.SubProtocol, "hls", StringComparison.OrdinalIgnoreCase)) + { + var supportedHlsAudioCodecs = new List<string> { "aac", "ac3", "eac3", "mp3" }; + if (string.Equals(playlistItem.Container, "mp4", StringComparison.OrdinalIgnoreCase)) + { + // fMP4 supports more codecs than TS + supportedHlsAudioCodecs.Add("alac"); + supportedHlsAudioCodecs.Add("flac"); + supportedHlsAudioCodecs.Add("opus"); + } + + audioCodecs = audioCodecs.Where(codec => supportedHlsAudioCodecs.Contains(codec)).ToArray(); + } + var directAudioStream = candidateAudioStreams.FirstOrDefault(stream => ContainerProfile.ContainsContainer(audioCodecs, stream.Codec)); playlistItem.AudioCodecs = audioCodecs; if (directAudioStream is not null) From 4873d2a54db7327b3cc021cc587d31d932b57f2e Mon Sep 17 00:00:00 2001 From: Cody Robibero <cody@robibe.ro> Date: Mon, 27 Feb 2023 05:48:37 -0700 Subject: [PATCH 118/858] Fix auth endpoints using api key (#9408) --- .../DefaultAuthorizationHandler.cs | 9 +++++- .../DefaultAuthorizationHandlerTests.cs | 32 ++++++++++++++++++- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs index b1d97e4a1d..de271ab640 100644 --- a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs +++ b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs @@ -46,6 +46,13 @@ namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy return Task.CompletedTask; } + if (isApiKey) + { + // Api keys are unrestricted. + context.Succeed(requirement); + return Task.CompletedTask; + } + var isInLocalNetwork = _httpContextAccessor.HttpContext is not null && _networkManager.IsInLocalNetwork(_httpContextAccessor.HttpContext.GetNormalizedRemoteIp()); var user = _userManager.GetUserById(userId); @@ -62,7 +69,7 @@ namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy } // Admins can do everything - if (isApiKey || context.User.IsInRole(UserRoles.Administrator)) + if (context.User.IsInRole(UserRoles.Administrator)) { context.Succeed(requirement); return Task.CompletedTask; diff --git a/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs index 7c85ddd620..ad8a051fdc 100644 --- a/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs @@ -1,9 +1,13 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Net; +using System.Security.Claims; using System.Threading.Tasks; using AutoFixture; using AutoFixture.AutoMoq; using Jellyfin.Api.Auth.DefaultAuthorizationPolicy; using Jellyfin.Api.Constants; +using Jellyfin.Data.Entities; using Jellyfin.Server.Implementations.Security; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Library; @@ -51,6 +55,32 @@ namespace Jellyfin.Api.Tests.Auth.DefaultAuthorizationPolicy Assert.True(context.HasSucceeded); } + [Fact] + public async Task ShouldSucceedOnApiKey() + { + TestHelpers.SetupConfigurationManager(_configurationManagerMock, true); + + _httpContextAccessor + .Setup(h => h.HttpContext!.Connection.RemoteIpAddress) + .Returns(new IPAddress(0)); + + _userManagerMock + .Setup(u => u.GetUserById(It.IsAny<Guid>())) + .Returns<User>(null); + + var claims = new[] + { + new Claim(InternalClaimTypes.IsApiKey, bool.TrueString) + }; + + var identity = new ClaimsIdentity(claims, string.Empty); + var principal = new ClaimsPrincipal(identity); + var context = new AuthorizationHandlerContext(_requirements, principal, null); + + await _sut.HandleAsync(context); + Assert.True(context.HasSucceeded); + } + [Theory] [MemberData(nameof(GetParts_ValidAuthHeader_Success_Data))] public void GetParts_ValidAuthHeader_Success(string input, Dictionary<string, string> parts) From c760a50d59280d7c2f78b44ba9788751ab980046 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Mon, 27 Feb 2023 16:03:12 +0100 Subject: [PATCH 119/858] Apply review suggestions --- .../MediaEncoding/EncodingHelper.cs | 16 ++++++++-------- MediaBrowser.Model/Dlna/StreamBuilder.cs | 18 +++++++++--------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index c08e3a076c..5f38ddbba3 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -2130,7 +2130,7 @@ namespace MediaBrowser.Controller.MediaEncoding var inputChannels = audioStream.Channels ?? 0; var outputChannels = outputAudioChannels ?? 0; - var bitrate = audioBitRate.HasValue ? audioBitRate.Value : int.MaxValue; + var bitrate = audioBitRate ?? int.MaxValue; if (string.IsNullOrEmpty(audioCodec) || string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase) @@ -2190,7 +2190,7 @@ namespace MediaBrowser.Controller.MediaEncoding public string GetAudioVbrModeParam(string encoder, int bitratePerChannel) { - if (encoder == "libfdk_aac") + if (string.Equals(encoder, "libfdk_aac", StringComparison.OrdinalIgnoreCase)) { return " -vbr:a " + bitratePerChannel switch { @@ -2202,7 +2202,7 @@ namespace MediaBrowser.Controller.MediaEncoding }; } - if (encoder == "libmp3lame") + if (string.Equals(encoder, "libmp3lame", StringComparison.OrdinalIgnoreCase)) { return " -qscale:a " + bitratePerChannel switch { @@ -2214,7 +2214,7 @@ namespace MediaBrowser.Controller.MediaEncoding }; } - if (encoder == "libvorbis") + if (string.Equals(encoder, "libvorbis", StringComparison.OrdinalIgnoreCase)) { return " -qscale:a " + bitratePerChannel switch { @@ -5870,8 +5870,8 @@ namespace MediaBrowser.Controller.MediaEncoding if (bitrate.HasValue) { - string vbrParam; - if (encodingOptions.EnableAudioVbr && (vbrParam = GetAudioVbrModeParam(codec, bitrate.Value / channels ?? 2)) != null) + var vbrParam = GetAudioVbrModeParam(codec, bitrate.Value / (channels ?? 2)); + if (encodingOptions.EnableAudioVbr && vbrParam is not null) { args += vbrParam; } @@ -5900,8 +5900,8 @@ namespace MediaBrowser.Controller.MediaEncoding if (bitrate.HasValue) { - string vbrParam; - if (encodingOptions.EnableAudioVbr && (vbrParam = GetAudioVbrModeParam(state.OutputAudioCodec, bitrate.Value / channels ?? 2)) != null) + var vbrParam = GetAudioVbrModeParam(state.OutputAudioCodec, bitrate.Value / (channels ?? 2)); + if (encodingOptions.EnableAudioVbr && vbrParam is not null) { audioTranscodeParams.Add(vbrParam); } diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index b3982fefa5..1e05aea275 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -25,6 +25,9 @@ namespace MediaBrowser.Model.Dlna private readonly ILogger _logger; private readonly ITranscoderSupport _transcoderSupport; + private static readonly string[] _supportedHlsVideoCodecs = new string[] { "h264", "hevc" }; + private static readonly string[] _supportedHlsAudioCodecsTs = new string[] { "aac", "ac3", "eac3", "mp3" }; + private static readonly string[] _supportedHlsAudioCodecsMp4 = new string[] { "aac", "ac3", "eac3", "mp3", "alac", "flac", "opus" }; /// <summary> /// Initializes a new instance of the <see cref="StreamBuilder"/> class. @@ -803,8 +806,7 @@ namespace MediaBrowser.Model.Dlna // Enforce HLS video codec restrictions if (string.Equals(playlistItem.SubProtocol, "hls", StringComparison.OrdinalIgnoreCase)) { - var supportedHlsVideoCodecs = new List<string> { "h264", "hevc" }; - videoCodecs = videoCodecs.Where(codec => supportedHlsVideoCodecs.Contains(codec)).ToArray(); + videoCodecs = videoCodecs.Where(codec => _supportedHlsVideoCodecs.Contains(codec)).ToArray(); } var directVideoCodec = ContainerProfile.ContainsContainer(videoCodecs, videoStream?.Codec) ? videoStream?.Codec : null; @@ -846,16 +848,14 @@ namespace MediaBrowser.Model.Dlna // Enforce HLS audio codec restrictions if (string.Equals(playlistItem.SubProtocol, "hls", StringComparison.OrdinalIgnoreCase)) { - var supportedHlsAudioCodecs = new List<string> { "aac", "ac3", "eac3", "mp3" }; if (string.Equals(playlistItem.Container, "mp4", StringComparison.OrdinalIgnoreCase)) { - // fMP4 supports more codecs than TS - supportedHlsAudioCodecs.Add("alac"); - supportedHlsAudioCodecs.Add("flac"); - supportedHlsAudioCodecs.Add("opus"); + audioCodecs = audioCodecs.Where(codec => _supportedHlsAudioCodecsMp4.Contains(codec)).ToArray(); + } + else + { + audioCodecs = audioCodecs.Where(codec => _supportedHlsAudioCodecsTs.Contains(codec)).ToArray(); } - - audioCodecs = audioCodecs.Where(codec => supportedHlsAudioCodecs.Contains(codec)).ToArray(); } var directAudioStream = candidateAudioStreams.FirstOrDefault(stream => ContainerProfile.ContainsContainer(audioCodecs, stream.Codec)); From 16f2cca882de67622aa80a1964a788a2977253ff Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Tue, 28 Feb 2023 15:12:43 +0100 Subject: [PATCH 120/858] Apply review suggestions --- Jellyfin.Api/Controllers/DynamicHlsController.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 7b1830761c..40ca2fcf76 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1690,8 +1690,8 @@ public class DynamicHlsController : BaseJellyfinApiController if (audioBitrate.HasValue) { - string vbrParam; - if (_encodingOptions.EnableAudioVbr && (vbrParam = _encodingHelper.GetAudioVbrModeParam(state.OutputAudioCodec, audioBitrate.Value / audioChannels ?? 2)) != null) + var vbrParam = _encodingHelper.GetAudioVbrModeParam(state.OutputAudioCodec, audioBitrate.Value / (audioChannels ?? 2)); + if (_encodingOptions.EnableAudioVbr && vbrParam is not null) { audioTranscodeParams += vbrParam; } @@ -1758,8 +1758,8 @@ public class DynamicHlsController : BaseJellyfinApiController if (bitrate.HasValue) { - string vbrParam; - if (_encodingOptions.EnableAudioVbr && (vbrParam = _encodingHelper.GetAudioVbrModeParam(state.OutputAudioCodec, bitrate.Value / channels ?? 2)) != null) + var vbrParam = _encodingHelper.GetAudioVbrModeParam(state.OutputAudioCodec, bitrate.Value / (channels ?? 2)); + if (_encodingOptions.EnableAudioVbr && vbrParam is not null) { args += vbrParam; } From 54cd3e6d551d797bace33d65334cf1e98669676d Mon Sep 17 00:00:00 2001 From: Bas <weblate@hanka.mp> Date: Mon, 27 Feb 2023 20:42:46 +0000 Subject: [PATCH 121/858] Translated using Weblate (Dutch) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/nl/ --- Emby.Server.Implementations/Localization/Core/nl.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index 081ba0cc7e..383096f7e2 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -95,13 +95,13 @@ "TaskDownloadMissingSubtitlesDescription": "Zoekt op het internet naar ontbrekende ondertiteling gebaseerd op metadataconfiguratie.", "TaskDownloadMissingSubtitles": "Ontbrekende ondertiteling downloaden", "TaskRefreshChannelsDescription": "Vernieuwt informatie van internet kanalen.", - "TaskRefreshChannels": "Vernieuw kanalen", + "TaskRefreshChannels": "Kanalen vernieuwen", "TaskCleanTranscodeDescription": "Verwijdert transcode bestanden ouder dan 1 dag.", "TaskCleanLogs": "Logboekmap opschonen", "TaskCleanTranscode": "Transcoderingsmap opschonen", "TaskUpdatePluginsDescription": "Downloadt en installeert updates van plug-ins waarvoor automatisch bijwerken is ingeschakeld.", "TaskUpdatePlugins": "Plug-ins bijwerken", - "TaskRefreshPeopleDescription": "Update metadata voor acteurs en regisseurs in de media bibliotheek.", + "TaskRefreshPeopleDescription": "Updatet metadata voor acteurs en regisseurs in je mediabibliotheek.", "TaskRefreshPeople": "Personen vernieuwen", "TaskCleanLogsDescription": "Verwijdert log bestanden ouder dan {0} dagen.", "TaskRefreshLibraryDescription": "Scant de mediabibliotheek op nieuwe bestanden en vernieuwt de metadata.", From 4b01aaa0f7c52557d1500daaae2bc457a56dbffe Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Wed, 1 Mar 2023 00:44:57 +0100 Subject: [PATCH 122/858] Allocate less Lists --- .../ConnectionManagerXmlBuilder.cs | 4 +-- .../ContentDirectoryXmlBuilder.cs | 4 +-- .../AudioBook/AudioBookListResolver.cs | 20 +++++------ .../Channels/ChannelManager.cs | 2 +- .../Collections/CollectionManager.cs | 18 ++++++---- .../Data/SqliteItemRepository.cs | 10 +++--- Emby.Server.Implementations/Dto/DtoService.cs | 12 +++---- .../Library/LibraryManager.cs | 33 +++++++++---------- .../Library/Resolvers/Audio/AudioResolver.cs | 3 +- .../Library/UserViewManager.cs | 2 +- .../Persistence/IItemRepository.cs | 2 +- .../Manager/ProviderManager.cs | 4 +-- .../Playlists/PlaylistItemsProvider.cs | 2 +- .../AudioDb/AudioDbArtistImageProvider.cs | 5 +-- .../MusicBrainz/MusicBrainzAlbumProvider.cs | 10 +++--- .../Plugins/Omdb/OmdbImageProvider.cs | 5 +-- .../Tmdb/BoxSets/TmdbBoxSetImageProvider.cs | 2 +- .../Tmdb/BoxSets/TmdbBoxSetProvider.cs | 4 +-- .../Tmdb/Movies/TmdbMovieImageProvider.cs | 2 +- .../Tmdb/People/TmdbPersonImageProvider.cs | 5 +-- .../Plugins/Tmdb/People/TmdbPersonProvider.cs | 4 +-- .../Tmdb/TV/TmdbEpisodeImageProvider.cs | 5 +-- .../Tmdb/TV/TmdbSeasonImageProvider.cs | 5 +-- .../Tmdb/TV/TmdbSeriesImageProvider.cs | 2 +- 24 files changed, 76 insertions(+), 89 deletions(-) diff --git a/Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs b/Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs index c484dac542..db1190ae7c 100644 --- a/Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs +++ b/Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs @@ -27,7 +27,7 @@ namespace Emby.Dlna.ConnectionManager /// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns> private static IEnumerable<StateVariable> GetStateVariables() { - var list = new List<StateVariable> + return new StateVariable[] { new StateVariable { @@ -114,8 +114,6 @@ namespace Emby.Dlna.ConnectionManager SendsEvents = false } }; - - return list; } } } diff --git a/Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs b/Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs index 3edaabb70e..9af28aa7cb 100644 --- a/Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs +++ b/Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs @@ -27,7 +27,7 @@ namespace Emby.Dlna.ContentDirectory /// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns> private static IEnumerable<StateVariable> GetStateVariables() { - var list = new List<StateVariable> + return new StateVariable[] { new StateVariable { @@ -154,8 +154,6 @@ namespace Emby.Dlna.ContentDirectory SendsEvents = false } }; - - return list; } } } diff --git a/Emby.Naming/AudioBook/AudioBookListResolver.cs b/Emby.Naming/AudioBook/AudioBookListResolver.cs index bdae20b6b2..ca304102fd 100644 --- a/Emby.Naming/AudioBook/AudioBookListResolver.cs +++ b/Emby.Naming/AudioBook/AudioBookListResolver.cs @@ -79,25 +79,25 @@ namespace Emby.Naming.AudioBook { if (group.Count() > 1 || haveChaptersOrPages) { - var ex = new List<AudioBookFileInfo>(); - var alt = new List<AudioBookFileInfo>(); + List<AudioBookFileInfo>? ex = null; + List<AudioBookFileInfo>? alt = null; foreach (var audioFile in group) { - var name = Path.GetFileNameWithoutExtension(audioFile.Path); - if (name.Equals("audiobook", StringComparison.OrdinalIgnoreCase) || - name.Contains(nameParserResult.Name, StringComparison.OrdinalIgnoreCase) || - name.Contains(nameWithReplacedDots, StringComparison.OrdinalIgnoreCase)) + var name = Path.GetFileNameWithoutExtension(audioFile.Path.AsSpan()); + if (name.Equals("audiobook", StringComparison.OrdinalIgnoreCase) + || name.Contains(nameParserResult.Name, StringComparison.OrdinalIgnoreCase) + || name.Contains(nameWithReplacedDots, StringComparison.OrdinalIgnoreCase)) { - alt.Add(audioFile); + (alt ??= new()).Add(audioFile); } else { - ex.Add(audioFile); + (ex ??= new()).Add(audioFile); } } - if (ex.Count > 0) + if (ex is not null) { var extra = ex .OrderBy(x => x.Container) @@ -108,7 +108,7 @@ namespace Emby.Naming.AudioBook extras.AddRange(extra); } - if (alt.Count > 0) + if (alt is not null) { var alternatives = alt .OrderBy(x => x.Container) diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs index 84ba194648..1e3c4dea14 100644 --- a/Emby.Server.Implementations/Channels/ChannelManager.cs +++ b/Emby.Server.Implementations/Channels/ChannelManager.cs @@ -401,7 +401,7 @@ namespace Emby.Server.Implementations.Channels } else { - results = new List<MediaSourceInfo>(); + results = Enumerable.Empty<MediaSourceInfo>(); } return results diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs index b53c8ca512..1796830552 100644 --- a/Emby.Server.Implementations/Collections/CollectionManager.cs +++ b/Emby.Server.Implementations/Collections/CollectionManager.cs @@ -206,8 +206,7 @@ namespace Emby.Server.Implementations.Collections throw new ArgumentException("No collection exists with the supplied Id"); } - var list = new List<LinkedChild>(); - var itemList = new List<BaseItem>(); + List<BaseItem>? itemList = null; var linkedChildrenList = collection.GetLinkedChildren(); var currentLinkedChildrenIds = linkedChildrenList.Select(i => i.Id).ToList(); @@ -223,18 +222,23 @@ namespace Emby.Server.Implementations.Collections if (!currentLinkedChildrenIds.Contains(id)) { - itemList.Add(item); + (itemList ??= new()).Add(item); - list.Add(LinkedChild.Create(item)); linkedChildrenList.Add(item); } } - if (list.Count > 0) + if (itemList is not null) { - LinkedChild[] newChildren = new LinkedChild[collection.LinkedChildren.Length + list.Count]; + var originalLen = collection.LinkedChildren.Length; + var newItemCount = itemList.Count; + LinkedChild[] newChildren = new LinkedChild[originalLen + newItemCount]; collection.LinkedChildren.CopyTo(newChildren, 0); - list.CopyTo(newChildren, collection.LinkedChildren.Length); + for (int i = 0; i < newItemCount; i++) + { + newChildren[originalLen + i] = LinkedChild.Create(itemList[i]); + } + collection.LinkedChildren = newChildren; collection.UpdateRatingToItems(linkedChildrenList); diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 0aa943270e..3bf4d07c59 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -586,7 +586,7 @@ namespace Emby.Server.Implementations.Data /// <exception cref="ArgumentNullException"> /// <paramref name="items"/> or <paramref name="cancellationToken"/> is <c>null</c>. /// </exception> - public void SaveItems(IEnumerable<BaseItem> items, CancellationToken cancellationToken) + public void SaveItems(IReadOnlyList<BaseItem> items, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(items); @@ -594,9 +594,11 @@ namespace Emby.Server.Implementations.Data CheckDisposed(); - var tuples = new List<(BaseItem, List<Guid>, BaseItem, string, List<string>)>(); - foreach (var item in items) + var itemsLen = items.Count; + var tuples = new ValueTuple<BaseItem, List<Guid>, BaseItem, string, List<string>>[itemsLen]; + for (int i = 0; i < itemsLen; i++) { + var item = items[i]; var ancestorIds = item.SupportsAncestors ? item.GetAncestorIds().Distinct().ToList() : null; @@ -606,7 +608,7 @@ namespace Emby.Server.Implementations.Data var userdataKey = item.GetUserDataKeys().FirstOrDefault(); var inheritedTags = item.GetInheritedTags(); - tuples.Add((item, ancestorIds, topParent, userdataKey, inheritedTags)); + tuples[i] = (item, ancestorIds, topParent, userdataKey, inheritedTags); } using (var connection = GetConnection()) diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index e928f1ff3a..45270de893 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -85,8 +85,8 @@ namespace Emby.Server.Implementations.Dto { var accessibleItems = user is null ? items : items.Where(x => x.IsVisible(user)).ToList(); var returnItems = new BaseItemDto[accessibleItems.Count]; - var programTuples = new List<(BaseItem, BaseItemDto)>(); - var channelTuples = new List<(BaseItemDto, LiveTvChannel)>(); + List<(BaseItem, BaseItemDto)> programTuples = null; + List<(BaseItemDto, LiveTvChannel)> channelTuples = null; for (int index = 0; index < accessibleItems.Count; index++) { @@ -95,11 +95,11 @@ namespace Emby.Server.Implementations.Dto if (item is LiveTvChannel tvChannel) { - channelTuples.Add((dto, tvChannel)); + (channelTuples ??= new()).Add((dto, tvChannel)); } else if (item is LiveTvProgram) { - programTuples.Add((item, dto)); + (programTuples ??= new()).Add((item, dto)); } if (item is IItemByName byName) @@ -122,12 +122,12 @@ namespace Emby.Server.Implementations.Dto returnItems[index] = dto; } - if (programTuples.Count > 0) + if (programTuples is not null) { LivetvManager.AddInfoToProgramDto(programTuples, options.Fields, user).GetAwaiter().GetResult(); } - if (channelTuples.Count > 0) + if (channelTuples is not null) { LivetvManager.AddChannelInfo(channelTuples, options, user); } diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index a3c66dc798..66bd68ddd0 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -356,8 +356,8 @@ namespace Emby.Server.Implementations.Library } var children = item.IsFolder - ? ((Folder)item).GetRecursiveChildren(false).ToList() - : new List<BaseItem>(); + ? ((Folder)item).GetRecursiveChildren(false) + : Enumerable.Empty<BaseItem>(); foreach (var metadataPath in GetMetadataPaths(item, children)) { @@ -1253,7 +1253,7 @@ namespace Emby.Server.Implementations.Library var parent = GetItemById(query.ParentId); if (parent is not null) { - SetTopParentIdsOrAncestors(query, new List<BaseItem> { parent }); + SetTopParentIdsOrAncestors(query, new[] { parent }); } } @@ -1277,7 +1277,7 @@ namespace Emby.Server.Implementations.Library var parent = GetItemById(query.ParentId); if (parent is not null) { - SetTopParentIdsOrAncestors(query, new List<BaseItem> { parent }); + SetTopParentIdsOrAncestors(query, new[] { parent }); } } @@ -1435,7 +1435,7 @@ namespace Emby.Server.Implementations.Library var parent = GetItemById(query.ParentId); if (parent is not null) { - SetTopParentIdsOrAncestors(query, new List<BaseItem> { parent }); + SetTopParentIdsOrAncestors(query, new[] { parent }); } } @@ -1455,7 +1455,7 @@ namespace Emby.Server.Implementations.Library _itemRepository.GetItemList(query)); } - private void SetTopParentIdsOrAncestors(InternalItemsQuery query, List<BaseItem> parents) + private void SetTopParentIdsOrAncestors(InternalItemsQuery query, IReadOnlyCollection<BaseItem> parents) { if (parents.All(i => i is ICollectionFolder || i is UserView)) { @@ -1602,7 +1602,7 @@ namespace Emby.Server.Implementations.Library { _logger.LogError(ex, "Error getting intros"); - return new List<IntroInfo>(); + return Enumerable.Empty<IntroInfo>(); } } @@ -2876,7 +2876,7 @@ namespace Emby.Server.Implementations.Library private async Task SavePeopleMetadataAsync(IEnumerable<PersonInfo> people, CancellationToken cancellationToken) { - var personsToSave = new List<BaseItem>(); + List<BaseItem> personsToSave = null; foreach (var person in people) { @@ -2918,12 +2918,12 @@ namespace Emby.Server.Implementations.Library if (saveEntity) { - personsToSave.Add(personEntity); + (personsToSave ??= new()).Add(personEntity); await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false); } } - if (personsToSave.Count > 0) + if (personsToSave is not null) { CreateItems(personsToSave, null, CancellationToken.None); } @@ -3085,22 +3085,19 @@ namespace Emby.Server.Implementations.Library throw new ArgumentNullException(nameof(path)); } - var removeList = new List<NameValuePair>(); + List<NameValuePair> removeList = null; foreach (var contentType in _configurationManager.Configuration.ContentTypes) { - if (string.IsNullOrWhiteSpace(contentType.Name)) - { - removeList.Add(contentType); - } - else if (_fileSystem.AreEqual(path, contentType.Name) + if (string.IsNullOrWhiteSpace(contentType.Name) + || _fileSystem.AreEqual(path, contentType.Name) || _fileSystem.ContainsSubPath(path, contentType.Name)) { - removeList.Add(contentType); + (removeList ??= new()).Add(contentType); } } - if (removeList.Count > 0) + if (removeList is not null) { _configurationManager.Configuration.ContentTypes = _configurationManager.Configuration.ContentTypes .Except(removeList) diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs index 06621700aa..69e9057984 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs @@ -158,7 +158,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio private MultiItemResolverResult ResolveMultipleAudio(Folder parent, IEnumerable<FileSystemMetadata> fileSystemEntries, bool parseName) { var files = new List<FileSystemMetadata>(); - var items = new List<BaseItem>(); var leftOver = new List<FileSystemMetadata>(); // Loop through each child file/folder and see if we find a video @@ -180,7 +179,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio var result = new MultiItemResolverResult { ExtraFiles = leftOver, - Items = items + Items = new List<BaseItem>() }; var isInMixedFolder = resolverResult.Count > 1 || (parent is not null && parent.IsTopParent); diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs index 1137625f44..0e2d34d39c 100644 --- a/Emby.Server.Implementations/Library/UserViewManager.cs +++ b/Emby.Server.Implementations/Library/UserViewManager.cs @@ -286,7 +286,7 @@ namespace Emby.Server.Implementations.Library if (parents.Count == 0) { - return new List<BaseItem>(); + return Array.Empty<BaseItem>(); } if (includeItemTypes.Length == 0) diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index 24f7b5cd36..2c52b2b45e 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -28,7 +28,7 @@ namespace MediaBrowser.Controller.Persistence /// </summary> /// <param name="items">The items.</param> /// <param name="cancellationToken">The cancellation token.</param> - void SaveItems(IEnumerable<BaseItem> items, CancellationToken cancellationToken); + void SaveItems(IReadOnlyList<BaseItem> items, CancellationToken cancellationToken); void SaveImages(BaseItem item); diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index c07839ff25..81ccd86534 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -284,12 +284,12 @@ namespace MediaBrowser.Providers.Manager } catch (OperationCanceledException) { - return new List<RemoteImageInfo>(); + return Enumerable.Empty<RemoteImageInfo>(); } catch (Exception ex) { _logger.LogError(ex, "{ProviderName} failed in GetImageInfos for type {ItemType} at {ItemPath}", provider.GetType().Name, item.GetType().Name, item.Path); - return new List<RemoteImageInfo>(); + return Enumerable.Empty<RemoteImageInfo>(); } } diff --git a/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs b/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs index db4c5f436e..9bd36f25c3 100644 --- a/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs +++ b/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs @@ -87,7 +87,7 @@ namespace MediaBrowser.Providers.Playlists return GetPlsItems(stream); } - return new List<LinkedChild>(); + return Enumerable.Empty<LinkedChild>(); } private IEnumerable<LinkedChild> GetPlsItems(Stream stream) diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs index b1a285a964..2232dfa0d7 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; +using System.Linq; using System.Net.Http; using System.Text.Json; using System.Threading; @@ -42,7 +43,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb /// <inheritdoc /> public IEnumerable<ImageType> GetSupportedImages(BaseItem item) { - return new List<ImageType> + return new ImageType[] { ImageType.Primary, ImageType.Logo, @@ -74,7 +75,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb } } - return new List<RemoteImageInfo>(); + return Enumerable.Empty<RemoteImageInfo>(); } private IEnumerable<RemoteImageInfo> GetImages(AudioDbArtistProvider.Artist item) diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs index 4aa4269891..d0bd7d6098 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs @@ -157,10 +157,10 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, Albu var artists = releaseSearchResult.ArtistCredit; if (artists is not null && artists.Count > 0) { - var artistResults = new List<RemoteSearchResult>(); - - foreach (var artist in artists) + var artistResults = new RemoteSearchResult[artists.Count]; + for (int i = 0; i < artists.Count; i++) { + var artist = artists[i]; var artistResult = new RemoteSearchResult { Name = artist.Name @@ -171,11 +171,11 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, Albu artistResult.SetProviderId(MetadataProvider.MusicBrainzArtist, artist.Artist!.Id.ToString()); } - artistResults.Add(artistResult); + artistResults[i] = artistResult; } searchResult.AlbumArtist = artistResults[0]; - searchResult.Artists = artistResults.ToArray(); + searchResult.Artists = artistResults; } searchResult.SetProviderId(MetadataProvider.MusicBrainzAlbum, releaseSearchResult.Id.ToString()); diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs index 60b373483f..140a64f52f 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs @@ -38,10 +38,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb public IEnumerable<ImageType> GetSupportedImages(BaseItem item) { - return new List<ImageType> - { - ImageType.Primary - }; + yield return ImageType.Primary; } public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs index ef32b0a074..a4c6cb47de 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs @@ -48,7 +48,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets /// <inheritdoc /> public IEnumerable<ImageType> GetSupportedImages(BaseItem item) { - return new List<ImageType> + return new ImageType[] { ImageType.Primary, ImageType.Backdrop diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs index e6c7dba250..c2018d820e 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs @@ -72,7 +72,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets var collectionSearchResults = await _tmdbClientManager.SearchCollectionAsync(searchInfo.Name, language, cancellationToken).ConfigureAwait(false); - var collections = new List<RemoteSearchResult>(); + var collections = new RemoteSearchResult[collectionSearchResults.Count]; for (var i = 0; i < collectionSearchResults.Count; i++) { var collection = new RemoteSearchResult @@ -82,7 +82,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets }; collection.SetProviderId(MetadataProvider.Tmdb, collectionSearchResults[i].Id.ToString(CultureInfo.InvariantCulture)); - collections.Add(collection); + collections[i] = collection; } return collections; diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs index 655fa5a16d..bfec48e7c7 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs @@ -49,7 +49,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies /// <inheritdoc /> public IEnumerable<ImageType> GetSupportedImages(BaseItem item) { - return new List<ImageType> + return new ImageType[] { ImageType.Primary, ImageType.Backdrop, diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs index bc959ee2bd..9e5404b325 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs @@ -46,10 +46,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People /// <inheritdoc /> public IEnumerable<ImageType> GetSupportedImages(BaseItem item) { - return new List<ImageType> - { - ImageType.Primary - }; + yield return ImageType.Primary; } /// <inheritdoc /> diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs index c03a1ca3b8..5c6e71fd89 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs @@ -67,7 +67,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People var personSearchResult = await _tmdbClientManager.SearchPersonAsync(searchInfo.Name, cancellationToken).ConfigureAwait(false); - var remoteSearchResults = new List<RemoteSearchResult>(); + var remoteSearchResults = new RemoteSearchResult[personSearchResult.Count]; for (var i = 0; i < personSearchResult.Count; i++) { var person = personSearchResult[i]; @@ -79,7 +79,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People }; remoteSearchResult.SetProviderId(MetadataProvider.Tmdb, person.Id.ToString(CultureInfo.InvariantCulture)); - remoteSearchResults.Add(remoteSearchResult); + remoteSearchResults[i] = remoteSearchResult; } return remoteSearchResults; diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs index abef732bbb..d1fec7cb13 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs @@ -47,10 +47,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV /// <inheritdoc /> public IEnumerable<ImageType> GetSupportedImages(BaseItem item) { - return new List<ImageType> - { - ImageType.Primary - }; + yield return ImageType.Primary; } /// <inheritdoc /> diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs index b8d1460db9..a743601ed3 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs @@ -48,10 +48,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV /// <inheritdoc /> public IEnumerable<ImageType> GetSupportedImages(BaseItem item) { - return new List<ImageType> - { - ImageType.Primary - }; + yield return ImageType.Primary; } /// <inheritdoc /> diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs index 79cb6e86d4..192fb052d7 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs @@ -48,7 +48,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV /// <inheritdoc /> public IEnumerable<ImageType> GetSupportedImages(BaseItem item) { - return new List<ImageType> + return new ImageType[] { ImageType.Primary, ImageType.Backdrop, From e58bf6b2be1c366f0fb705d60f1d242df7123386 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20F=C3=BCrni=C3=9F?= <nils.fuerniss@gmail.com> Date: Wed, 1 Mar 2023 00:46:08 +0100 Subject: [PATCH 123/858] Add SeasonProviderIds to EpisodeInfo (#9407) Co-authored-by: Cody Robibero <cody@robibe.ro> --- MediaBrowser.Controller/Entities/TV/Episode.cs | 5 +++++ MediaBrowser.Controller/Providers/EpisodeInfo.cs | 3 +++ 2 files changed, 8 insertions(+) diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs index c83149a6dc..597b4cecbc 100644 --- a/MediaBrowser.Controller/Entities/TV/Episode.cs +++ b/MediaBrowser.Controller/Entities/TV/Episode.cs @@ -308,6 +308,11 @@ namespace MediaBrowser.Controller.Entities.TV id.SeriesDisplayOrder = series.DisplayOrder; } + if (Season is not null) + { + id.SeasonProviderIds = Season.ProviderIds; + } + id.IsMissingEpisode = IsMissingEpisode; id.IndexNumberEnd = IndexNumberEnd; diff --git a/MediaBrowser.Controller/Providers/EpisodeInfo.cs b/MediaBrowser.Controller/Providers/EpisodeInfo.cs index b59a037384..c4ad352a3b 100644 --- a/MediaBrowser.Controller/Providers/EpisodeInfo.cs +++ b/MediaBrowser.Controller/Providers/EpisodeInfo.cs @@ -12,10 +12,13 @@ namespace MediaBrowser.Controller.Providers public EpisodeInfo() { SeriesProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + SeasonProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); } public Dictionary<string, string> SeriesProviderIds { get; set; } + public Dictionary<string, string> SeasonProviderIds { get; set; } + public int? IndexNumberEnd { get; set; } public bool IsMissingEpisode { get; set; } From d280dc65549b2507c058bcf1823e3ab040293da7 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Wed, 1 Mar 2023 16:43:55 +0100 Subject: [PATCH 124/858] Reduce log spam Fixes #7801 --- .../ScheduledTasks/ScheduledTaskWorker.cs | 9 +++------ .../ScheduledTasks/TaskManager.cs | 7 ++----- Jellyfin.Networking/Manager/NetworkManager.cs | 6 +++--- 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs index ee9aa85699..1af2c96d2f 100644 --- a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs +++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs @@ -93,11 +93,8 @@ namespace Emby.Server.Implementations.ScheduledTasks public ScheduledTaskWorker(IScheduledTask scheduledTask, IApplicationPaths applicationPaths, ITaskManager taskManager, ILogger logger) { ArgumentNullException.ThrowIfNull(scheduledTask); - ArgumentNullException.ThrowIfNull(applicationPaths); - ArgumentNullException.ThrowIfNull(taskManager); - ArgumentNullException.ThrowIfNull(logger); ScheduledTask = scheduledTask; @@ -332,7 +329,7 @@ namespace Emby.Server.Implementations.ScheduledTasks return; } - _logger.LogInformation("{0} fired for task: {1}", trigger.GetType().Name, Name); + _logger.LogDebug("{0} fired for task: {1}", trigger.GetType().Name, Name); trigger.Stop(); @@ -378,7 +375,7 @@ namespace Emby.Server.Implementations.ScheduledTasks CurrentCancellationTokenSource = new CancellationTokenSource(); - _logger.LogInformation("Executing {0}", Name); + _logger.LogDebug("Executing {0}", Name); ((TaskManager)_taskManager).OnTaskExecuting(this); @@ -406,7 +403,7 @@ namespace Emby.Server.Implementations.ScheduledTasks } catch (Exception ex) { - _logger.LogError(ex, "Error"); + _logger.LogError(ex, "Error executing Scheduled Task"); failureException = ex; diff --git a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs index 6dc20e66ba..42c30c959d 100644 --- a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs +++ b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs @@ -132,7 +132,7 @@ namespace Emby.Server.Implementations.ScheduledTasks { var type = scheduledTask.ScheduledTask.GetType(); - _logger.LogInformation("Queuing task {0}", type.Name); + _logger.LogDebug("Queuing task {0}", type.Name); lock (_taskQueue) { @@ -172,7 +172,7 @@ namespace Emby.Server.Implementations.ScheduledTasks { var type = task.ScheduledTask.GetType(); - _logger.LogInformation("Queuing task {0}", type.Name); + _logger.LogDebug("Queuing task {0}", type.Name); lock (_taskQueue) { @@ -254,9 +254,6 @@ namespace Emby.Server.Implementations.ScheduledTasks /// </summary> private void ExecuteQueuedTasks() { - _logger.LogInformation("ExecuteQueuedTasks"); - - // Execute queued tasks lock (_taskQueue) { var list = new List<Tuple<Type, TaskOptions>>(); diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 88332ce393..f406e27a6d 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -1019,8 +1019,8 @@ namespace Jellyfin.Networking.Manager _internalInterfaces = CreateCollection(_interfaceAddresses.Where(IsInLocalNetwork)); } - _logger.LogInformation("Defined LAN addresses : {0}", _lanSubnets.AsString()); - _logger.LogInformation("Defined LAN exclusions : {0}", _excludedSubnets.AsString()); + _logger.LogInformation("Defined LAN addresses: {0}", _lanSubnets.AsString()); + _logger.LogInformation("Defined LAN exclusions: {0}", _excludedSubnets.AsString()); _logger.LogInformation("Using LAN addresses: {0}", _lanSubnets.Exclude(_excludedSubnets, true).AsNetworks().AsString()); } } @@ -1145,7 +1145,7 @@ namespace Jellyfin.Networking.Manager } _logger.LogDebug("Discovered {0} interfaces.", _interfaceAddresses.Count); - _logger.LogDebug("Interfaces addresses : {0}", _interfaceAddresses.AsString()); + _logger.LogDebug("Interfaces addresses: {0}", _interfaceAddresses.AsString()); } } From 1f15724398c1e05a3c5b72b7d7bd062413529890 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Thu, 2 Mar 2023 20:57:59 +0100 Subject: [PATCH 125/858] Use source audio bitrate if requested codec is lossless --- .../Controllers/DynamicHlsController.cs | 18 ++++---- Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 13 +++++- Jellyfin.Api/Helpers/StreamingHelpers.cs | 16 ++++--- .../MediaEncoding/EncodingHelper.cs | 45 +++++++------------ 4 files changed, 48 insertions(+), 44 deletions(-) diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 40ca2fcf76..4232d4c8a0 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -12,6 +12,7 @@ using Jellyfin.Api.Attributes; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.PlaybackDtos; using Jellyfin.Api.Models.StreamingDtos; +using Jellyfin.Extensions; using Jellyfin.MediaEncoding.Hls.Playlist; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; @@ -1688,7 +1689,7 @@ public class DynamicHlsController : BaseJellyfinApiController var audioBitrate = state.OutputAudioBitrate; var audioChannels = state.OutputAudioChannels; - if (audioBitrate.HasValue) + if (audioBitrate.HasValue && !EncodingHelper.LosslessAudioCodecs.Contains(state.ActualOutputAudioCodec, StringComparison.OrdinalIgnoreCase)) { var vbrParam = _encodingHelper.GetAudioVbrModeParam(state.OutputAudioCodec, audioBitrate.Value / (audioChannels ?? 2)); if (_encodingOptions.EnableAudioVbr && vbrParam is not null) @@ -1717,11 +1718,11 @@ public class DynamicHlsController : BaseJellyfinApiController // dts, flac, opus and truehd are experimental in mp4 muxer var strictArgs = string.Empty; - - if (string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase) - || string.Equals(state.ActualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase) - || string.Equals(state.ActualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase) - || string.Equals(state.ActualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase)) + var actualOutputAudioCodec = state.ActualOutputAudioCodec; + if (string.Equals(actualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase) + || string.Equals(actualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase) + || string.Equals(actualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase) + || string.Equals(actualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase)) { strictArgs = " -strict -2"; } @@ -1755,10 +1756,9 @@ public class DynamicHlsController : BaseJellyfinApiController } var bitrate = state.OutputAudioBitrate; - - if (bitrate.HasValue) + if (bitrate.HasValue && !EncodingHelper.LosslessAudioCodecs.Contains(actualOutputAudioCodec, StringComparison.OrdinalIgnoreCase)) { - var vbrParam = _encodingHelper.GetAudioVbrModeParam(state.OutputAudioCodec, bitrate.Value / (channels ?? 2)); + var vbrParam = _encodingHelper.GetAudioVbrModeParam(actualOutputAudioCodec, bitrate.Value / (channels ?? 2)); if (_encodingOptions.EnableAudioVbr && vbrParam is not null) { args += vbrParam; diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index 555babc0e1..4486954c62 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Extensions; using Jellyfin.Api.Models.StreamingDtos; +using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; @@ -223,9 +224,17 @@ public class DynamicHlsHelper sdrVideoUrl += "&AllowVideoStreamCopy=false"; var sdrOutputVideoBitrate = _encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec); - var sdrOutputAudioBitrate = _encodingHelper.GetAudioBitrateParam(state.VideoRequest, state.AudioStream, state.OutputAudioChannels) ?? 0; - var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate; + var sdrOutputAudioBitrate = 0; + if (EncodingHelper.LosslessAudioCodecs.Contains(state.VideoRequest.AudioCodec, StringComparison.OrdinalIgnoreCase)) + { + sdrOutputAudioBitrate = state.AudioStream.BitRate ?? 0; + } + else + { + sdrOutputAudioBitrate = _encodingHelper.GetAudioBitrateParam(state.VideoRequest, state.AudioStream, state.OutputAudioChannels) ?? 0; + } + var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate; var sdrPlaylist = AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup); // Provide a workaround for the case issue between flac and fLaC. diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index 6ce98d2317..9c91dcc6fe 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -181,12 +181,18 @@ public static class StreamingHelpers : GetOutputFileExtension(state, mediaSource); } + var outputAudioCodec = streamingRequest.AudioCodec; + if (EncodingHelper.LosslessAudioCodecs.Contains(outputAudioCodec)) + { + state.OutputAudioBitrate = state.AudioStream.BitRate ?? 0; + } + else + { + state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, streamingRequest.AudioCodec, state.AudioStream, state.OutputAudioChannels) ?? 0; + } + + state.OutputAudioCodec = outputAudioCodec; state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.'); - - state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, streamingRequest.AudioCodec, state.AudioStream, state.OutputAudioChannels); - - state.OutputAudioCodec = streamingRequest.AudioCodec; - state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec); if (state.VideoRequest is not null) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 5f38ddbba3..b6118e600a 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -86,6 +86,16 @@ namespace MediaBrowser.Controller.MediaEncoding { "truehd", 6 }, }; + public static readonly string[] LosslessAudioCodecs = new string[] + { + "alac", + "ape", + "flac", + "mlp", + "truehd", + "wavpack" + }; + public EncodingHelper( IApplicationPaths appPaths, IMediaEncoder mediaEncoder, @@ -2149,17 +2159,6 @@ namespace MediaBrowser.Controller.MediaEncoding }; } - if (string.Equals(audioCodec, "flac", StringComparison.OrdinalIgnoreCase) - || string.Equals(audioCodec, "alac", StringComparison.OrdinalIgnoreCase)) - { - if (inputChannels >= 6) - { - return Math.Min(3584000, bitrate); - } - - return Math.Min(1536000, bitrate); - } - if (string.Equals(audioCodec, "dts", StringComparison.OrdinalIgnoreCase) || string.Equals(audioCodec, "dca", StringComparison.OrdinalIgnoreCase)) { @@ -2172,20 +2171,10 @@ namespace MediaBrowser.Controller.MediaEncoding }; } - if (string.Equals(audioCodec, "truehd", StringComparison.OrdinalIgnoreCase)) - { - return (inputChannels, outputChannels) switch - { - (> 0, > 0) => Math.Min(outputChannels * 768000, bitrate), - (> 0, _) => Math.Min(inputChannels * 768000, bitrate), - (_, _) => Math.Min(768000, bitrate) - }; - } - // Empty bitrate area is not allow on iOS // Default audio bitrate to 128K per channel if we don't have codec specific defaults // https://ffmpeg.org/ffmpeg-codecs.html#toc-Codec-Options - return 128000 * (outputAudioChannels ?? audioStream.Channels ?? 1); + return 128000 * (outputAudioChannels ?? audioStream.Channels ?? 2); } public string GetAudioVbrModeParam(string encoder, int bitratePerChannel) @@ -5867,8 +5856,7 @@ namespace MediaBrowser.Controller.MediaEncoding } var bitrate = state.OutputAudioBitrate; - - if (bitrate.HasValue) + if (bitrate.HasValue && !LosslessAudioCodecs.Contains(codec, StringComparison.OrdinalIgnoreCase)) { var vbrParam = GetAudioVbrModeParam(codec, bitrate.Value / (channels ?? 2)); if (encodingOptions.EnableAudioVbr && vbrParam is not null) @@ -5897,10 +5885,11 @@ namespace MediaBrowser.Controller.MediaEncoding var bitrate = state.OutputAudioBitrate; var channels = state.OutputAudioChannels; + var outputCodec = state.OutputAudioCodec; - if (bitrate.HasValue) + if (bitrate.HasValue && !LosslessAudioCodecs.Contains(outputCodec, StringComparison.OrdinalIgnoreCase)) { - var vbrParam = GetAudioVbrModeParam(state.OutputAudioCodec, bitrate.Value / (channels ?? 2)); + var vbrParam = GetAudioVbrModeParam(outputCodec, bitrate.Value / (channels ?? 2)); if (encodingOptions.EnableAudioVbr && vbrParam is not null) { audioTranscodeParams.Add(vbrParam); @@ -5916,12 +5905,12 @@ namespace MediaBrowser.Controller.MediaEncoding audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture)); } - if (!string.IsNullOrEmpty(state.OutputAudioCodec)) + if (!string.IsNullOrEmpty(outputCodec)) { audioTranscodeParams.Add("-acodec " + GetAudioEncoder(state)); } - if (!string.Equals(state.OutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase)) + if (!string.Equals(outputCodec, "opus", StringComparison.OrdinalIgnoreCase)) { // opus only supports specific sampling rates var sampleRate = state.OutputAudioSampleRate; From 0905d622241ad8826cb21be51a5a16ad6a47079d Mon Sep 17 00:00:00 2001 From: nyanmisaka <nst799610810@gmail.com> Date: Sun, 5 Mar 2023 00:22:43 +0800 Subject: [PATCH 126/858] Adapt vulkan filtering to 6.0 --- .../MediaEncoding/EncodingHelper.cs | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index e02a932b12..f2fb3705c9 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -639,6 +639,26 @@ namespace MediaBrowser.Controller.MediaEncoding deviceIndex); } + private string GetVulkanDeviceArgs(int deviceIndex, string deviceName, string srcDeviceAlias, string alias) + { + alias ??= VulkanAlias; + deviceIndex = deviceIndex >= 0 + ? deviceIndex + : 0; + var vendorOpts = string.IsNullOrEmpty(deviceName) + ? ":" + deviceIndex + : ":" + "\"" + deviceName + "\""; + var options = string.IsNullOrEmpty(srcDeviceAlias) + ? vendorOpts + : "@" + srcDeviceAlias; + + return string.Format( + CultureInfo.InvariantCulture, + " -init_hw_device vulkan={0}{1}", + alias, + options); + } + private string GetOpenclDeviceArgs(int deviceIndex, string deviceVendorName, string srcDeviceAlias, string alias) { alias ??= OpenclAlias; @@ -821,6 +841,12 @@ namespace MediaBrowser.Controller.MediaEncoding args.Append(GetOpenclDeviceArgs(0, "Advanced Micro Devices", null, OpenclAlias)); filterDevArgs = GetFilterHwDeviceArgs(OpenclAlias); } + else + { + // libplacebo wants an explicitly set vulkan filter device. + args.Append(GetVulkanDeviceArgs(0, null, VaapiAlias, VulkanAlias)); + filterDevArgs = GetFilterHwDeviceArgs(VulkanAlias); + } } else { @@ -4126,7 +4152,9 @@ namespace MediaBrowser.Controller.MediaEncoding // sw => hw if (doVkTonemap) { - mainFilters.Add("hwupload=derive_device=vulkan:extra_hw_frames=16"); + mainFilters.Add("hwupload_vaapi"); + mainFilters.Add("hwmap=derive_device=vulkan"); + mainFilters.Add("format=vulkan"); } } else if (isVaapiDecoder) @@ -4156,6 +4184,7 @@ namespace MediaBrowser.Controller.MediaEncoding { // map from vaapi to vulkan via vaapi-vulkan interop (Vega/gfx9+). mainFilters.Add("hwmap=derive_device=vulkan"); + mainFilters.Add("format=vulkan"); } // vk tonemap @@ -4234,7 +4263,9 @@ namespace MediaBrowser.Controller.MediaEncoding // prefer vaapi hwupload to vulkan hwupload, // Mesa RADV does not support a dedicated transfer queue. - subFilters.Add("hwupload=derive_device=vaapi,format=vaapi,hwmap=derive_device=vulkan"); + subFilters.Add("hwupload_vaapi"); + subFilters.Add("hwmap=derive_device=vulkan"); + subFilters.Add("format=vulkan"); overlayFilters.Add("overlay_vulkan=eof_action=endall:shortest=1:repeatlast=0"); overlayFilters.Add("scale_vulkan=format=nv12"); From 160baa02fd9a2e7b4e2b3dcc28769274d0588413 Mon Sep 17 00:00:00 2001 From: Joe Rogers <1337joe@gmail.com> Date: Mon, 6 Mar 2023 22:18:26 -0500 Subject: [PATCH 127/858] Remove some BaseItem references to make ItemResolveArgs more usable for testing. --- .../Library/LibraryManager.cs | 2 +- .../Entities/AggregateFolder.cs | 2 +- .../Entities/CollectionFolder.cs | 2 +- .../Library/ItemResolveArgs.cs | 17 +++++++---------- .../Library/EpisodeResolverTest.cs | 6 ++++-- .../Library/MovieResolverTests.cs | 3 ++- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 66bd68ddd0..9fa4d3599f 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -537,7 +537,7 @@ namespace Emby.Server.Implementations.Library collectionType = GetContentTypeOverride(fullPath, true); } - var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, directoryService) + var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, directoryService, this) { Parent = parent, FileInfo = fileInfo, diff --git a/MediaBrowser.Controller/Entities/AggregateFolder.cs b/MediaBrowser.Controller/Entities/AggregateFolder.cs index 08c622cde0..aacb38607e 100644 --- a/MediaBrowser.Controller/Entities/AggregateFolder.cs +++ b/MediaBrowser.Controller/Entities/AggregateFolder.cs @@ -120,7 +120,7 @@ namespace MediaBrowser.Controller.Entities var path = ContainingFolderPath; - var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService) + var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService, LibraryManager) { FileInfo = FileSystem.GetDirectoryInfo(path) }; diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs index 5ac619d8f5..ff17797179 100644 --- a/MediaBrowser.Controller/Entities/CollectionFolder.cs +++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs @@ -288,7 +288,7 @@ namespace MediaBrowser.Controller.Entities { var path = ContainingFolderPath; - var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService) + var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService, LibraryManager) { FileInfo = FileSystem.GetDirectoryInfo(path), Parent = GetParent() as Folder, diff --git a/MediaBrowser.Controller/Library/ItemResolveArgs.cs b/MediaBrowser.Controller/Library/ItemResolveArgs.cs index 01986d3032..730d89356d 100644 --- a/MediaBrowser.Controller/Library/ItemResolveArgs.cs +++ b/MediaBrowser.Controller/Library/ItemResolveArgs.cs @@ -23,6 +23,7 @@ namespace MediaBrowser.Controller.Library /// </summary> private readonly IServerApplicationPaths _appPaths; + private readonly ILibraryManager _libraryManager; private LibraryOptions _libraryOptions; /// <summary> @@ -30,10 +31,12 @@ namespace MediaBrowser.Controller.Library /// </summary> /// <param name="appPaths">The app paths.</param> /// <param name="directoryService">The directory service.</param> - public ItemResolveArgs(IServerApplicationPaths appPaths, IDirectoryService directoryService) + /// <param name="libraryManager">The library manager.</param> + public ItemResolveArgs(IServerApplicationPaths appPaths, IDirectoryService directoryService, ILibraryManager libraryManager) { _appPaths = appPaths; DirectoryService = directoryService; + _libraryManager = libraryManager; } // TODO remove dependencies as properties, they should be injected where it makes sense @@ -47,7 +50,7 @@ namespace MediaBrowser.Controller.Library public LibraryOptions LibraryOptions { - get => _libraryOptions ??= Parent is null ? new LibraryOptions() : BaseItem.LibraryManager.GetLibraryOptions(Parent); + get => _libraryOptions ??= Parent is null ? new LibraryOptions() : _libraryManager.GetLibraryOptions(Parent); set => _libraryOptions = value; } @@ -231,21 +234,15 @@ namespace MediaBrowser.Controller.Library /// <summary> /// Gets the configured content type for the path. /// </summary> - /// <remarks> - /// This is subject to future refactoring as it relies on a static property in BaseItem. - /// </remarks> /// <returns>The configured content type.</returns> public string GetConfiguredContentType() { - return BaseItem.LibraryManager.GetConfiguredContentType(Path); + return _libraryManager.GetConfiguredContentType(Path); } /// <summary> /// Gets the file system children that do not hit the ignore file check. /// </summary> - /// <remarks> - /// This is subject to future refactoring as it relies on a static property in BaseItem. - /// </remarks> /// <returns>The file system children that are not ignored.</returns> public IEnumerable<FileSystemMetadata> GetActualFileSystemChildren() { @@ -253,7 +250,7 @@ namespace MediaBrowser.Controller.Library for (var i = 0; i < numberOfChildren; i++) { var child = FileSystemChildren[i]; - if (BaseItem.LibraryManager.IgnoreFile(child, Parent)) + if (_libraryManager.IgnoreFile(child, Parent)) { continue; } diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs index 286ba04059..65f19a051a 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs @@ -25,7 +25,8 @@ namespace Jellyfin.Server.Implementations.Tests.Library var episodeResolver = new EpisodeResolver(Mock.Of<ILogger<EpisodeResolver>>(), _namingOptions); var itemResolveArgs = new ItemResolveArgs( Mock.Of<IServerApplicationPaths>(), - Mock.Of<IDirectoryService>()) + Mock.Of<IDirectoryService>(), + null) { Parent = parent, CollectionType = CollectionType.TvShows, @@ -48,7 +49,8 @@ namespace Jellyfin.Server.Implementations.Tests.Library var episodeResolver = new EpisodeResolverMock(Mock.Of<ILogger<EpisodeResolver>>(), _namingOptions); var itemResolveArgs = new ItemResolveArgs( Mock.Of<IServerApplicationPaths>(), - Mock.Of<IDirectoryService>()) + Mock.Of<IDirectoryService>(), + null) { Parent = series, CollectionType = CollectionType.TvShows, diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs index efc3ac0c2a..f96bd6138e 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs @@ -21,7 +21,8 @@ public class MovieResolverTests var movieResolver = new MovieResolver(Mock.Of<IImageProcessor>(), Mock.Of<ILogger<MovieResolver>>(), _namingOptions); var itemResolveArgs = new ItemResolveArgs( Mock.Of<IServerApplicationPaths>(), - Mock.Of<IDirectoryService>()) + Mock.Of<IDirectoryService>(), + null) { Parent = null, FileInfo = new FileSystemMetadata From 1c3a97bf6adadf4e3b22177e1e965691637d0426 Mon Sep 17 00:00:00 2001 From: Joe Rogers <1337joe@gmail.com> Date: Mon, 6 Mar 2023 23:00:55 -0500 Subject: [PATCH 128/858] Inject IDirectoryService where needed instead of passing it through ItemResolveArgs --- .../Library/LibraryManager.cs | 8 +++++--- .../Resolvers/Audio/MusicAlbumResolver.cs | 7 +++++-- .../Resolvers/Audio/MusicArtistResolver.cs | 15 +++++++++------ .../Library/Resolvers/BaseVideoResolver.cs | 7 +++++-- .../Library/Resolvers/ExtraResolver.cs | 8 +++++--- .../Library/Resolvers/GenericVideoResolver.cs | 6 ++++-- .../Library/Resolvers/Movies/MovieResolver.cs | 13 +++++++------ .../Library/Resolvers/PhotoResolver.cs | 18 ++++++++++++++---- .../Library/Resolvers/TV/EpisodeResolver.cs | 6 ++++-- .../Entities/AggregateFolder.cs | 2 +- .../Entities/CollectionFolder.cs | 2 +- .../Library/ItemResolveArgs.cs | 10 ++-------- .../Library/EpisodeResolverTest.cs | 8 +++----- .../Library/MovieResolverTests.cs | 3 +-- 14 files changed, 66 insertions(+), 47 deletions(-) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 9fa4d3599f..e5c520ca2b 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -113,6 +113,7 @@ namespace Emby.Server.Implementations.Library /// <param name="imageProcessor">The image processor.</param> /// <param name="memoryCache">The memory cache.</param> /// <param name="namingOptions">The naming options.</param> + /// <param name="directoryService">The directory service.</param> public LibraryManager( IServerApplicationHost appHost, ILoggerFactory loggerFactory, @@ -128,7 +129,8 @@ namespace Emby.Server.Implementations.Library IItemRepository itemRepository, IImageProcessor imageProcessor, IMemoryCache memoryCache, - NamingOptions namingOptions) + NamingOptions namingOptions, + IDirectoryService directoryService) { _appHost = appHost; _logger = loggerFactory.CreateLogger<LibraryManager>(); @@ -146,7 +148,7 @@ namespace Emby.Server.Implementations.Library _memoryCache = memoryCache; _namingOptions = namingOptions; - _extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions); + _extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService); _configurationManager.ConfigurationUpdated += ConfigurationUpdated; @@ -537,7 +539,7 @@ namespace Emby.Server.Implementations.Library collectionType = GetContentTypeOverride(fullPath, true); } - var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, directoryService, this) + var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, this) { Parent = parent, FileInfo = fileInfo, diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs index a922e36855..bbc70701cb 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs @@ -25,16 +25,19 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio { private readonly ILogger<MusicAlbumResolver> _logger; private readonly NamingOptions _namingOptions; + private readonly IDirectoryService _directoryService; /// <summary> /// Initializes a new instance of the <see cref="MusicAlbumResolver"/> class. /// </summary> /// <param name="logger">The logger.</param> /// <param name="namingOptions">The naming options.</param> - public MusicAlbumResolver(ILogger<MusicAlbumResolver> logger, NamingOptions namingOptions) + /// <param name="directoryService">The directory service.</param> + public MusicAlbumResolver(ILogger<MusicAlbumResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService) { _logger = logger; _namingOptions = namingOptions; + _directoryService = directoryService; } /// <summary> @@ -109,7 +112,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio } // If args contains music it's a music album - if (ContainsMusic(args.FileSystemChildren, true, args.DirectoryService)) + if (ContainsMusic(args.FileSystemChildren, true, _directoryService)) { return true; } diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs index 2538c2b5b4..c858dc53d9 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Emby.Naming.Common; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Model.Entities; using Microsoft.Extensions.Logging; @@ -18,19 +19,23 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio public class MusicArtistResolver : ItemResolver<MusicArtist> { private readonly ILogger<MusicAlbumResolver> _logger; - private NamingOptions _namingOptions; + private readonly NamingOptions _namingOptions; + private readonly IDirectoryService _directoryService; /// <summary> /// Initializes a new instance of the <see cref="MusicArtistResolver"/> class. /// </summary> /// <param name="logger">Instance of the <see cref="MusicAlbumResolver"/> interface.</param> /// <param name="namingOptions">The <see cref="NamingOptions"/>.</param> + /// <param name="directoryService">The directory service.</param> public MusicArtistResolver( ILogger<MusicAlbumResolver> logger, - NamingOptions namingOptions) + NamingOptions namingOptions, + IDirectoryService directoryService) { _logger = logger; _namingOptions = namingOptions; + _directoryService = directoryService; } /// <summary> @@ -78,9 +83,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio return null; } - var directoryService = args.DirectoryService; - - var albumResolver = new MusicAlbumResolver(_logger, _namingOptions); + var albumResolver = new MusicAlbumResolver(_logger, _namingOptions, _directoryService); var directories = args.FileSystemChildren.Where(i => i.IsDirectory); @@ -97,7 +100,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio } // If we contain a music album assume we are an artist folder - if (albumResolver.IsMusicAlbum(fileSystemInfo.FullName, directoryService)) + if (albumResolver.IsMusicAlbum(fileSystemInfo.FullName, _directoryService)) { // Stop once we see a music album state.Stop(); diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs index e8615e7db7..9b133bef48 100644 --- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs @@ -25,14 +25,17 @@ namespace Emby.Server.Implementations.Library.Resolvers { private readonly ILogger _logger; - protected BaseVideoResolver(ILogger logger, NamingOptions namingOptions) + protected BaseVideoResolver(ILogger logger, NamingOptions namingOptions, IDirectoryService directoryService) { _logger = logger; NamingOptions = namingOptions; + DirectoryService = directoryService; } protected NamingOptions NamingOptions { get; } + protected IDirectoryService DirectoryService { get; } + /// <summary> /// Resolves the specified args. /// </summary> @@ -65,7 +68,7 @@ namespace Emby.Server.Implementations.Library.Resolvers var filename = child.Name; if (child.IsDirectory) { - if (IsDvdDirectory(child.FullName, filename, args.DirectoryService)) + if (IsDvdDirectory(child.FullName, filename, DirectoryService)) { videoType = VideoType.Dvd; } diff --git a/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs b/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs index 30c52e19d3..0b255f673b 100644 --- a/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs @@ -4,6 +4,7 @@ using System.IO; using Emby.Naming.Common; using Emby.Naming.Video; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Model.Entities; using Microsoft.Extensions.Logging; @@ -25,11 +26,12 @@ namespace Emby.Server.Implementations.Library.Resolvers /// </summary> /// <param name="logger">The logger.</param> /// <param name="namingOptions">An instance of <see cref="NamingOptions"/>.</param> - public ExtraResolver(ILogger<ExtraResolver> logger, NamingOptions namingOptions) + /// <param name="directoryService">The directory service.</param> + public ExtraResolver(ILogger<ExtraResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService) { _namingOptions = namingOptions; - _trailerResolvers = new IItemResolver[] { new GenericVideoResolver<Trailer>(logger, namingOptions) }; - _videoResolvers = new IItemResolver[] { new GenericVideoResolver<Video>(logger, namingOptions) }; + _trailerResolvers = new IItemResolver[] { new GenericVideoResolver<Trailer>(logger, namingOptions, directoryService) }; + _videoResolvers = new IItemResolver[] { new GenericVideoResolver<Video>(logger, namingOptions, directoryService) }; } /// <summary> diff --git a/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs index 5e33b402d5..ba320266a4 100644 --- a/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs @@ -2,6 +2,7 @@ using Emby.Naming.Common; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Library.Resolvers @@ -18,8 +19,9 @@ namespace Emby.Server.Implementations.Library.Resolvers /// </summary> /// <param name="logger">The logger.</param> /// <param name="namingOptions">The naming options.</param> - public GenericVideoResolver(ILogger logger, NamingOptions namingOptions) - : base(logger, namingOptions) + /// <param name="directoryService">The directory service.</param> + public GenericVideoResolver(ILogger logger, NamingOptions namingOptions, IDirectoryService directoryService) + : base(logger, namingOptions, directoryService) { } } diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index ef4fa1fd2d..ea980b9929 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -43,8 +43,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies /// <param name="imageProcessor">The image processor.</param> /// <param name="logger">The logger.</param> /// <param name="namingOptions">The naming options.</param> - public MovieResolver(IImageProcessor imageProcessor, ILogger<MovieResolver> logger, NamingOptions namingOptions) - : base(logger, namingOptions) + /// <param name="directoryService">The directory service.</param> + public MovieResolver(IImageProcessor imageProcessor, ILogger<MovieResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService) + : base(logger, namingOptions, directoryService) { _imageProcessor = imageProcessor; } @@ -97,12 +98,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase)) { - movie = FindMovie<MusicVideo>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false); + movie = FindMovie<MusicVideo>(args, args.Path, args.Parent, files, DirectoryService, collectionType, false); } if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase)) { - movie = FindMovie<Video>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false); + movie = FindMovie<Video>(args, args.Path, args.Parent, files, DirectoryService, collectionType, false); } if (string.IsNullOrEmpty(collectionType)) @@ -118,12 +119,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies return null; } - movie = FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true); + movie = FindMovie<Movie>(args, args.Path, args.Parent, files, DirectoryService, collectionType, true); } if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase)) { - movie = FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true); + movie = FindMovie<Movie>(args, args.Path, args.Parent, files, DirectoryService, collectionType, true); } // ignore extras diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs index e11fb262eb..9026160ff0 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs @@ -1,7 +1,5 @@ #nullable disable -#pragma warning disable CS1591 - using System; using System.Collections.Generic; using System.IO; @@ -12,15 +10,20 @@ using Jellyfin.Extensions; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Model.Entities; namespace Emby.Server.Implementations.Library.Resolvers { + /// <summary> + /// Class PhotoResolver. + /// </summary> public class PhotoResolver : ItemResolver<Photo> { private readonly IImageProcessor _imageProcessor; private readonly NamingOptions _namingOptions; + private readonly IDirectoryService _directoryService; private static readonly HashSet<string> _ignoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { @@ -35,10 +38,17 @@ namespace Emby.Server.Implementations.Library.Resolvers "default" }; - public PhotoResolver(IImageProcessor imageProcessor, NamingOptions namingOptions) + /// <summary> + /// Initializes a new instance of the <see cref="PhotoResolver"/> class. + /// </summary> + /// <param name="imageProcessor">The image processor.</param> + /// <param name="namingOptions">The naming options.</param> + /// <param name="directoryService">The directory service.</param> + public PhotoResolver(IImageProcessor imageProcessor, NamingOptions namingOptions, IDirectoryService directoryService) { _imageProcessor = imageProcessor; _namingOptions = namingOptions; + _directoryService = directoryService; } /// <summary> @@ -61,7 +71,7 @@ namespace Emby.Server.Implementations.Library.Resolvers var filename = Path.GetFileNameWithoutExtension(args.Path); // Make sure the image doesn't belong to a video file - var files = args.DirectoryService.GetFiles(Path.GetDirectoryName(args.Path)); + var files = _directoryService.GetFiles(Path.GetDirectoryName(args.Path)); foreach (var file in files) { diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs index 0fcc5070b3..392ee4c771 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs @@ -5,6 +5,7 @@ using System.Linq; using Emby.Naming.Common; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using Microsoft.Extensions.Logging; @@ -20,8 +21,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV /// </summary> /// <param name="logger">The logger.</param> /// <param name="namingOptions">The naming options.</param> - public EpisodeResolver(ILogger<EpisodeResolver> logger, NamingOptions namingOptions) - : base(logger, namingOptions) + /// <param name="directoryService">The directory service.</param> + public EpisodeResolver(ILogger<EpisodeResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService) + : base(logger, namingOptions, directoryService) { } diff --git a/MediaBrowser.Controller/Entities/AggregateFolder.cs b/MediaBrowser.Controller/Entities/AggregateFolder.cs index aacb38607e..d789033f16 100644 --- a/MediaBrowser.Controller/Entities/AggregateFolder.cs +++ b/MediaBrowser.Controller/Entities/AggregateFolder.cs @@ -120,7 +120,7 @@ namespace MediaBrowser.Controller.Entities var path = ContainingFolderPath; - var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService, LibraryManager) + var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, LibraryManager) { FileInfo = FileSystem.GetDirectoryInfo(path) }; diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs index ff17797179..095b261c05 100644 --- a/MediaBrowser.Controller/Entities/CollectionFolder.cs +++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs @@ -288,7 +288,7 @@ namespace MediaBrowser.Controller.Entities { var path = ContainingFolderPath; - var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService, LibraryManager) + var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, LibraryManager) { FileInfo = FileSystem.GetDirectoryInfo(path), Parent = GetParent() as Folder, diff --git a/MediaBrowser.Controller/Library/ItemResolveArgs.cs b/MediaBrowser.Controller/Library/ItemResolveArgs.cs index 730d89356d..c701021679 100644 --- a/MediaBrowser.Controller/Library/ItemResolveArgs.cs +++ b/MediaBrowser.Controller/Library/ItemResolveArgs.cs @@ -1,12 +1,11 @@ #nullable disable -#pragma warning disable CA1721, CA1819, CS1591 +#pragma warning disable CS1591 using System; using System.Collections.Generic; using System.Linq; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.IO; @@ -30,18 +29,13 @@ namespace MediaBrowser.Controller.Library /// Initializes a new instance of the <see cref="ItemResolveArgs" /> class. /// </summary> /// <param name="appPaths">The app paths.</param> - /// <param name="directoryService">The directory service.</param> /// <param name="libraryManager">The library manager.</param> - public ItemResolveArgs(IServerApplicationPaths appPaths, IDirectoryService directoryService, ILibraryManager libraryManager) + public ItemResolveArgs(IServerApplicationPaths appPaths, ILibraryManager libraryManager) { _appPaths = appPaths; - DirectoryService = directoryService; _libraryManager = libraryManager; } - // TODO remove dependencies as properties, they should be injected where it makes sense - public IDirectoryService DirectoryService { get; } - /// <summary> /// Gets or sets the file system children. /// </summary> diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs index 65f19a051a..6d0ed7bbba 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs @@ -22,10 +22,9 @@ namespace Jellyfin.Server.Implementations.Tests.Library { var parent = new Folder { Name = "extras" }; - var episodeResolver = new EpisodeResolver(Mock.Of<ILogger<EpisodeResolver>>(), _namingOptions); + var episodeResolver = new EpisodeResolver(Mock.Of<ILogger<EpisodeResolver>>(), _namingOptions, Mock.Of<IDirectoryService>()); var itemResolveArgs = new ItemResolveArgs( Mock.Of<IServerApplicationPaths>(), - Mock.Of<IDirectoryService>(), null) { Parent = parent, @@ -46,10 +45,9 @@ namespace Jellyfin.Server.Implementations.Tests.Library // Have to create a mock because of moq proxies not being castable to a concrete implementation // https://github.com/jellyfin/jellyfin/blob/ab0cff8556403e123642dc9717ba778329554634/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs#L48 - var episodeResolver = new EpisodeResolverMock(Mock.Of<ILogger<EpisodeResolver>>(), _namingOptions); + var episodeResolver = new EpisodeResolverMock(Mock.Of<ILogger<EpisodeResolver>>(), _namingOptions, Mock.Of<IDirectoryService>()); var itemResolveArgs = new ItemResolveArgs( Mock.Of<IServerApplicationPaths>(), - Mock.Of<IDirectoryService>(), null) { Parent = series, @@ -64,7 +62,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library private sealed class EpisodeResolverMock : EpisodeResolver { - public EpisodeResolverMock(ILogger<EpisodeResolver> logger, NamingOptions namingOptions) : base(logger, namingOptions) + public EpisodeResolverMock(ILogger<EpisodeResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService) : base(logger, namingOptions, directoryService) { } diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs index f96bd6138e..aed584355c 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs @@ -18,10 +18,9 @@ public class MovieResolverTests [Fact] public void Resolve_GivenLocalAlternateVersion_ResolvesToVideo() { - var movieResolver = new MovieResolver(Mock.Of<IImageProcessor>(), Mock.Of<ILogger<MovieResolver>>(), _namingOptions); + var movieResolver = new MovieResolver(Mock.Of<IImageProcessor>(), Mock.Of<ILogger<MovieResolver>>(), _namingOptions, Mock.Of<IDirectoryService>()); var itemResolveArgs = new ItemResolveArgs( Mock.Of<IServerApplicationPaths>(), - Mock.Of<IDirectoryService>(), null) { Parent = null, From 18b8efa7e0c532ce51a0d72a6d8690f8f4a3f302 Mon Sep 17 00:00:00 2001 From: Joe Rogers <1337joe@gmail.com> Date: Mon, 6 Mar 2023 23:22:37 -0500 Subject: [PATCH 129/858] Add tests for audio book resolving --- .../Library/AudioResolverTests.cs | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 tests/Jellyfin.Server.Implementations.Tests/Library/AudioResolverTests.cs diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/AudioResolverTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/AudioResolverTests.cs new file mode 100644 index 0000000000..ca5829f10a --- /dev/null +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/AudioResolverTests.cs @@ -0,0 +1,73 @@ +using System.Linq; +using Emby.Naming.Common; +using Emby.Server.Implementations.Library.Resolvers.Audio; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.IO; +using Moq; +using Xunit; + +namespace Jellyfin.Server.Implementations.Tests.Library; + +public class AudioResolverTests +{ + private static readonly NamingOptions _namingOptions = new(); + + [Theory] + [InlineData("words.mp3")] // single non-tagged file + [InlineData("chapter 01.mp3")] + [InlineData("part 1.mp3")] + [InlineData("chapter 01.mp3", "non-media.txt")] + [InlineData("title.mp3", "title.epub")] + [InlineData("01.mp3", "subdirectory/")] // single media file with sub-directory - note that this will hide any contents in the subdirectory + public void Resolve_AudiobookDirectory_SingleResult(params string[] children) + { + var resolved = TestResolveChildren("/parent/title", children); + Assert.NotNull(resolved); + } + + [Theory] + /* Results that can't be displayed as an audio book. */ + [InlineData] // no contents + [InlineData("subdirectory/")] + [InlineData("non-media.txt")] + /* Names don't indicate parts of a single book. */ + [InlineData("Name.mp3", "Another Name.mp3")] + /* Results that are an audio book but not currently navigable as such (multiple chapters and/or parts). */ + [InlineData("01.mp3", "02.mp3")] + [InlineData("chapter 01.mp3", "chapter 02.mp3")] + [InlineData("part 1.mp3", "part 2.mp3")] + [InlineData("chapter 01 part 01.mp3", "chapter 01 part 02.mp3")] + /* Mismatched chapters, parts, and named files. */ + [InlineData("chapter 01.mp3", "part 2.mp3")] + public void Resolve_AudiobookDirectory_NoResult(params string[] children) + { + var resolved = TestResolveChildren("/parent/book title", children); + Assert.Null(resolved); + } + + private Audio? TestResolveChildren(string parent, string[] children) + { + var childrenMetadata = children.Select(name => new FileSystemMetadata + { + FullName = parent + "/" + name, + IsDirectory = name.EndsWith('/') + }).ToArray(); + + var resolver = new AudioResolver(_namingOptions); + var itemResolveArgs = new ItemResolveArgs( + null, + Mock.Of<ILibraryManager>()) + { + CollectionType = "books", + FileInfo = new FileSystemMetadata + { + FullName = parent, + IsDirectory = true + }, + FileSystemChildren = childrenMetadata + }; + + return resolver.Resolve(itemResolveArgs); + } +} From 361fff3a0c1b2e5c14e991d53f5736e909b889b6 Mon Sep 17 00:00:00 2001 From: Joe Rogers <1337joe@gmail.com> Date: Mon, 6 Mar 2023 23:27:21 -0500 Subject: [PATCH 130/858] Fix cases where multiple files are resolved as a single book --- .../Library/Resolvers/Audio/AudioResolver.cs | 3 ++- .../Library/AudioResolverTests.cs | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs index 69e9057984..a74f824752 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs @@ -192,7 +192,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio continue; } - if (resolvedItem.Files.Count == 0) + // Until multi-part books are handled letting files stack hides them from browsing in the client + if (resolvedItem.Files.Count == 0 || resolvedItem.Extras.Count > 0 || resolvedItem.AlternateVersions.Count > 0) { continue; } diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/AudioResolverTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/AudioResolverTests.cs index ca5829f10a..d136c1bc68 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/AudioResolverTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/AudioResolverTests.cs @@ -40,6 +40,9 @@ public class AudioResolverTests [InlineData("chapter 01 part 01.mp3", "chapter 01 part 02.mp3")] /* Mismatched chapters, parts, and named files. */ [InlineData("chapter 01.mp3", "part 2.mp3")] + [InlineData("book title.mp3", "chapter name.mp3")] // "book title" resolves as alternate version of book based on directory name + [InlineData("01 Content.mp3", "01 Credits.mp3")] // resolves as alternate versions of chapter 1 + [InlineData("Chapter Name.mp3", "Part 1.mp3")] public void Resolve_AudiobookDirectory_NoResult(params string[] children) { var resolved = TestResolveChildren("/parent/book title", children); From 891e2495c94fdfb3464cd0bb42c9950d27e28e38 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Tue, 7 Mar 2023 17:58:04 +0100 Subject: [PATCH 131/858] Disable real time monitoring by default --- Emby.Server.Implementations/Library/LibraryManager.cs | 4 +++- MediaBrowser.Model/Configuration/LibraryOptions.cs | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 66bd68ddd0..c089bdce1e 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -2054,7 +2054,9 @@ namespace Emby.Server.Implementations.Library .Find(folder => folder is CollectionFolder) as CollectionFolder; } - return collectionFolder is null ? new LibraryOptions() : collectionFolder.GetLibraryOptions(); + return collectionFolder is null + ? new LibraryOptions() + : collectionFolder.GetLibraryOptions(); } public string GetContentType(BaseItem item) diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs index 81f2f02bc5..885e86d4bf 100644 --- a/MediaBrowser.Model/Configuration/LibraryOptions.cs +++ b/MediaBrowser.Model/Configuration/LibraryOptions.cs @@ -20,7 +20,6 @@ namespace MediaBrowser.Model.Configuration AutomaticallyAddToCollection = false; EnablePhotos = true; SaveSubtitlesWithMedia = true; - EnableRealtimeMonitor = true; PathInfos = Array.Empty<MediaPathInfo>(); EnableAutomaticSeriesGrouping = true; SeasonZeroDisplayName = "Specials"; From 7a937319920a19aa5341adf3608086d58d90442b Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Tue, 7 Mar 2023 18:44:59 +0100 Subject: [PATCH 132/858] Ignore avg critic rating Rotten Tomatoes --- .../Parsers/BaseNfoParser.cs | 107 +++++++----------- 1 file changed, 40 insertions(+), 67 deletions(-) diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs index 159b8d6580..8bd30447a2 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs @@ -274,16 +274,13 @@ namespace MediaBrowser.XbmcMetadata.Parsers { var val = reader.ReadElementContentAsString(); - if (!string.IsNullOrWhiteSpace(val)) + if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var added)) { - if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var added)) - { - item.DateCreated = added; - } - else - { - Logger.LogWarning("Invalid Added value found: {Value}", val); - } + item.DateCreated = added; + } + else + { + Logger.LogWarning("Invalid Added value found: {Value}", val); } break; @@ -376,15 +373,13 @@ namespace MediaBrowser.XbmcMetadata.Parsers case "playcount": { var val = reader.ReadElementContentAsString(); - if (!string.IsNullOrWhiteSpace(val) && !string.IsNullOrWhiteSpace(nfoConfiguration.UserId)) + if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var count) + && Guid.TryParse(nfoConfiguration.UserId, out var guid)) { - if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var count)) - { - var user = _userManager.GetUserById(Guid.Parse(nfoConfiguration.UserId)); - userData = _userDataManager.GetUserData(user, item); - userData.PlayCount = count; - _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None); - } + var user = _userManager.GetUserById(guid); + userData = _userDataManager.GetUserData(user, item); + userData.PlayCount = count; + _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None); } break; @@ -393,11 +388,11 @@ namespace MediaBrowser.XbmcMetadata.Parsers case "lastplayed": { var val = reader.ReadElementContentAsString(); - if (!string.IsNullOrWhiteSpace(val) && !string.IsNullOrWhiteSpace(nfoConfiguration.UserId)) + if (Guid.TryParse(nfoConfiguration.UserId, out var guid)) { if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var added)) { - var user = _userManager.GetUserById(Guid.Parse(nfoConfiguration.UserId)); + var user = _userManager.GetUserById(guid); userData = _userDataManager.GetUserData(user, item); userData.LastPlayedDate = added; _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None); @@ -487,12 +482,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers { var text = reader.ReadElementContentAsString(); - if (!string.IsNullOrWhiteSpace(text)) + if (int.TryParse(text.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime)) { - if (int.TryParse(text.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime)) - { - item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks; - } + item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks; } break; @@ -630,13 +622,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers { var val = reader.ReadElementContentAsString(); - var hasDisplayOrder = item as IHasDisplayOrder; - if (hasDisplayOrder is not null) + if (item is IHasDisplayOrder hasDisplayOrder && !string.IsNullOrWhiteSpace(val)) { - if (!string.IsNullOrWhiteSpace(val)) - { - hasDisplayOrder.DisplayOrder = val; - } + hasDisplayOrder.DisplayOrder = val; } break; @@ -646,12 +634,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers { var val = reader.ReadElementContentAsString(); - if (!string.IsNullOrWhiteSpace(val)) + if (int.TryParse(val, out var productionYear) && productionYear > 1850) { - if (int.TryParse(val, out var productionYear) && productionYear > 1850) - { - item.ProductionYear = productionYear; - } + item.ProductionYear = productionYear; } break; @@ -661,13 +646,10 @@ namespace MediaBrowser.XbmcMetadata.Parsers { var rating = reader.ReadElementContentAsString(); - if (!string.IsNullOrWhiteSpace(rating)) + // All external meta is saving this as '.' for decimal I believe...but just to be sure + if (float.TryParse(rating.Replace(',', '.'), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var val)) { - // All external meta is saving this as '.' for decimal I believe...but just to be sure - if (float.TryParse(rating.Replace(',', '.'), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var val)) - { - item.CommunityRating = val; - } + item.CommunityRating = val; } break; @@ -697,13 +679,10 @@ namespace MediaBrowser.XbmcMetadata.Parsers var val = reader.ReadElementContentAsString(); - if (!string.IsNullOrWhiteSpace(val)) + if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var date) && date.Year > 1850) { - if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var date) && date.Year > 1850) - { - item.PremiereDate = date; - item.ProductionYear = date.Year; - } + item.PremiereDate = date; + item.ProductionYear = date.Year; } break; @@ -715,12 +694,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers var val = reader.ReadElementContentAsString(); - if (!string.IsNullOrWhiteSpace(val)) + if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var date) && date.Year > 1850) { - if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var date) && date.Year > 1850) - { - item.EndDate = date; - } + item.EndDate = date; } break; @@ -1191,21 +1167,21 @@ namespace MediaBrowser.XbmcMetadata.Parsers case "value": var val = reader.ReadElementContentAsString(); - if (!string.IsNullOrWhiteSpace(val)) + if (float.TryParse(val, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var ratingValue)) { - if (float.TryParse(val, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var ratingValue)) + // if ratingName contains tomato --> assume critic rating + if (ratingName is not null + && ratingName.Contains("tomato", StringComparison.OrdinalIgnoreCase) + && !ratingName.Contains("audience", StringComparison.OrdinalIgnoreCase)) { - // if ratingName contains tomato --> assume critic rating - if (ratingName is not null && - ratingName.Contains("tomato", StringComparison.OrdinalIgnoreCase) && - !ratingName.Contains("audience", StringComparison.OrdinalIgnoreCase)) + if (!ratingName.Contains("avg", StringComparison.OrdinalIgnoreCase)) { item.CriticRating = ratingValue; } - else - { - item.CommunityRating = ratingValue; - } + } + else + { + item.CommunityRating = ratingValue; } } @@ -1289,12 +1265,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers { var val = reader.ReadElementContentAsString(); - if (!string.IsNullOrWhiteSpace(val)) + if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intVal)) { - if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intVal)) - { - sortOrder = intVal; - } + sortOrder = intVal; } break; From dab75d35d2648ff8717aa6dc08eed1db640aea40 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Tue, 7 Mar 2023 21:51:48 +0100 Subject: [PATCH 133/858] Enable nullable for more files --- Emby.Dlna/PlayTo/PlayToController.cs | 90 +++++-------- Emby.Dlna/PlayTo/PlayToManager.cs | 5 +- MediaBrowser.Model/Dlna/ContainerProfile.cs | 2 +- MediaBrowser.Model/Dlna/DirectPlayProfile.cs | 6 +- MediaBrowser.Model/Dlna/ITranscoderSupport.cs | 18 --- MediaBrowser.Model/Dlna/MediaOptions.cs | 10 +- MediaBrowser.Model/Dlna/StreamBuilder.cs | 123 +++++++++--------- 7 files changed, 109 insertions(+), 145 deletions(-) diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs index 7b1f942c5a..86db363374 100644 --- a/Emby.Dlna/PlayTo/PlayToController.cs +++ b/Emby.Dlna/PlayTo/PlayToController.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -66,7 +64,8 @@ namespace Emby.Dlna.PlayTo IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, - IMediaEncoder mediaEncoder) + IMediaEncoder mediaEncoder, + Device device) { _session = session; _sessionManager = sessionManager; @@ -82,14 +81,7 @@ namespace Emby.Dlna.PlayTo _localization = localization; _mediaSourceManager = mediaSourceManager; _mediaEncoder = mediaEncoder; - } - public bool IsSessionActive => !_disposed && _device is not null; - - public bool SupportsMediaControl => IsSessionActive; - - public void Init(Device device) - { _device = device; _device.OnDeviceUnavailable = OnDeviceUnavailable; _device.PlaybackStart += OnDevicePlaybackStart; @@ -102,6 +94,10 @@ namespace Emby.Dlna.PlayTo _deviceDiscovery.DeviceLeft += OnDeviceDiscoveryDeviceLeft; } + public bool IsSessionActive => !_disposed; + + public bool SupportsMediaControl => IsSessionActive; + /* * Send a message to the DLNA device to notify what is the next track in the playlist. */ @@ -131,22 +127,22 @@ namespace Emby.Dlna.PlayTo } } - private void OnDeviceDiscoveryDeviceLeft(object sender, GenericEventArgs<UpnpDeviceInfo> e) + private void OnDeviceDiscoveryDeviceLeft(object? sender, GenericEventArgs<UpnpDeviceInfo> e) { var info = e.Argument; if (!_disposed - && info.Headers.TryGetValue("USN", out string usn) + && info.Headers.TryGetValue("USN", out string? usn) && usn.IndexOf(_device.Properties.UUID, StringComparison.OrdinalIgnoreCase) != -1 && (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1 - || (info.Headers.TryGetValue("NT", out string nt) + || (info.Headers.TryGetValue("NT", out string? nt) && nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1))) { OnDeviceUnavailable(); } } - private async void OnDeviceMediaChanged(object sender, MediaChangedEventArgs e) + private async void OnDeviceMediaChanged(object? sender, MediaChangedEventArgs e) { if (_disposed || string.IsNullOrEmpty(e.OldMediaInfo.Url)) { @@ -188,7 +184,7 @@ namespace Emby.Dlna.PlayTo } } - private async void OnDevicePlaybackStopped(object sender, PlaybackStoppedEventArgs e) + private async void OnDevicePlaybackStopped(object? sender, PlaybackStoppedEventArgs e) { if (_disposed) { @@ -257,7 +253,7 @@ namespace Emby.Dlna.PlayTo } } - private async void OnDevicePlaybackStart(object sender, PlaybackStartEventArgs e) + private async void OnDevicePlaybackStart(object? sender, PlaybackStartEventArgs e) { if (_disposed) { @@ -281,7 +277,7 @@ namespace Emby.Dlna.PlayTo } } - private async void OnDevicePlaybackProgress(object sender, PlaybackProgressEventArgs e) + private async void OnDevicePlaybackProgress(object? sender, PlaybackProgressEventArgs e) { if (_disposed) { @@ -486,9 +482,9 @@ namespace Emby.Dlna.PlayTo private PlaylistItem CreatePlaylistItem( BaseItem item, - User user, + User? user, long startPostionTicks, - string mediaSourceId, + string? mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex) { @@ -525,7 +521,7 @@ namespace Emby.Dlna.PlayTo return playlistItem; } - private string GetDlnaHeaders(PlaylistItem item) + private string? GetDlnaHeaders(PlaylistItem item) { var profile = item.Profile; var streamInfo = item.StreamInfo; @@ -579,7 +575,7 @@ namespace Emby.Dlna.PlayTo return null; } - private PlaylistItem GetPlaylistItem(BaseItem item, 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)) { @@ -696,7 +692,6 @@ namespace Emby.Dlna.PlayTo _device.MediaChanged -= OnDeviceMediaChanged; _deviceDiscovery.DeviceLeft -= OnDeviceDiscoveryDeviceLeft; _device.OnDeviceUnavailable = null; - _device = null; _disposed = true; } @@ -716,7 +711,7 @@ namespace Emby.Dlna.PlayTo case GeneralCommandType.ToggleMute: return _device.ToggleMute(cancellationToken); case GeneralCommandType.SetAudioStreamIndex: - if (command.Arguments.TryGetValue("Index", out string index)) + if (command.Arguments.TryGetValue("Index", out string? index)) { if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val)) { @@ -740,7 +735,7 @@ namespace Emby.Dlna.PlayTo throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null"); case GeneralCommandType.SetVolume: - if (command.Arguments.TryGetValue("Volume", out string vol)) + if (command.Arguments.TryGetValue("Volume", out string? vol)) { if (int.TryParse(vol, NumberStyles.Integer, CultureInfo.InvariantCulture, out var volume)) { @@ -865,34 +860,19 @@ namespace Emby.Dlna.PlayTo throw new ObjectDisposedException(GetType().Name); } - if (_device is null) + return name switch { - return Task.CompletedTask; - } - - if (name == SessionMessageType.Play) - { - return SendPlayCommand(data as PlayRequest, cancellationToken); - } - - if (name == SessionMessageType.Playstate) - { - return SendPlaystateCommand(data as PlaystateRequest, cancellationToken); - } - - if (name == SessionMessageType.GeneralCommand) - { - return SendGeneralCommand(data as GeneralCommand, cancellationToken); - } - - // Not supported or needed right now - return Task.CompletedTask; + SessionMessageType.Play => SendPlayCommand((data as PlayRequest)!, cancellationToken), + SessionMessageType.Playstate => SendPlaystateCommand((data as PlaystateRequest)!, cancellationToken), + SessionMessageType.GeneralCommand => SendGeneralCommand((data as GeneralCommand)!, cancellationToken), + _ => Task.CompletedTask // Not supported or needed right now + }; } private class StreamParams { - private MediaSourceInfo _mediaSource; - private IMediaSourceManager _mediaSourceManager; + private MediaSourceInfo? _mediaSource; + private IMediaSourceManager? _mediaSourceManager; public Guid ItemId { get; set; } @@ -904,17 +884,17 @@ namespace Emby.Dlna.PlayTo public int? SubtitleStreamIndex { get; set; } - public string DeviceProfileId { get; set; } + public string? DeviceProfileId { get; set; } - public string DeviceId { get; set; } + public string? DeviceId { get; set; } - public string MediaSourceId { get; set; } + public string? MediaSourceId { get; set; } - public string LiveStreamId { get; set; } + public string? LiveStreamId { get; set; } - public BaseItem Item { get; set; } + public BaseItem? Item { get; set; } - public async Task<MediaSourceInfo> GetMediaSource(CancellationToken cancellationToken) + public async Task<MediaSourceInfo?> GetMediaSource(CancellationToken cancellationToken) { if (_mediaSource is not null) { @@ -944,8 +924,8 @@ namespace Emby.Dlna.PlayTo { var part = parts[i]; - if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase) || - string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase) + || string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase)) { if (Guid.TryParse(parts[i + 1], out var result)) { diff --git a/Emby.Dlna/PlayTo/PlayToManager.cs b/Emby.Dlna/PlayTo/PlayToManager.cs index f4a9a90af4..b469c9cb06 100644 --- a/Emby.Dlna/PlayTo/PlayToManager.cs +++ b/Emby.Dlna/PlayTo/PlayToManager.cs @@ -205,12 +205,11 @@ namespace Emby.Dlna.PlayTo _userDataManager, _localization, _mediaSourceManager, - _mediaEncoder); + _mediaEncoder, + device); sessionInfo.AddController(controller); - controller.Init(device); - var profile = _dlnaManager.GetProfile(device.Properties.ToDeviceIdentification()) ?? _dlnaManager.GetDefaultProfile(); diff --git a/MediaBrowser.Model/Dlna/ContainerProfile.cs b/MediaBrowser.Model/Dlna/ContainerProfile.cs index 927df8e4ef..9780042684 100644 --- a/MediaBrowser.Model/Dlna/ContainerProfile.cs +++ b/MediaBrowser.Model/Dlna/ContainerProfile.cs @@ -11,7 +11,7 @@ namespace MediaBrowser.Model.Dlna [XmlAttribute("type")] public DlnaProfileType Type { get; set; } - public ProfileCondition[]? Conditions { get; set; } = Array.Empty<ProfileCondition>(); + public ProfileCondition[] Conditions { get; set; } = Array.Empty<ProfileCondition>(); [XmlAttribute("container")] public string Container { get; set; } = string.Empty; diff --git a/MediaBrowser.Model/Dlna/DirectPlayProfile.cs b/MediaBrowser.Model/Dlna/DirectPlayProfile.cs index 03c3a72657..f68235d869 100644 --- a/MediaBrowser.Model/Dlna/DirectPlayProfile.cs +++ b/MediaBrowser.Model/Dlna/DirectPlayProfile.cs @@ -18,17 +18,17 @@ namespace MediaBrowser.Model.Dlna [XmlAttribute("type")] public DlnaProfileType Type { get; set; } - public bool SupportsContainer(string container) + public bool SupportsContainer(string? container) { return ContainerProfile.ContainsContainer(Container, container); } - public bool SupportsVideoCodec(string codec) + public bool SupportsVideoCodec(string? codec) { return Type == DlnaProfileType.Video && ContainerProfile.ContainsContainer(VideoCodec, codec); } - public bool SupportsAudioCodec(string codec) + public bool SupportsAudioCodec(string? codec) { return (Type == DlnaProfileType.Audio || Type == DlnaProfileType.Video) && ContainerProfile.ContainsContainer(AudioCodec, codec); } diff --git a/MediaBrowser.Model/Dlna/ITranscoderSupport.cs b/MediaBrowser.Model/Dlna/ITranscoderSupport.cs index a70ce44cc6..d7397399dd 100644 --- a/MediaBrowser.Model/Dlna/ITranscoderSupport.cs +++ b/MediaBrowser.Model/Dlna/ITranscoderSupport.cs @@ -10,22 +10,4 @@ namespace MediaBrowser.Model.Dlna bool CanExtractSubtitles(string codec); } - - public class FullTranscoderSupport : ITranscoderSupport - { - public bool CanEncodeToAudioCodec(string codec) - { - return true; - } - - public bool CanEncodeToSubtitleCodec(string codec) - { - return true; - } - - public bool CanExtractSubtitles(string codec) - { - return true; - } - } } diff --git a/MediaBrowser.Model/Dlna/MediaOptions.cs b/MediaBrowser.Model/Dlna/MediaOptions.cs index 29aecf97fc..7ec0dd473b 100644 --- a/MediaBrowser.Model/Dlna/MediaOptions.cs +++ b/MediaBrowser.Model/Dlna/MediaOptions.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using MediaBrowser.Model.Dto; @@ -59,22 +57,22 @@ namespace MediaBrowser.Model.Dlna /// <summary> /// Gets or sets the media sources. /// </summary> - public MediaSourceInfo[] MediaSources { get; set; } + public MediaSourceInfo[] MediaSources { get; set; } = Array.Empty<MediaSourceInfo>(); /// <summary> /// Gets or sets the device profile. /// </summary> - public DeviceProfile Profile { get; set; } + required public DeviceProfile Profile { get; set; } /// <summary> /// Gets or sets a media source id. Optional. Only needed if a specific AudioStreamIndex or SubtitleStreamIndex are requested. /// </summary> - public string MediaSourceId { get; set; } + public string? MediaSourceId { get; set; } /// <summary> /// Gets or sets the device id. /// </summary> - public string DeviceId { get; set; } + public string? DeviceId { get; set; } /// <summary> /// Gets or sets an override of supported number of audio channels diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index ab81bfb34c..af71e0d3f8 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Collections.Generic; using System.Globalization; @@ -37,29 +35,20 @@ namespace MediaBrowser.Model.Dlna _logger = logger; } - /// <summary> - /// Initializes a new instance of the <see cref="StreamBuilder"/> class. - /// </summary> - /// <param name="logger">The <see cref="ILogger"/> object.</param> - public StreamBuilder(ILogger<StreamBuilder> logger) - : this(new FullTranscoderSupport(), logger) - { - } - /// <summary> /// Gets the optimal audio stream. /// </summary> /// <param name="options">The <see cref="MediaOptions"/> object to get the audio stream from.</param> /// <returns>The <see cref="StreamInfo"/> of the optimal audio stream.</returns> - public StreamInfo GetOptimalAudioStream(MediaOptions options) + public StreamInfo? GetOptimalAudioStream(MediaOptions options) { ValidateMediaOptions(options, false); var mediaSources = new List<MediaSourceInfo>(); foreach (var mediaSource in options.MediaSources) { - if (string.IsNullOrEmpty(options.MediaSourceId) || - string.Equals(mediaSource.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase)) + if (string.IsNullOrEmpty(options.MediaSourceId) + || string.Equals(mediaSource.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase)) { mediaSources.Add(mediaSource); } @@ -68,7 +57,7 @@ namespace MediaBrowser.Model.Dlna var streams = new List<StreamInfo>(); foreach (var mediaSourceInfo in mediaSources) { - StreamInfo streamInfo = GetOptimalAudioStream(mediaSourceInfo, options); + StreamInfo? streamInfo = GetOptimalAudioStream(mediaSourceInfo, options); if (streamInfo is not null) { streams.Add(streamInfo); @@ -84,7 +73,7 @@ namespace MediaBrowser.Model.Dlna return GetOptimalStream(streams, options.GetMaxBitrate(true) ?? 0); } - private StreamInfo GetOptimalAudioStream(MediaSourceInfo item, MediaOptions options) + private StreamInfo? GetOptimalAudioStream(MediaSourceInfo item, MediaOptions options) { var playlistItem = new StreamInfo { @@ -138,7 +127,7 @@ namespace MediaBrowser.Model.Dlna } } - TranscodingProfile transcodingProfile = null; + TranscodingProfile? transcodingProfile = null; foreach (var tcProfile in options.Profile.TranscodingProfiles) { if (tcProfile.Type == playlistItem.MediaType @@ -190,15 +179,15 @@ namespace MediaBrowser.Model.Dlna /// </summary> /// <param name="options">The <see cref="MediaOptions"/> object to get the video stream from.</param> /// <returns>The <see cref="StreamInfo"/> of the optimal video stream.</returns> - public StreamInfo GetOptimalVideoStream(MediaOptions options) + public StreamInfo? GetOptimalVideoStream(MediaOptions options) { ValidateMediaOptions(options, true); var mediaSources = new List<MediaSourceInfo>(); foreach (var mediaSourceInfo in options.MediaSources) { - if (string.IsNullOrEmpty(options.MediaSourceId) || - string.Equals(mediaSourceInfo.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase)) + if (string.IsNullOrEmpty(options.MediaSourceId) + || string.Equals(mediaSourceInfo.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase)) { mediaSources.Add(mediaSourceInfo); } @@ -223,7 +212,7 @@ namespace MediaBrowser.Model.Dlna return GetOptimalStream(streams, options.GetMaxBitrate(false) ?? 0); } - private static StreamInfo GetOptimalStream(List<StreamInfo> streams, long maxBitrate) + private static StreamInfo? GetOptimalStream(List<StreamInfo> streams, long maxBitrate) => SortMediaSources(streams, maxBitrate).FirstOrDefault(); private static IOrderedEnumerable<StreamInfo> SortMediaSources(List<StreamInfo> streams, long maxBitrate) @@ -366,7 +355,7 @@ namespace MediaBrowser.Model.Dlna /// <param name="type">The <see cref="DlnaProfileType"/>.</param> /// <param name="playProfile">The <see cref="DirectPlayProfile"/> object to get the video stream from.</param> /// <returns>The the normalized input container.</returns> - public static string NormalizeMediaSourceFormatIntoSingleContainer(string inputContainer, DeviceProfile profile, DlnaProfileType type, DirectPlayProfile playProfile = null) + public static string? NormalizeMediaSourceFormatIntoSingleContainer(string inputContainer, DeviceProfile? profile, DlnaProfileType type, DirectPlayProfile? playProfile = null) { if (string.IsNullOrEmpty(inputContainer)) { @@ -394,7 +383,7 @@ namespace MediaBrowser.Model.Dlna return formats[0]; } - private (DirectPlayProfile Profile, PlayMethod? PlayMethod, TranscodeReason TranscodeReasons) GetAudioDirectPlayProfile(MediaSourceInfo item, MediaStream audioStream, MediaOptions options) + private (DirectPlayProfile? Profile, PlayMethod? PlayMethod, TranscodeReason TranscodeReasons) GetAudioDirectPlayProfile(MediaSourceInfo item, MediaStream audioStream, MediaOptions options) { var directPlayProfile = options.Profile.DirectPlayProfiles .FirstOrDefault(x => x.Type == DlnaProfileType.Audio && IsAudioDirectPlaySupported(x, item, audioStream)); @@ -449,7 +438,7 @@ namespace MediaBrowser.Model.Dlna return (directPlayProfile, null, transcodeReasons); } - private static TranscodeReason GetTranscodeReasonsFromDirectPlayProfile(MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream, IEnumerable<DirectPlayProfile> directPlayProfiles) + private static TranscodeReason GetTranscodeReasonsFromDirectPlayProfile(MediaSourceInfo item, MediaStream? videoStream, MediaStream audioStream, IEnumerable<DirectPlayProfile> directPlayProfiles) { var mediaType = videoStream is null ? DlnaProfileType.Audio : DlnaProfileType.Video; @@ -575,7 +564,7 @@ namespace MediaBrowser.Model.Dlna } } - private static void SetStreamInfoOptionsFromDirectPlayProfile(MediaOptions options, MediaSourceInfo item, StreamInfo playlistItem, DirectPlayProfile directPlayProfile) + private static void SetStreamInfoOptionsFromDirectPlayProfile(MediaOptions options, MediaSourceInfo item, StreamInfo playlistItem, DirectPlayProfile? directPlayProfile) { var container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video, directPlayProfile); var protocol = "http"; @@ -587,7 +576,7 @@ namespace MediaBrowser.Model.Dlna playlistItem.SubProtocol = protocol; playlistItem.VideoCodecs = new[] { item.VideoStream.Codec }; - playlistItem.AudioCodecs = ContainerProfile.SplitValue(directPlayProfile.AudioCodec); + playlistItem.AudioCodecs = ContainerProfile.SplitValue(directPlayProfile?.AudioCodec); } private StreamInfo BuildVideoItem(MediaSourceInfo item, MediaOptions options) @@ -646,7 +635,7 @@ namespace MediaBrowser.Model.Dlna isEligibleForDirectPlay, isEligibleForDirectStream); - DirectPlayProfile directPlayProfile = null; + DirectPlayProfile? directPlayProfile = null; if (isEligibleForDirectPlay || isEligibleForDirectStream) { // See if it can be direct played @@ -677,16 +666,16 @@ namespace MediaBrowser.Model.Dlna playlistItem.AudioStreamIndex = audioStream?.Index; if (audioStream is not null) { - playlistItem.AudioCodecs = ContainerProfile.SplitValue(directPlayProfile.AudioCodec); + playlistItem.AudioCodecs = ContainerProfile.SplitValue(directPlayProfile?.AudioCodec); } SetStreamInfoOptionsFromDirectPlayProfile(options, item, playlistItem, directPlayProfile); - BuildStreamVideoItem(playlistItem, options, item, videoStream, audioStream, candidateAudioStreams, directPlayProfile.Container, directPlayProfile.VideoCodec, directPlayProfile.AudioCodec); + BuildStreamVideoItem(playlistItem, options, item, videoStream, audioStream, candidateAudioStreams, directPlayProfile?.Container, directPlayProfile?.VideoCodec, directPlayProfile?.AudioCodec); } if (subtitleStream is not null) { - var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, directPlay.Value, _transcoderSupport, directPlayProfile.Container, null); + var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, directPlay.Value, _transcoderSupport, directPlayProfile?.Container, null); playlistItem.SubtitleDeliveryMethod = subtitleProfile.Method; playlistItem.SubtitleFormat = subtitleProfile.Format; @@ -748,7 +737,14 @@ namespace MediaBrowser.Model.Dlna return playlistItem; } - private TranscodingProfile GetVideoTranscodeProfile(MediaSourceInfo item, MediaOptions options, MediaStream videoStream, MediaStream audioStream, IEnumerable<MediaStream> candidateAudioStreams, MediaStream subtitleStream, StreamInfo playlistItem) + private TranscodingProfile? GetVideoTranscodeProfile( + MediaSourceInfo item, + MediaOptions options, + MediaStream? videoStream, + MediaStream? audioStream, + IEnumerable<MediaStream> candidateAudioStreams, + MediaStream? subtitleStream, + StreamInfo playlistItem) { if (!(item.SupportsTranscoding || item.SupportsDirectStream)) { @@ -795,7 +791,16 @@ namespace MediaBrowser.Model.Dlna return transcodingProfiles.FirstOrDefault(); } - private void BuildStreamVideoItem(StreamInfo playlistItem, MediaOptions options, MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream, IEnumerable<MediaStream> candidateAudioStreams, string container, string videoCodec, string audioCodec) + private void BuildStreamVideoItem( + StreamInfo playlistItem, + MediaOptions options, + MediaSourceInfo item, + MediaStream? videoStream, + MediaStream? audioStream, + IEnumerable<MediaStream> candidateAudioStreams, + string? container, + string? videoCodec, + string? audioCodec) { // Prefer matching video codecs var videoCodecs = ContainerProfile.SplitValue(videoCodec); @@ -862,12 +867,12 @@ namespace MediaBrowser.Model.Dlna int? bitDepth = videoStream?.BitDepth; int? videoBitrate = videoStream?.BitRate; double? videoLevel = videoStream?.Level; - string videoProfile = videoStream?.Profile; - string videoRangeType = videoStream?.VideoRangeType; + string? videoProfile = videoStream?.Profile; + string? videoRangeType = videoStream?.VideoRangeType; float videoFramerate = videoStream is null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0; bool? isAnamorphic = videoStream?.IsAnamorphic; bool? isInterlaced = videoStream?.IsInterlaced; - string videoCodecTag = videoStream?.CodecTag; + string? videoCodecTag = videoStream?.CodecTag; bool? isAvc = videoStream?.IsAVC; TransportStreamTimestamp? timestamp = videoStream is null ? TransportStreamTimestamp.None : item.Timestamp; @@ -903,11 +908,11 @@ namespace MediaBrowser.Model.Dlna playlistItem.AudioBitrate = Math.Min(playlistItem.AudioBitrate ?? audioBitrate, audioBitrate); bool? isSecondaryAudio = audioStream is null ? null : item.IsSecondaryAudio(audioStream); - int? inputAudioBitrate = audioStream is null ? null : audioStream.BitRate; - int? audioChannels = audioStream is null ? null : audioStream.Channels; - string audioProfile = audioStream is null ? null : audioStream.Profile; - int? inputAudioSampleRate = audioStream is null ? null : audioStream.SampleRate; - int? inputAudioBitDepth = audioStream is null ? null : audioStream.BitDepth; + int? inputAudioBitrate = audioStream?.BitRate; + int? audioChannels = audioStream?.Channels; + string? audioProfile = audioStream?.Profile; + int? inputAudioSampleRate = audioStream?.SampleRate; + int? inputAudioBitDepth = audioStream?.BitDepth; var appliedAudioConditions = options.Profile.CodecProfiles .Where(i => i.Type == CodecType.VideoAudio && @@ -955,7 +960,7 @@ namespace MediaBrowser.Model.Dlna playlistItem?.TranscodeReasons); } - private static int GetDefaultAudioBitrate(string audioCodec, int? audioChannels) + private static int GetDefaultAudioBitrate(string? audioCodec, int? audioChannels) { if (!string.IsNullOrEmpty(audioCodec)) { @@ -988,9 +993,9 @@ namespace MediaBrowser.Model.Dlna return 192000; } - private static int GetAudioBitrate(long maxTotalBitrate, string[] targetAudioCodecs, MediaStream audioStream, StreamInfo item) + private static int GetAudioBitrate(long maxTotalBitrate, string[] targetAudioCodecs, MediaStream? audioStream, StreamInfo item) { - string targetAudioCodec = targetAudioCodecs.Length == 0 ? null : targetAudioCodecs[0]; + string? targetAudioCodec = targetAudioCodecs.Length == 0 ? null : targetAudioCodecs[0]; int? targetAudioChannels = item.GetTargetAudioChannels(targetAudioCodec); @@ -1081,13 +1086,13 @@ namespace MediaBrowser.Model.Dlna return 7168000; } - private (DirectPlayProfile Profile, PlayMethod? PlayMethod, int? AudioStreamIndex, TranscodeReason TranscodeReasons) GetVideoDirectPlayProfile( + private (DirectPlayProfile? Profile, PlayMethod? PlayMethod, int? AudioStreamIndex, TranscodeReason TranscodeReasons) GetVideoDirectPlayProfile( MediaOptions options, MediaSourceInfo mediaSource, - MediaStream videoStream, - MediaStream audioStream, + MediaStream? videoStream, + MediaStream? audioStream, ICollection<MediaStream> candidateAudioStreams, - MediaStream subtitleStream, + MediaStream? subtitleStream, bool isEligibleForDirectPlay, bool isEligibleForDirectStream) { @@ -1110,12 +1115,12 @@ namespace MediaBrowser.Model.Dlna int? bitDepth = videoStream?.BitDepth; int? videoBitrate = videoStream?.BitRate; double? videoLevel = videoStream?.Level; - string videoProfile = videoStream?.Profile; - string videoRangeType = videoStream?.VideoRangeType; + string? videoProfile = videoStream?.Profile; + string? videoRangeType = videoStream?.VideoRangeType; float videoFramerate = videoStream is null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0; bool? isAnamorphic = videoStream?.IsAnamorphic; bool? isInterlaced = videoStream?.IsInterlaced; - string videoCodecTag = videoStream?.CodecTag; + string? videoCodecTag = videoStream?.CodecTag; bool? isAvc = videoStream?.IsAVC; TransportStreamTimestamp? timestamp = videoStream is null ? TransportStreamTimestamp.None : mediaSource.Timestamp; @@ -1203,14 +1208,14 @@ namespace MediaBrowser.Model.Dlna } // Check video codec - string videoCodec = videoStream?.Codec; + string? videoCodec = videoStream?.Codec; if (!directPlayProfile.SupportsVideoCodec(videoCodec)) { directPlayProfileReasons |= TranscodeReason.VideoCodecNotSupported; } // Check audio codec - MediaStream selectedAudioStream = null; + MediaStream? selectedAudioStream = null; if (candidateAudioStreams.Any()) { selectedAudioStream = candidateAudioStreams.FirstOrDefault(audioStream => directPlayProfile.SupportsAudioCodec(audioStream.Codec)); @@ -1331,8 +1336,8 @@ namespace MediaBrowser.Model.Dlna SubtitleProfile[] subtitleProfiles, PlayMethod playMethod, ITranscoderSupport transcoderSupport, - string outputContainer, - string transcodingSubProtocol) + string? outputContainer, + string? transcodingSubProtocol) { if (!subtitleStream.IsExternal && (playMethod != PlayMethod.Transcode || !string.Equals(transcodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase))) { @@ -1405,7 +1410,7 @@ namespace MediaBrowser.Model.Dlna }; } - private static bool IsSubtitleEmbedSupported(string transcodingContainer) + private static bool IsSubtitleEmbedSupported(string? transcodingContainer) { if (!string.IsNullOrEmpty(transcodingContainer)) { @@ -1427,7 +1432,7 @@ namespace MediaBrowser.Model.Dlna return false; } - private static SubtitleProfile GetExternalSubtitleProfile(MediaSourceInfo mediaSource, MediaStream subtitleStream, SubtitleProfile[] subtitleProfiles, PlayMethod playMethod, ITranscoderSupport transcoderSupport, bool allowConversion) + private static SubtitleProfile? GetExternalSubtitleProfile(MediaSourceInfo mediaSource, MediaStream subtitleStream, SubtitleProfile[] subtitleProfiles, PlayMethod playMethod, ITranscoderSupport transcoderSupport, bool allowConversion) { foreach (var profile in subtitleProfiles) { @@ -1560,7 +1565,7 @@ namespace MediaBrowser.Model.Dlna private static IEnumerable<ProfileCondition> GetProfileConditionsForAudio( IEnumerable<CodecProfile> codecProfiles, string container, - string codec, + string? codec, int? audioChannels, int? audioBitrate, int? audioSampleRate, @@ -1580,7 +1585,7 @@ namespace MediaBrowser.Model.Dlna return conditions.Where(condition => !ConditionProcessor.IsAudioConditionSatisfied(condition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth)); } - private void ApplyTranscodingConditions(StreamInfo item, IEnumerable<ProfileCondition> conditions, string qualifier, bool enableQualifiedConditions, bool enableNonQualifiedConditions) + private void ApplyTranscodingConditions(StreamInfo item, IEnumerable<ProfileCondition> conditions, string? qualifier, bool enableQualifiedConditions, bool enableNonQualifiedConditions) { foreach (ProfileCondition condition in conditions) { @@ -2056,7 +2061,7 @@ namespace MediaBrowser.Model.Dlna } // Check audio codec - string audioCodec = audioStream?.Codec; + string? audioCodec = audioStream?.Codec; if (!profile.SupportsAudioCodec(audioCodec)) { return false; From b615f544174b8bab317eece3b559aa1417228c30 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 8 Mar 2023 05:16:14 +0000 Subject: [PATCH 134/858] chore(deps): update dependency lrcparser to v2023 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 93aad5d918..76a97ef648 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -21,7 +21,7 @@ <PackageVersion Include="FsCheck.Xunit" Version="2.16.5" /> <PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" /> <PackageVersion Include="libse" Version="3.6.10" /> - <PackageVersion Include="LrcParser" Version="2022.529.1" /> + <PackageVersion Include="LrcParser" Version="2023.308.0" /> <PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" /> <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.3" /> <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.3" /> From 651fc6677628aff004507d2da487b8ae8941cc4c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 8 Mar 2023 08:15:29 +0000 Subject: [PATCH 135/858] chore(deps): update dependency newtonsoft.json to v13.0.3 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 93aad5d918..3a8aaa2ec6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -51,7 +51,7 @@ <PackageVersion Include="Mono.Nat" Version="3.0.4" /> <PackageVersion Include="Moq" Version="4.18.4" /> <PackageVersion Include="NEbml" Version="0.11.0" /> - <PackageVersion Include="Newtonsoft.Json" Version="13.0.2" /> + <PackageVersion Include="Newtonsoft.Json" Version="13.0.3" /> <PackageVersion Include="PlaylistsNET" Version="1.3.1" /> <PackageVersion Include="prometheus-net.AspNetCore" Version="8.0.0" /> <PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" /> From 761b9ed6a1d7fa5f097e7baaa68fe480ceca82d6 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Wed, 8 Mar 2023 15:19:40 +0100 Subject: [PATCH 136/858] Allow webp for local images --- MediaBrowser.Controller/Entities/BaseItem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index b8601cccd9..8fe9cfa7f9 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -47,7 +47,7 @@ namespace MediaBrowser.Controller.Entities /// The supported image extensions. /// </summary> public static readonly string[] SupportedImageExtensions - = new[] { ".png", ".jpg", ".jpeg", ".tbn", ".gif" }; + = new[] { ".png", ".jpg", ".jpeg", ".webp", ".tbn", ".gif" }; private static readonly List<string> _supportedExtensions = new List<string>(SupportedImageExtensions) { From daefdaf8b0867d8f46056c80ecd19a94c5d7561e Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Thu, 9 Mar 2023 14:13:57 +0100 Subject: [PATCH 137/858] Extend language code handling --- .../Localization/LocalizationManager.cs | 84 +++++++++++-------- .../Globalization/ILocalizationManager.cs | 3 +- .../Localization/LocalizationManagerTests.cs | 4 +- 3 files changed, 55 insertions(+), 36 deletions(-) diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index 6e2a33fd56..166b71b4a3 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -184,10 +184,19 @@ namespace Emby.Server.Implementations.Localization /// <inheritdoc /> public IEnumerable<ParentalRating> GetParentalRatings() { - var ratings = GetParentalRatingsDictionary().Values.ToList(); + // Use server default language for ratings + // Fall back to empty list if there are no parental ratings for that language + var ratings = GetParentalRatingsDictionary()?.Values.ToList() + ?? new List<ParentalRating>(); - // Add common ratings to ensure them being available for selection. + // Add common ratings to ensure them being available for selection // Based on the US rating system due to it being the main source of rating in the metadata providers + // Unrated + if (!ratings.Any(x => x.Value is null)) + { + ratings.Add(new ParentalRating("Unrated", null)); + } + // Minimum rating possible if (!ratings.Any(x => x.Value == 0)) { @@ -237,36 +246,26 @@ namespace Emby.Server.Implementations.Localization /// <summary> /// Gets the parental ratings dictionary. /// </summary> + /// <param name="countryCode">The optional two letter ISO language string.</param> /// <returns><see cref="Dictionary{String, ParentalRating}" />.</returns> - private Dictionary<string, ParentalRating> GetParentalRatingsDictionary() + private Dictionary<string, ParentalRating>? GetParentalRatingsDictionary(string? countryCode = null) { - var countryCode = _configurationManager.Configuration.MetadataCountryCode; - - // Fall back to US ratings if no country code is specified or country code does not exist. + // Fallback to server default if no country code is specified. if (string.IsNullOrEmpty(countryCode)) { - countryCode = "us"; + countryCode = _configurationManager.Configuration.MetadataCountryCode; } - return GetRatings(countryCode) - ?? GetRatings("us") - ?? throw new InvalidOperationException($"Invalid resource path: '{CountriesPath}'"); - } + if (_allParentalRatings.TryGetValue(countryCode, out var countryValue)) + { + return countryValue; + } - /// <summary> - /// Gets the ratings for a country. - /// </summary> - /// <param name="countryCode">The country code.</param> - /// <returns>The ratings.</returns> - private Dictionary<string, ParentalRating>? GetRatings(string countryCode) - { - _allParentalRatings.TryGetValue(countryCode, out var countryValue); - - return countryValue; + return null; } /// <inheritdoc /> - public int? GetRatingLevel(string rating) + public int? GetRatingLevel(string rating, string? countryCode = null) { ArgumentException.ThrowIfNullOrEmpty(rating); @@ -280,32 +279,51 @@ namespace Emby.Server.Implementations.Localization rating = rating.Replace("Rated :", string.Empty, StringComparison.OrdinalIgnoreCase); rating = rating.Replace("Rated ", string.Empty, StringComparison.OrdinalIgnoreCase); - var ratingsDictionary = GetParentalRatingsDictionary(); - - if (ratingsDictionary.TryGetValue(rating, out ParentalRating? value)) + // Use rating system matching the language + if (!string.IsNullOrEmpty(countryCode)) { - return value.Value; + var ratingsDictionary = GetParentalRatingsDictionary(countryCode); + if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRating? value)) + { + return value.Value; + } } - - // If we don't find anything check all ratings systems - foreach (var dictionary in _allParentalRatings.Values) + else { - if (dictionary.TryGetValue(rating, out value)) + // Fall back to server default language for ratings check + // If it has no ratings, use the US ratings + var ratingsDictionary = GetParentalRatingsDictionary() ?? GetParentalRatingsDictionary("us"); + if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRating? value)) { return value.Value; } } - // Try splitting by : to handle "Germany: FSK 18" + // If we don't find anything, check all ratings systems + foreach (var dictionary in _allParentalRatings.Values) + { + if (dictionary.TryGetValue(rating, out var value)) + { + return value.Value; + } + } + + // Try splitting by : to handle "Germany: FSK-18" if (rating.Contains(':', StringComparison.OrdinalIgnoreCase)) { return GetRatingLevel(rating.AsSpan().RightPart(':').ToString()); } - // Remove prefix country code to handle "DE-18" + // Handle prefix country code to handle "DE-18" if (rating.Contains('-', StringComparison.OrdinalIgnoreCase)) { - return GetRatingLevel(rating.AsSpan().RightPart('-').ToString()); + var ratingSpan = rating.AsSpan(); + + // Extract culture from country prefix + var culture = FindLanguageInfo(ratingSpan.LeftPart('-').ToString()); + + // Check rating system of culture + return GetRatingLevel(ratingSpan.RightPart('-').ToString(), culture?.TwoLetterISOLanguageName); } return null; diff --git a/MediaBrowser.Model/Globalization/ILocalizationManager.cs b/MediaBrowser.Model/Globalization/ILocalizationManager.cs index e00157dce9..02a29e7faf 100644 --- a/MediaBrowser.Model/Globalization/ILocalizationManager.cs +++ b/MediaBrowser.Model/Globalization/ILocalizationManager.cs @@ -30,8 +30,9 @@ namespace MediaBrowser.Model.Globalization /// Gets the rating level. /// </summary> /// <param name="rating">The rating.</param> + /// <param name="countryCode">The optional two letter ISO language string.</param> /// <returns><see cref="int" /> or <c>null</c>.</returns> - int? GetRatingLevel(string rating); + int? GetRatingLevel(string rating, string? countryCode = null); /// <summary> /// Gets the localized string. diff --git a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs index ab3682ccf6..7fabe99045 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs @@ -83,7 +83,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization await localizationManager.LoadAll(); var ratings = localizationManager.GetParentalRatings().ToList(); - Assert.Equal(53, ratings.Count); + Assert.Equal(54, ratings.Count); var tvma = ratings.FirstOrDefault(x => x.Name.Equals("TV-MA", StringComparison.Ordinal)); Assert.NotNull(tvma); @@ -100,7 +100,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization await localizationManager.LoadAll(); var ratings = localizationManager.GetParentalRatings().ToList(); - Assert.Equal(18, ratings.Count); + Assert.Equal(19, ratings.Count); var fsk = ratings.FirstOrDefault(x => x.Name.Equals("FSK-12", StringComparison.Ordinal)); Assert.NotNull(fsk); From 6b0135d03b757600bf9066d9ddd9bc423b470192 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3bert=20=C3=96rn=20Ketilsson?= <robertornk@outlook.com> Date: Wed, 8 Mar 2023 16:42:27 +0000 Subject: [PATCH 138/858] Translated using Weblate (Icelandic) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/is/ --- Emby.Server.Implementations/Localization/Core/is.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/is.json b/Emby.Server.Implementations/Localization/Core/is.json index b262a8b424..a40f495061 100644 --- a/Emby.Server.Implementations/Localization/Core/is.json +++ b/Emby.Server.Implementations/Localization/Core/is.json @@ -107,5 +107,14 @@ "TasksApplicationCategory": "Forrit", "TasksLibraryCategory": "Miðlasafn", "TasksMaintenanceCategory": "Viðhald", - "Default": "Sjálfgefið" + "Default": "Sjálfgefið", + "TaskCleanActivityLog": "Hreinsa athafnaskrá", + "TaskRefreshPeople": "Endurnýja fólk", + "TaskDownloadMissingSubtitles": "Sækja texta sem vantar", + "TaskOptimizeDatabase": "Fínstilla gagnagrunn", + "Undefined": "Óskilgreint", + "TaskCleanLogsDescription": "Eyðir færslu skrám sem eru meira en {0} gömul.", + "TaskCleanLogs": "Hreinsa færslu skrá", + "TaskDownloadMissingSubtitlesDescription": "Leitar á netinu að texta sem vantar miðað við uppsetningu lýsigagna.", + "HearingImpaired": "Heyrnarskertur" } From b96420b7865538439c6a96305d70fa4f16dfd591 Mon Sep 17 00:00:00 2001 From: SenorSmartyPants <senorsmartypants@gmail.com> Date: Sun, 26 Feb 2023 13:32:46 -0600 Subject: [PATCH 139/858] Clean Extra Names - Adds regular expression to CleanStrings to remove suffix style extra naming from the name presented in JF. - Override Resolve for Extras to enable parsename - remove exclusion on parsename of extratypes --- Emby.Naming/Common/NamingOptions.cs | 3 ++- Emby.Naming/Video/VideoResolver.cs | 3 +-- .../Library/Resolvers/ExtraResolver.cs | 11 +++++++++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index e9161a6b7b..1406c64435 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -157,7 +157,8 @@ namespace Emby.Naming.Common @"^(?<cleaned>.+?)(\[.*\])", @"^\s*(?<cleaned>.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)", @"^\s*\[[^\]]+\](?!\.\w+$)\s*(?<cleaned>.+)", - @"^\s*(?<cleaned>.+?)\s+-\s+[0-9]+\s*$" + @"^\s*(?<cleaned>.+?)\s+-\s+[0-9]+\s*$", + @"^\s*(?<cleaned>.+?)(([-._ ](trailer|sample))|-(scene|clip|behindthescenes|deleted|deletedscene|featurette|short|interview|other|extra))$" }; SubtitleFileExtensions = new[] diff --git a/Emby.Naming/Video/VideoResolver.cs b/Emby.Naming/Video/VideoResolver.cs index 858e9dd2f5..db5bfdbf94 100644 --- a/Emby.Naming/Video/VideoResolver.cs +++ b/Emby.Naming/Video/VideoResolver.cs @@ -87,8 +87,7 @@ namespace Emby.Naming.Video name = cleanDateTimeResult.Name; year = cleanDateTimeResult.Year; - if (extraResult.ExtraType is null - && TryCleanString(name, namingOptions, out var newName)) + if (TryCleanString(name, namingOptions, out var newName)) { name = newName; } diff --git a/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs b/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs index 0b255f673b..b4791b9456 100644 --- a/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs @@ -4,6 +4,7 @@ using System.IO; using Emby.Naming.Common; using Emby.Naming.Video; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Model.Entities; @@ -15,7 +16,7 @@ namespace Emby.Server.Implementations.Library.Resolvers /// <summary> /// Resolves a Path into a Video or Video subclass. /// </summary> - internal class ExtraResolver + internal class ExtraResolver : BaseVideoResolver<Video> { private readonly NamingOptions _namingOptions; private readonly IItemResolver[] _trailerResolvers; @@ -28,10 +29,16 @@ namespace Emby.Server.Implementations.Library.Resolvers /// <param name="namingOptions">An instance of <see cref="NamingOptions"/>.</param> /// <param name="directoryService">The directory service.</param> public ExtraResolver(ILogger<ExtraResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService) + : base(logger, namingOptions, directoryService) { _namingOptions = namingOptions; _trailerResolvers = new IItemResolver[] { new GenericVideoResolver<Trailer>(logger, namingOptions, directoryService) }; - _videoResolvers = new IItemResolver[] { new GenericVideoResolver<Video>(logger, namingOptions, directoryService) }; + _videoResolvers = new IItemResolver[] { this }; + } + + protected override Video Resolve(ItemResolveArgs args) + { + return ResolveVideo<Video>(args, true); } /// <summary> From 99816b07dceeb72f437a0578172a9d488ce0fcc5 Mon Sep 17 00:00:00 2001 From: SenorSmartyPants <senorsmartypants@gmail.com> Date: Sun, 26 Feb 2023 13:35:00 -0600 Subject: [PATCH 140/858] Enable NFO processing for Extras - Change test to prevent owned items from using parent NFO. Test is now in MovieNFOSaver, only movie type will use movie.nfo. --- MediaBrowser.Providers/Manager/ProviderManager.cs | 6 ------ MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs | 3 ++- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 81ccd86534..d8122511ee 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -404,12 +404,6 @@ namespace MediaBrowser.Providers.Manager return false; } - // Prevent owned items from reading the same local metadata file as their owner - if (!item.OwnerId.Equals(default) && provider is ILocalMetadataProvider) - { - return false; - } - if (includeDisabled) { return true; diff --git a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs index 21e7e2335b..28cf9f7438 100644 --- a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs @@ -62,7 +62,8 @@ namespace MediaBrowser.XbmcMetadata.Savers { yield return Path.ChangeExtension(item.Path, ".nfo"); - if (!item.IsInMixedFolder) + // only allow movie object to read movie.nfo, not owned videos (which will be itemtype video, not movie) + if (!item.IsInMixedFolder && item.ItemType.Equals(typeof(Movie))) { yield return Path.Combine(item.ContainingFolderPath, "movie.nfo"); } From 04f23a0e735b29376ced0f6c78aa762f11b8b475 Mon Sep 17 00:00:00 2001 From: SenorSmartyPants <senorsmartypants@gmail.com> Date: Sun, 26 Feb 2023 14:13:21 -0600 Subject: [PATCH 141/858] Change test to allow owned items to run local providers I need more information about the need for this test, to make sure I am not introducing an issue. --- .../Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs b/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs index 5ca59f0ede..400e30bd63 100644 --- a/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs +++ b/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs @@ -368,8 +368,8 @@ namespace Jellyfin.Providers.Tests.Manager [Theory] [InlineData(nameof(ICustomMetadataProvider), true)] [InlineData(nameof(IRemoteMetadataProvider), true)] - [InlineData(nameof(ILocalMetadataProvider), false)] - public void GetMetadataProviders_CanRefreshMetadataOwned_WhenNotLocal(string providerType, bool expected) + [InlineData(nameof(ILocalMetadataProvider), true)] + public void GetMetadataProviders_CanRefreshMetadataOwned(string providerType, bool expected) { GetMetadataProviders_CanRefreshMetadata_Tester(providerType, expected, ownedItem: true); } From 4f0615452b2ae25927ffb80102f1fc91cf95d49c Mon Sep 17 00:00:00 2001 From: SenorSmartyPants <senorsmartypants@gmail.com> Date: Mon, 27 Feb 2023 12:01:29 -0600 Subject: [PATCH 142/858] Update MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs Apply code review Co-authored-by: Cody Robibero <cody@robibe.ro> --- MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs index 28cf9f7438..82e1dc860d 100644 --- a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs @@ -63,7 +63,7 @@ namespace MediaBrowser.XbmcMetadata.Savers yield return Path.ChangeExtension(item.Path, ".nfo"); // only allow movie object to read movie.nfo, not owned videos (which will be itemtype video, not movie) - if (!item.IsInMixedFolder && item.ItemType.Equals(typeof(Movie))) + if (!item.IsInMixedFolder && item.ItemType == typeof(Movie)) { yield return Path.Combine(item.ContainingFolderPath, "movie.nfo"); } From ef3868ff501c2f025f235f10ae520d07f18fa516 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3bert=20=C3=96rn=20Ketilsson?= <robertornk@outlook.com> Date: Thu, 9 Mar 2023 19:29:39 -0500 Subject: [PATCH 143/858] Backport pull request #9178 from jellyfin/release-10.8.z Escape the path to pass as a command line argument Original-merge: 09f1c7f535653e99dbc22ace7cd166ce4c457a83 Merged-by: Dmitry Lyzo <56478732+dmitrylyzo@users.noreply.github.com> Backported-by: crobibero <cody@robibe.ro> --- Jellyfin.Api/Controllers/DynamicHlsController.cs | 6 ++++-- Jellyfin.Api/Jellyfin.Api.csproj | 1 + .../Attachments/AttachmentExtractor.cs | 5 +++-- MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 4d8b4de24f..7b8f874524 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -19,6 +19,8 @@ using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Net; +using MediaBrowser.MediaEncoding.Encoder; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Entities; @@ -1654,8 +1656,8 @@ public class DynamicHlsController : BaseJellyfinApiController startNumber.ToString(CultureInfo.InvariantCulture), baseUrlParam, isEventPlaylist ? "event" : "vod", - outputTsArg, - outputPath).Trim(); + EncodingUtils.NormalizePath(outputTsArg), + EncodingUtils.NormalizePath(outputPath)).Trim(); } /// <summary> diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index a8a44fd3e0..6a0a4706be 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -22,6 +22,7 @@ <ItemGroup> <ProjectReference Include="..\Emby.Dlna\Emby.Dlna.csproj" /> <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" /> + <ProjectReference Include="..\MediaBrowser.MediaEncoding\MediaBrowser.MediaEncoding.csproj" /> <ProjectReference Include="..\src\Jellyfin.MediaEncoding.Hls\Jellyfin.MediaEncoding.Hls.csproj" /> </ItemGroup> diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs index db177ff769..80091bf5a8 100644 --- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs +++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs @@ -14,6 +14,7 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.MediaEncoding.Encoder; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; @@ -301,10 +302,10 @@ namespace MediaBrowser.MediaEncoding.Attachments var processArgs = string.Format( CultureInfo.InvariantCulture, - "-dump_attachment:{1} {2} -i {0} -t 0 -f null null", + "-dump_attachment:{1} \"{2}\" -i {0} -t 0 -f null null", inputPath, attachmentStreamIndex, - outputPath); + EncodingUtils.NormalizePath(outputPath)); int exitCode; diff --git a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs index d0ea0429b5..95a93974ad 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs @@ -56,7 +56,7 @@ namespace MediaBrowser.MediaEncoding.Encoder /// </summary> /// <param name="path">The path.</param> /// <returns>System.String.</returns> - private static string NormalizePath(string path) + public static string NormalizePath(string path) { // Quotes are valid path characters in linux and they need to be escaped here with a leading \ return path.Replace("\"", "\\\"", StringComparison.Ordinal); From 65090ac817f86c7591d916a7ce0d9ceb4f26e821 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Thu, 9 Mar 2023 19:33:51 -0500 Subject: [PATCH 144/858] Backport pull request #9351 from jellyfin/release-10.8.z Fix EqualsAny condition check for int and double Original-merge: e8b0ae07afd9fc08a216d6aa632ee20d6d88566b Merged-by: Bond-009 <bond.009@outlook.com> Backported-by: crobibero <cody@robibe.ro> --- MediaBrowser.Model/Dlna/ConditionProcessor.cs | 39 ++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/MediaBrowser.Model/Dlna/ConditionProcessor.cs b/MediaBrowser.Model/Dlna/ConditionProcessor.cs index 00b406bbe6..f5e1a3c496 100644 --- a/MediaBrowser.Model/Dlna/ConditionProcessor.cs +++ b/MediaBrowser.Model/Dlna/ConditionProcessor.cs @@ -136,12 +136,26 @@ namespace MediaBrowser.Model.Dlna return !condition.IsRequired; } - if (int.TryParse(condition.Value, CultureInfo.InvariantCulture, out var expected)) + var conditionType = condition.Condition; + if (condition.Condition == ProfileConditionType.EqualsAny) { - switch (condition.Condition) + foreach (var singleConditionString in condition.Value.AsSpan().Split('|')) + { + if (int.TryParse(singleConditionString, NumberStyles.Integer, CultureInfo.InvariantCulture, out int conditionValue) + && conditionValue.Equals(currentValue)) + { + return true; + } + } + + return false; + } + + if (int.TryParse(condition.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var expected)) + { + switch (conditionType) { case ProfileConditionType.Equals: - case ProfileConditionType.EqualsAny: return currentValue.Value.Equals(expected); case ProfileConditionType.GreaterThanEqual: return currentValue.Value >= expected; @@ -212,9 +226,24 @@ namespace MediaBrowser.Model.Dlna return !condition.IsRequired; } - if (double.TryParse(condition.Value, CultureInfo.InvariantCulture, out var expected)) + var conditionType = condition.Condition; + if (condition.Condition == ProfileConditionType.EqualsAny) { - switch (condition.Condition) + foreach (var singleConditionString in condition.Value.AsSpan().Split('|')) + { + if (double.TryParse(singleConditionString, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out double conditionValue) + && conditionValue.Equals(currentValue)) + { + return true; + } + } + + return false; + } + + if (double.TryParse(condition.Value, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var expected)) + { + switch (conditionType) { case ProfileConditionType.Equals: return currentValue.Value.Equals(expected); From f6060bd14b79937eb1f0784e53486f0fc3726c8a Mon Sep 17 00:00:00 2001 From: nyanmisaka <nst799610810@gmail.com> Date: Thu, 9 Mar 2023 19:33:52 -0500 Subject: [PATCH 145/858] Backport pull request #9355 from jellyfin/release-10.8.z Some VAAPI VPP and OpenCL fixes Original-merge: c8077122463c7b984f706b5f1b79abd30461ab40 Merged-by: Bond-009 <bond.009@outlook.com> Backported-by: crobibero <cody@robibe.ro> --- .../MediaEncoding/EncodingHelper.cs | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index f2fb3705c9..f07ac07513 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -3297,7 +3297,7 @@ namespace MediaBrowser.Controller.MediaEncoding // OUTPUT nv12 surface(memory) // prefer hwmap to hwdownload on opencl. - var hwTransferFilter = hasGraphicalSubs ? "hwdownload" : "hwmap"; + var hwTransferFilter = hasGraphicalSubs ? "hwdownload" : "hwmap=mode=read"; mainFilters.Add(hwTransferFilter); mainFilters.Add("format=nv12"); } @@ -3540,7 +3540,7 @@ namespace MediaBrowser.Controller.MediaEncoding // OUTPUT nv12 surface(memory) // prefer hwmap to hwdownload on opencl. // qsv hwmap is not fully implemented for the time being. - mainFilters.Add(isHwmapUsable ? "hwmap" : "hwdownload"); + mainFilters.Add(isHwmapUsable ? "hwmap=mode=read" : "hwdownload"); mainFilters.Add("format=nv12"); } @@ -3698,6 +3698,13 @@ namespace MediaBrowser.Controller.MediaEncoding var outFormat = doTonemap ? string.Empty : "nv12"; var hwScaleFilter = GetHwScaleFilter(isVaapiDecoder ? "vaapi" : "qsv", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH); + + // allocate extra pool sizes for vaapi vpp + if (!string.IsNullOrEmpty(hwScaleFilter) && isVaapiDecoder) + { + hwScaleFilter += ":extra_hw_frames=24"; + } + // hw scale mainFilters.Add(hwScaleFilter); } @@ -3744,7 +3751,7 @@ namespace MediaBrowser.Controller.MediaEncoding // OUTPUT nv12 surface(memory) // prefer hwmap to hwdownload on opencl/vaapi. // qsv hwmap is not fully implemented for the time being. - mainFilters.Add(isHwmapUsable ? "hwmap" : "hwdownload"); + mainFilters.Add(isHwmapUsable ? "hwmap=mode=read" : "hwdownload"); mainFilters.Add("format=nv12"); } @@ -3973,6 +3980,13 @@ namespace MediaBrowser.Controller.MediaEncoding var outFormat = doTonemap ? string.Empty : "nv12"; var hwScaleFilter = GetHwScaleFilter("vaapi", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH); + + // allocate extra pool sizes for vaapi vpp + if (!string.IsNullOrEmpty(hwScaleFilter)) + { + hwScaleFilter += ":extra_hw_frames=24"; + } + // hw scale mainFilters.Add(hwScaleFilter); } @@ -4014,7 +4028,7 @@ namespace MediaBrowser.Controller.MediaEncoding // OUTPUT nv12 surface(memory) // prefer hwmap to hwdownload on opencl/vaapi. - mainFilters.Add(isHwmapNotUsable ? "hwdownload" : "hwmap"); + mainFilters.Add(isHwmapNotUsable ? "hwdownload" : "hwmap=mode=read"); mainFilters.Add("format=nv12"); } @@ -4367,6 +4381,13 @@ namespace MediaBrowser.Controller.MediaEncoding outFormat = doOclTonemap ? string.Empty : "nv12"; var hwScaleFilter = GetHwScaleFilter("vaapi", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH); + + // allocate extra pool sizes for vaapi vpp + if (!string.IsNullOrEmpty(hwScaleFilter)) + { + hwScaleFilter += ":extra_hw_frames=24"; + } + // hw scale mainFilters.Add(hwScaleFilter); } From 638bda629ba2fdceb58b0732f9c2cf4684edc115 Mon Sep 17 00:00:00 2001 From: nyanmisaka <nst799610810@gmail.com> Date: Thu, 9 Mar 2023 19:35:54 -0500 Subject: [PATCH 146/858] Backport pull request #9391 from jellyfin/release-10.8.z Fix H.264 baseline hwaccel and enable enhanced Nvdec by default Original-merge: 22a8283a9e3425da0496c28e6737dfadf9c67b33 Merged-by: Bond-009 <bond.009@outlook.com> Backported-by: crobibero <cody@robibe.ro> --- .../MediaEncoding/EncodingHelper.cs | 17 +++++++++++++---- .../Configuration/EncodingOptions.cs | 3 ++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index f07ac07513..0f3912e283 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -4909,6 +4909,10 @@ namespace MediaBrowser.Controller.MediaEncoding // Set the av1 codec explicitly to trigger hw accelerator, otherwise libdav1d will be used. var isAv1 = string.Equals(videoCodec, "av1", StringComparison.OrdinalIgnoreCase); + // Allow profile mismatch if decoding H.264 baseline with d3d11va and vaapi hwaccels. + var profileMismatch = string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase) + && string.Equals(state.VideoStream?.Profile, "baseline", StringComparison.OrdinalIgnoreCase); + if (bitDepth == 10 && isCodecAvailable) { if (string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase) @@ -4933,14 +4937,16 @@ namespace MediaBrowser.Controller.MediaEncoding { if (isVaapiSupported && isCodecAvailable) { - return " -hwaccel vaapi" + (outputHwSurface ? " -hwaccel_output_format vaapi" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty); + return " -hwaccel vaapi" + (outputHwSurface ? " -hwaccel_output_format vaapi" : string.Empty) + + (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty); } if (isD3d11Supported && isCodecAvailable) { // set -threads 3 to intel d3d11va decoder explicitly. Lower threads may result in dead lock. // on newer devices such as Xe, the larger the init_pool_size, the longer the initialization time for opencl to derive from d3d11. - return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11" : string.Empty) + " -threads 3" + (isAv1 ? " -c:v av1" : string.Empty); + return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11" : string.Empty) + + (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + " -threads 3" + (isAv1 ? " -c:v av1" : string.Empty); } } else @@ -4975,7 +4981,8 @@ namespace MediaBrowser.Controller.MediaEncoding { if (isD3d11Supported && isCodecAvailable) { - return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty); + return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11" : string.Empty) + + (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty); } } @@ -4984,9 +4991,11 @@ namespace MediaBrowser.Controller.MediaEncoding && isVaapiSupported && isCodecAvailable) { - return " -hwaccel vaapi" + (outputHwSurface ? " -hwaccel_output_format vaapi" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty); + return " -hwaccel vaapi" + (outputHwSurface ? " -hwaccel_output_format vaapi" : string.Empty) + + (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty); } + // Apple videotoolbox if (string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase) && isVideotoolboxSupported && isCodecAvailable) diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index 0ff95a2e1f..f348d8417b 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -39,7 +39,8 @@ public class EncodingOptions DeinterlaceMethod = "yadif"; EnableDecodingColorDepth10Hevc = true; EnableDecodingColorDepth10Vp9 = true; - EnableEnhancedNvdecDecoder = false; + // Enhanced Nvdec or system native decoder is required for DoVi to SDR tone-mapping. + EnableEnhancedNvdecDecoder = true; PreferSystemNativeHwDecoder = true; EnableIntelLowPowerH264HwEncoder = false; EnableIntelLowPowerHevcHwEncoder = false; From f21ab50a81dec27a14f7f985917eca7ea55176b2 Mon Sep 17 00:00:00 2001 From: nyanmisaka <nst799610810@gmail.com> Date: Thu, 9 Mar 2023 19:38:15 -0500 Subject: [PATCH 147/858] Backport pull request #9422 from jellyfin/release-10.8.z Fix stream map when using filter_complex with unlabeled output Original-merge: 6821a2ab358761282a0030c42c837b39bad089e1 Merged-by: Bond-009 <bond.009@outlook.com> Backported-by: crobibero <cody@robibe.ro> --- .../Controllers/DynamicHlsController.cs | 6 ++- .../MediaEncoding/EncodingHelper.cs | 42 +++++++++++++++++-- .../EncoderValidatorTests.cs | 4 ++ .../EncoderValidatorTestsData.cs | 24 +++++++++++ 4 files changed, 72 insertions(+), 4 deletions(-) diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 7b8f874524..16c77a923f 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1842,7 +1842,11 @@ public class DynamicHlsController : BaseJellyfinApiController // args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0"; // video processing filters. - args += _encodingHelper.GetVideoProcessingFilterParam(state, _encodingOptions, codec); + var videoProcessParam = _encodingHelper.GetVideoProcessingFilterParam(state, _encodingOptions, codec); + + var negativeMapArgs = _encodingHelper.GetNegativeMapArgsByFilters(state, videoProcessParam); + + args = negativeMapArgs + args + videoProcessParam; // -start_at_zero is necessary to use with -ss when seeking, // otherwise the target position cannot be determined. diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 0f3912e283..afe5a686e7 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -43,6 +43,9 @@ namespace MediaBrowser.Controller.MediaEncoding private readonly Version _maxKerneli915Hang = new Version(6, 1, 3); private readonly Version _minFixedKernel60i915Hang = new Version(6, 0, 18); + private readonly Version _minFFmpegImplictHwaccel = new Version(6, 0); + private readonly Version _minFFmpegHwaUnsafeOutput = new Version(6, 0); + private static readonly string[] _videoProfilesH264 = new[] { "ConstrainedBaseline", @@ -2455,6 +2458,30 @@ namespace MediaBrowser.Controller.MediaEncoding return args; } + /// <summary> + /// Gets the negative map args by filters. + /// </summary> + /// <param name="state">The state.</param> + /// <param name="videoProcessFilters">The videoProcessFilters.</param> + /// <returns>System.String.</returns> + public string GetNegativeMapArgsByFilters(EncodingJobInfo state, string videoProcessFilters) + { + string args = string.Empty; + + // http://ffmpeg.org/ffmpeg-all.html#toc-Complex-filtergraphs-1 + if (state.VideoStream != null && videoProcessFilters.Contains("-filter_complex", StringComparison.Ordinal)) + { + int videoStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.VideoStream); + + args += string.Format( + CultureInfo.InvariantCulture, + "-map -0:{0} ", + videoStreamIndex); + } + + return args; + } + /// <summary> /// Determines which stream will be used for playback. /// </summary> @@ -4906,13 +4933,19 @@ namespace MediaBrowser.Controller.MediaEncoding var isVideotoolboxSupported = isMacOS && _mediaEncoder.SupportsHwaccel("videotoolbox"); var isCodecAvailable = options.HardwareDecodingCodecs.Contains(videoCodec, StringComparison.OrdinalIgnoreCase); + var ffmpegVersion = _mediaEncoder.EncoderVersion; + // Set the av1 codec explicitly to trigger hw accelerator, otherwise libdav1d will be used. - var isAv1 = string.Equals(videoCodec, "av1", StringComparison.OrdinalIgnoreCase); + var isAv1 = ffmpegVersion < _minFFmpegImplictHwaccel + && string.Equals(videoCodec, "av1", StringComparison.OrdinalIgnoreCase); // Allow profile mismatch if decoding H.264 baseline with d3d11va and vaapi hwaccels. var profileMismatch = string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase) && string.Equals(state.VideoStream?.Profile, "baseline", StringComparison.OrdinalIgnoreCase); + // Disable the extra internal copy in nvdec. We already handle it in filter chain. + var nvdecNoInternalCopy = ffmpegVersion >= _minFFmpegHwaUnsafeOutput; + if (bitDepth == 10 && isCodecAvailable) { if (string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase) @@ -4966,7 +4999,8 @@ namespace MediaBrowser.Controller.MediaEncoding if (options.EnableEnhancedNvdecDecoder) { // set -threads 1 to nvdec decoder explicitly since it doesn't implement threading support. - return " -hwaccel cuda" + (outputHwSurface ? " -hwaccel_output_format cuda" : string.Empty) + " -threads 1" + (isAv1 ? " -c:v av1" : string.Empty); + return " -hwaccel cuda" + (outputHwSurface ? " -hwaccel_output_format cuda" : string.Empty) + + (nvdecNoInternalCopy ? " -hwaccel_flags +unsafe_output" : string.Empty) + " -threads 1" + (isAv1 ? " -c:v av1" : string.Empty); } else { @@ -5799,7 +5833,9 @@ namespace MediaBrowser.Controller.MediaEncoding // video processing filters. var videoProcessParam = GetVideoProcessingFilterParam(state, encodingOptions, videoCodec); - args += videoProcessParam; + var negativeMapArgs = GetNegativeMapArgsByFilters(state, videoProcessParam); + + args = negativeMapArgs + args + videoProcessParam; hasCopyTs = videoProcessParam.Contains("copyts", StringComparison.OrdinalIgnoreCase); diff --git a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs index 1b27e344ba..db7e91c6a2 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs @@ -17,6 +17,8 @@ namespace Jellyfin.MediaEncoding.Tests } [Theory] + [InlineData(EncoderValidatorTestsData.FFmpegV60Output, true)] + [InlineData(EncoderValidatorTestsData.FFmpegV512Output, true)] [InlineData(EncoderValidatorTestsData.FFmpegV44Output, true)] [InlineData(EncoderValidatorTestsData.FFmpegV432Output, true)] [InlineData(EncoderValidatorTestsData.FFmpegV431Output, true)] @@ -36,6 +38,8 @@ namespace Jellyfin.MediaEncoding.Tests { public GetFFmpegVersionTestData() { + Add(EncoderValidatorTestsData.FFmpegV60Output, new Version(6, 0)); + Add(EncoderValidatorTestsData.FFmpegV512Output, new Version(5, 1, 2)); Add(EncoderValidatorTestsData.FFmpegV44Output, new Version(4, 4)); Add(EncoderValidatorTestsData.FFmpegV432Output, new Version(4, 3, 2)); Add(EncoderValidatorTestsData.FFmpegV431Output, new Version(4, 3, 1)); diff --git a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs index 02bf046ed1..89ba42da0c 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs @@ -2,6 +2,30 @@ namespace Jellyfin.MediaEncoding.Tests { internal static class EncoderValidatorTestsData { + public const string FFmpegV60Output = @"ffmpeg version 6.0-Jellyfin Copyright (c) 2000-2023 the FFmpeg developers +built with gcc 12.2.0 (crosstool-NG 1.25.0.90_cf9beb1) +configuration: --prefix=/ffbuild/prefix --pkg-config=pkg-config --pkg-config-flags=--static --cross-prefix=x86_64-w64-mingw32- --arch=x86_64 --target-os=mingw32 --extra-version=Jellyfin --extra-cflags= --extra-cxxflags= --extra-ldflags= --extra-ldexeflags= --extra-libs= --enable-gpl --enable-version3 --enable-lto --disable-ffplay --disable-debug --disable-doc --disable-ptx-compression --disable-sdl2 --disable-w32threads --enable-pthreads --enable-iconv --enable-libxml2 --enable-zlib --enable-libfreetype --enable-libfribidi --enable-gmp --enable-lzma --enable-fontconfig --enable-libvorbis --enable-opencl --enable-amf --enable-chromaprint --enable-libdav1d --enable-dxva2 --enable-d3d11va --enable-libfdk-aac --enable-ffnvcodec --enable-cuda --enable-cuda-llvm --enable-cuvid --enable-nvdec --enable-nvenc --enable-libass --enable-libbluray --enable-libmp3lame --enable-libopus --enable-libtheora --enable-libvpx --enable-libwebp --enable-libvpl --enable-schannel --enable-libsrt --enable-libsvtav1 --enable-vulkan --enable-libshaderc --enable-libplacebo --enable-libx264 --enable-libx265 --enable-libzimg --enable-libzvbi +libavutil 58. 2.100 / 58. 2.100 +libavcodec 60. 3.100 / 60. 3.100 +libavformat 60. 3.100 / 60. 3.100 +libavdevice 60. 1.100 / 60. 1.100 +libavfilter 9. 3.100 / 9. 3.100 +libswscale 7. 1.100 / 7. 1.100 +libswresample 4. 10.100 / 4. 10.100 +libpostproc 57. 1.100 / 57. 1.100"; + + public const string FFmpegV512Output = @"ffmpeg version 5.1.2-Jellyfin Copyright (c) 2000-2022 the FFmpeg developers +built with gcc 10-win32 (GCC) 20220324 +configuration: --prefix=/opt/ffmpeg --arch=x86_64 --target-os=mingw32 --cross-prefix=x86_64-w64-mingw32- --pkg-config=pkg-config --pkg-config-flags=--static --extra-libs='-lfftw3f -lstdc++' --extra-cflags=-DCHROMAPRINT_NODLL --extra-version=Jellyfin --disable-ffplay --disable-debug --disable-doc --disable-sdl2 --disable-ptx-compression --disable-w32threads --enable-pthreads --enable-shared --enable-lto --enable-gpl --enable-version3 --enable-schannel --enable-iconv --enable-libxml2 --enable-zlib --enable-lzma --enable-gmp --enable-chromaprint --enable-libfreetype --enable-libfribidi --enable-libfontconfig --enable-libass --enable-libbluray --enable-libmp3lame --enable-libopus --enable-libtheora --enable-libvorbis --enable-libwebp --enable-libvpx --enable-libzimg --enable-libx264 --enable-libx265 --enable-libsvtav1 --enable-libdav1d --enable-libfdk-aac --enable-opencl --enable-dxva2 --enable-d3d11va --enable-amf --enable-libmfx --enable-ffnvcodec --enable-cuda --enable-cuda-llvm --enable-cuvid --enable-nvdec --enable-nvenc +libavutil 57. 28.100 / 57. 28.100 +libavcodec 59. 37.100 / 59. 37.100 +libavformat 59. 27.100 / 59. 27.100 +libavdevice 59. 7.100 / 59. 7.100 +libavfilter 8. 44.100 / 8. 44.100 +libswscale 6. 7.100 / 6. 7.100 +libswresample 4. 7.100 / 4. 7.100 +libpostproc 56. 6.100 / 56. 6.100"; + public const string FFmpegV44Output = @"ffmpeg version 4.4-Jellyfin Copyright (c) 2000-2021 the FFmpeg developers built with gcc 10.3.0 (Rev5, Built by MSYS2 project) configuration: --disable-static --enable-shared --extra-version=Jellyfin --disable-ffplay --disable-debug --enable-gpl --enable-version3 --enable-bzlib --enable-iconv --enable-lzma --enable-zlib --enable-sdl2 --enable-fontconfig --enable-gmp --enable-libass --enable-libzimg --enable-libbluray --enable-libfreetype --enable-libmp3lame --enable-libopus --enable-libtheora --enable-libvorbis --enable-libwebp --enable-libvpx --enable-libx264 --enable-libx265 --enable-libdav1d --enable-opencl --enable-dxva2 --enable-d3d11va --enable-amf --enable-libmfx --enable-cuda --enable-cuda-llvm --enable-cuvid --enable-nvenc --enable-nvdec --enable-ffnvcodec --enable-gnutls From 2146ddd20c5b8fe7a5bbe09d9ab0dbc18b1706f5 Mon Sep 17 00:00:00 2001 From: nyanmisaka <nst799610810@gmail.com> Date: Thu, 9 Mar 2023 19:38:16 -0500 Subject: [PATCH 148/858] Backport pull request #9430 from jellyfin/release-10.8.z Fix Live TV hardware decoding Original-merge: efc79295decce252e03978814fc09505bbb47956 Merged-by: Bond-009 <bond.009@outlook.com> Backported-by: crobibero <cody@robibe.ro> --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index afe5a686e7..e6ff988438 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -4792,7 +4792,7 @@ namespace MediaBrowser.Controller.MediaEncoding } // HWA decoders can handle both video files and video folders. - var videoType = mediaSource.VideoType; + var videoType = state.VideoType; if (videoType != VideoType.VideoFile && videoType != VideoType.Iso && videoType != VideoType.Dvd From 2403a0a36792d060d913abbd86bec03816da750b Mon Sep 17 00:00:00 2001 From: Shadowghost <Shadowghost@users.noreply.github.com> Date: Sun, 5 Feb 2023 17:24:13 +0100 Subject: [PATCH 149/858] Apply review suggestions --- Jellyfin.Api/Helpers/TranscodingJobHelper.cs | 2 +- .../MediaEncoding/EncodingHelper.cs | 2 +- .../Encoder/MediaEncoder.cs | 12 +-- MediaBrowser.Model/Dlna/StreamBuilder.cs | 1 - .../MediaInfo/FFProbeVideoInfo.cs | 87 +++++++++---------- 5 files changed, 51 insertions(+), 53 deletions(-) diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index 1ba7395508..3bb3ad358f 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -325,7 +325,7 @@ public class TranscodingJobHelper : IDisposable await DeletePartialStreamFiles(job.Path!, job.Type, 0, 1500).ConfigureAwait(false); if (job.MediaSource?.VideoType == VideoType.Dvd || job.MediaSource?.VideoType == VideoType.BluRay) { - var path = Path.Join(job.Path, "/" + job.MediaSource.Id + ".concat"); + var path = Path.Join(job.Path, job.MediaSource.Id + ".concat"); File.Delete(path); } } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 075e33cb82..3789bcac90 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -943,7 +943,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (state.MediaSource.VideoType == VideoType.Dvd || state.MediaSource.VideoType == VideoType.BluRay) { - var tmpConcatPath = Path.Join(options.TranscodingTempPath, "/" + state.MediaSource.Id + ".concat"); + var tmpConcatPath = Path.Join(options.TranscodingTempPath, state.MediaSource.Id + ".concat"); _mediaEncoder.GenerateConcatConfig(state.MediaSource, tmpConcatPath); arg.Append(" -f concat -safe 0 ") .Append(" -i ") diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 347e1de50a..49c81923b4 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -11,6 +11,7 @@ using System.Text.Json; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Extensions; using Jellyfin.Extensions.Json; using Jellyfin.Extensions.Json.Converters; using MediaBrowser.Common; @@ -896,7 +897,7 @@ namespace MediaBrowser.MediaEncoding.Encoder // Check for multiple big titles (> 900 MB) var titles = allVobs .Where(vob => vob.Length >= 900 * 1024 * 1024) - .Select(vob => _fileSystem.GetFileNameWithoutExtension(vob).Split('_')[1]) + .Select(vob => _fileSystem.GetFileNameWithoutExtension(vob).AsSpan().RightPart('_').ToString()) .GroupBy(x => x) .Select(y => y.First()) .ToList(); @@ -904,12 +905,12 @@ namespace MediaBrowser.MediaEncoding.Encoder // Fall back to first title if no big title is found if (titles.FirstOrDefault() == null) { - titles.Add(_fileSystem.GetFileNameWithoutExtension(allVobs[0]).Split('_')[1]); + titles.Add(_fileSystem.GetFileNameWithoutExtension(allVobs[0]).AsSpan().RightPart('_').ToString()); } // Aggregate all VOBs of the titles return allVobs - .Where(vob => titles.Contains(_fileSystem.GetFileNameWithoutExtension(vob).Split('_')[1])) + .Where(vob => titles.Contains(_fileSystem.GetFileNameWithoutExtension(vob).AsSpan().RightPart('_').ToString())) .Select(i => i.FullName) .ToList(); } @@ -917,7 +918,7 @@ namespace MediaBrowser.MediaEncoding.Encoder public IEnumerable<string> GetPrimaryPlaylistM2TsFiles(string path, uint? titleNumber) { var validPlaybackFiles = _blurayExaminer.GetDiscInfo(path).Files; - var directoryFiles = _fileSystem.GetFiles(path + "/BDMV/STREAM/"); + var directoryFiles = _fileSystem.GetFiles(Path.Join(path, "BDMV", "STREAM")); return directoryFiles .Where(f => validPlaybackFiles.Contains(f.Name, StringComparer.OrdinalIgnoreCase)) @@ -941,7 +942,6 @@ namespace MediaBrowser.MediaEncoding.Encoder foreach (var path in files) { - var fileinfo = _fileSystem.GetFileInfo(path); var mediaInfoResult = GetMediaInfo( new MediaInfoRequest { @@ -961,7 +961,7 @@ namespace MediaBrowser.MediaEncoding.Encoder lines.Add("duration " + duration); } - File.WriteAllLinesAsync(concatFilePath, lines, CancellationToken.None).GetAwaiter().GetResult(); + File.WriteAllLines(concatFilePath, lines); } public bool CanExtractSubtitles(string codec) diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index f6c930f5c8..ef73096b43 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -639,7 +639,6 @@ namespace MediaBrowser.Model.Dlna if (item.VideoType == VideoType.Dvd || item.VideoType == VideoType.BluRay) { isEligibleForDirectPlay = false; - isEligibleForDirectStream = false; } if (bitrateLimitExceeded) diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index 0ec7b368b4..f75de47f30 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -328,54 +328,56 @@ namespace MediaBrowser.Providers.MediaInfo { var video = (Video)item; - // Use BD Info if it has multiple m2ts. Otherwise, treat it like a video file and rely more on ffprobe output - if (blurayInfo.Files.Length > 1) + if (blurayInfo.Files.Length <= 1) { - int? currentHeight = null; - int? currentWidth = null; - int? currentBitRate = null; + return; + } - var videoStream = mediaStreams.FirstOrDefault(s => s.Type == MediaStreamType.Video); + // Use BD Info if it has multiple m2ts. Otherwise, treat it like a video file and rely more on ffprobe output + int? currentHeight = null; + int? currentWidth = null; + int? currentBitRate = null; - // Grab the values that ffprobe recorded - if (videoStream is not null) + var videoStream = mediaStreams.FirstOrDefault(s => s.Type == MediaStreamType.Video); + + // Grab the values that ffprobe recorded + if (videoStream is not null) + { + currentBitRate = videoStream.BitRate; + currentWidth = videoStream.Width; + currentHeight = videoStream.Height; + } + + // Fill video properties from the BDInfo result + mediaStreams.Clear(); + mediaStreams.AddRange(blurayInfo.MediaStreams); + + if (blurayInfo.RunTimeTicks.HasValue && blurayInfo.RunTimeTicks.Value > 0) + { + video.RunTimeTicks = blurayInfo.RunTimeTicks; + } + + if (blurayInfo.Chapters is not null) + { + double[] brChapter = blurayInfo.Chapters; + chapters = new ChapterInfo[brChapter.Length]; + for (int i = 0; i < brChapter.Length; i++) { - currentBitRate = videoStream.BitRate; - currentWidth = videoStream.Width; - currentHeight = videoStream.Height; - } - - // Fill video properties from the BDInfo result - mediaStreams.Clear(); - mediaStreams.AddRange(blurayInfo.MediaStreams); - - if (blurayInfo.RunTimeTicks.HasValue && blurayInfo.RunTimeTicks.Value > 0) - { - video.RunTimeTicks = blurayInfo.RunTimeTicks; - } - - if (blurayInfo.Chapters is not null) - { - double[] brChapter = blurayInfo.Chapters; - chapters = new ChapterInfo[brChapter.Length]; - for (int i = 0; i < brChapter.Length; i++) + chapters[i] = new ChapterInfo { - chapters[i] = new ChapterInfo - { - StartPositionTicks = TimeSpan.FromSeconds(brChapter[i]).Ticks - }; - } + StartPositionTicks = TimeSpan.FromSeconds(brChapter[i]).Ticks + }; } + } - videoStream = mediaStreams.FirstOrDefault(s => s.Type == MediaStreamType.Video); + videoStream = mediaStreams.FirstOrDefault(s => s.Type == MediaStreamType.Video); - // Use the ffprobe values if these are empty - if (videoStream is not null) - { - videoStream.BitRate = IsEmpty(videoStream.BitRate) ? currentBitRate : videoStream.BitRate; - videoStream.Width = IsEmpty(videoStream.Width) ? currentWidth : videoStream.Width; - videoStream.Height = IsEmpty(videoStream.Height) ? currentHeight : videoStream.Height; - } + // Use the ffprobe values if these are empty + if (videoStream is not null) + { + videoStream.BitRate = IsEmpty(videoStream.BitRate) ? currentBitRate : videoStream.BitRate; + videoStream.Width = IsEmpty(videoStream.Width) ? currentWidth : videoStream.Width; + videoStream.Height = IsEmpty(videoStream.Height) ? currentHeight : videoStream.Height; } } @@ -391,10 +393,7 @@ namespace MediaBrowser.Providers.MediaInfo /// <returns>VideoStream.</returns> private BlurayDiscInfo GetBDInfo(string path) { - if (string.IsNullOrWhiteSpace(path)) - { - throw new ArgumentNullException(nameof(path)); - } + ArgumentException.ThrowIfNullOrEmpty(path); try { From cd852d43c16f0e038ba547053bd4c80794ba990c Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Sun, 5 Feb 2023 22:59:58 +0100 Subject: [PATCH 150/858] Add more comments and logging, streamline code --- .../MediaEncoding/EncodingHelper.cs | 2 +- .../MediaEncoding/IMediaEncoder.cs | 3 +- .../Encoder/MediaEncoder.cs | 21 +++++++---- .../MediaInfo/FFProbeVideoInfo.cs | 35 +++++++++++++------ 4 files changed, 42 insertions(+), 19 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 3789bcac90..2b39c74e23 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -542,7 +542,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (state.MediaSource.VideoType == VideoType.BluRay) { - return _mediaEncoder.GetInputArgument(_mediaEncoder.GetPrimaryPlaylistM2TsFiles(state.MediaPath, null).ToList(), state.MediaSource); + return _mediaEncoder.GetInputArgument(_mediaEncoder.GetPrimaryPlaylistM2tsFiles(state.MediaPath).ToList(), state.MediaSource); } return _mediaEncoder.GetInputArgument(mediaPath, state.MediaSource); diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index 716adc8f0b..83be267a7a 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -208,9 +208,8 @@ namespace MediaBrowser.Controller.MediaEncoding /// Gets the primary playlist of .m2ts files. /// </summary> /// <param name="path">The to the .m2ts files.</param> - /// <param name="titleNumber">The title number to start with.</param> /// <returns>A playlist.</returns> - IEnumerable<string> GetPrimaryPlaylistM2TsFiles(string path, uint? titleNumber); + IEnumerable<string> GetPrimaryPlaylistM2tsFiles(string path); /// <summary> /// Generates a FFmpeg concat config for the source. diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 49c81923b4..05ea7a86d4 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -873,7 +873,7 @@ namespace MediaBrowser.MediaEncoding.Encoder /// <inheritdoc /> public IEnumerable<string> GetPrimaryPlaylistVobFiles(string path, uint? titleNumber) { - // Eliminate menus and intros by omitting VIDEO_TS.VOB and all subsequent title VOBs ending with _0.VOB + // Eliminate menus and intros by omitting VIDEO_TS.VOB and all subsequent title .vob files ending with _0.VOB var allVobs = _fileSystem.GetFiles(path, true) .Where(file => string.Equals(file.Extension, ".VOB", StringComparison.OrdinalIgnoreCase)) .Where(file => !string.Equals(file.Name, "VIDEO_TS.VOB", StringComparison.OrdinalIgnoreCase)) @@ -891,7 +891,7 @@ namespace MediaBrowser.MediaEncoding.Encoder return vobs.Select(i => i.FullName); } - _logger.LogWarning("Could not determine VOB file list for title {Title} of {Path}.", titleNumber, path); + _logger.LogWarning("Could not determine .vob files for title {Title} of {Path}.", titleNumber, path); } // Check for multiple big titles (> 900 MB) @@ -908,18 +908,22 @@ namespace MediaBrowser.MediaEncoding.Encoder titles.Add(_fileSystem.GetFileNameWithoutExtension(allVobs[0]).AsSpan().RightPart('_').ToString()); } - // Aggregate all VOBs of the titles + // Aggregate all .vob files of the titles return allVobs .Where(vob => titles.Contains(_fileSystem.GetFileNameWithoutExtension(vob).AsSpan().RightPart('_').ToString())) .Select(i => i.FullName) .ToList(); } - public IEnumerable<string> GetPrimaryPlaylistM2TsFiles(string path, uint? titleNumber) + public IEnumerable<string> GetPrimaryPlaylistM2tsFiles(string path) { + // Get all playable .m2ts files var validPlaybackFiles = _blurayExaminer.GetDiscInfo(path).Files; + + // Get all files from the BDMV/STREAMING directory var directoryFiles = _fileSystem.GetFiles(Path.Join(path, "BDMV", "STREAM")); + // Only return playable local .m2ts files return directoryFiles .Where(f => validPlaybackFiles.Contains(f.Name, StringComparer.OrdinalIgnoreCase)) .Select(f => f.FullName); @@ -927,6 +931,7 @@ namespace MediaBrowser.MediaEncoding.Encoder public void GenerateConcatConfig(MediaSourceInfo source, string concatFilePath) { + // Get all playable files var files = new List<string>(); var videoType = source.VideoType; if (videoType == VideoType.Dvd) @@ -935,11 +940,11 @@ namespace MediaBrowser.MediaEncoding.Encoder } else if (videoType == VideoType.BluRay) { - files = GetPrimaryPlaylistM2TsFiles(source.Path, null).ToList(); + files = GetPrimaryPlaylistM2tsFiles(source.Path).ToList(); } + // Generate concat configuration entries for each file var lines = new List<string>(); - foreach (var path in files) { var mediaInfoResult = GetMediaInfo( @@ -957,10 +962,14 @@ namespace MediaBrowser.MediaEncoding.Encoder var duration = TimeSpan.FromTicks(mediaInfoResult.RunTimeTicks.Value).TotalSeconds; + // Add file path stanza to concat configuration lines.Add("file " + "'" + path + "'"); + + // Add duration stanza to concat configuration lines.Add("duration " + duration); } + // Write concat configuration File.WriteAllLines(concatFilePath, lines); } diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index f75de47f30..7200d674c2 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -91,8 +91,17 @@ namespace MediaBrowser.Providers.MediaInfo { if (item.VideoType == VideoType.Dvd) { - // Fetch metadata of first VOB + // Get list of playable .vob files var vobs = _mediaEncoder.GetPrimaryPlaylistVobFiles(item.Path, null).ToList(); + + // Return if no playable .vob files are found + if (vobs.Count == 0) + { + _logger.LogError("No playable .vob files found in DVD structure, skipping FFprobe."); + return ItemUpdateType.MetadataImport; + } + + // Fetch metadata of first .vob file mediaInfoResult = await GetMediaInfo( new Video { @@ -100,10 +109,10 @@ namespace MediaBrowser.Providers.MediaInfo }, cancellationToken).ConfigureAwait(false); - // Remove first VOB + // Remove first .vob file vobs.RemoveAt(0); - // Add runtime from all other VOBs + // Sum up the runtime of all .vob files foreach (var vob in vobs) { var tmpMediaInfo = await GetMediaInfo( @@ -118,20 +127,26 @@ namespace MediaBrowser.Providers.MediaInfo } else if (item.VideoType == VideoType.BluRay) { + // Get BD disc information blurayDiscInfo = GetBDInfo(item.Path); - var m2ts = _mediaEncoder.GetPrimaryPlaylistM2TsFiles(item.Path, null).ToList(); + + // Get playable .m2ts files + var m2ts = _mediaEncoder.GetPrimaryPlaylistM2tsFiles(item.Path).ToList(); + + // Return if no playable .m2ts files are found + if (blurayDiscInfo.Files.Length == 0 || m2ts.Count == 0) + { + _logger.LogError("No playable .m2ts files found in Blu-ray structure, skipping FFprobe."); + return ItemUpdateType.MetadataImport; + } + + // Fetch metadata of first .m2ts file mediaInfoResult = await GetMediaInfo( new Video { Path = m2ts.First() }, cancellationToken).ConfigureAwait(false); - - if (blurayDiscInfo.Files.Length == 0) - { - _logger.LogError("No playable vobs found in bluray structure, skipping ffprobe."); - return ItemUpdateType.MetadataImport; - } } else { From 47aa07c3424ce0041e0a79eea1ab7f6621485b94 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Fri, 17 Feb 2023 00:26:03 +0100 Subject: [PATCH 151/858] Fix DLNA playback of DVD and BD folders --- Jellyfin.Api/Helpers/TranscodingJobHelper.cs | 8 ++++++-- .../Probing/ProbeResultNormalizer.cs | 13 ++++++++++++- MediaBrowser.Model/Dlna/StreamInfo.cs | 6 ++++-- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index 3bb3ad358f..ee210117e6 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -325,8 +325,12 @@ public class TranscodingJobHelper : IDisposable await DeletePartialStreamFiles(job.Path!, job.Type, 0, 1500).ConfigureAwait(false); if (job.MediaSource?.VideoType == VideoType.Dvd || job.MediaSource?.VideoType == VideoType.BluRay) { - var path = Path.Join(job.Path, job.MediaSource.Id + ".concat"); - File.Delete(path); + var concatFilePath = Path.Join(_serverConfigurationManager.GetTranscodePath(), job.MediaSource.Id + ".concat"); + if (File.Exists(concatFilePath)) + { + _logger.LogInformation("Deleting ffmpeg concat configuration at {Path}", concatFilePath); + _fileSystem.DeleteFile(concatFilePath); + } } } diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 99310a75dd..dc15e169fa 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -251,12 +251,23 @@ namespace MediaBrowser.MediaEncoding.Probing return null; } + // Handle MPEG-1 container if (string.Equals(format, "mpegvideo", StringComparison.OrdinalIgnoreCase)) { return "mpeg"; } - format = format.Replace("matroska", "mkv", StringComparison.OrdinalIgnoreCase); + // Handle MPEG-2 container + if (string.Equals(format, "mpeg", StringComparison.OrdinalIgnoreCase)) + { + return "ts"; + } + + // Handle matroska container + if (string.Equals(format, "matroska", StringComparison.OrdinalIgnoreCase)) + { + return "mkv"; + } return format; } diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index 3b55099079..fdf3afe973 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -108,8 +108,10 @@ namespace MediaBrowser.Model.Dlna public string MediaSourceId => MediaSource?.Id; public bool IsDirectStream => - PlayMethod == PlayMethod.DirectStream || - PlayMethod == PlayMethod.DirectPlay; + !(MediaSource?.VideoType == VideoType.Dvd + || MediaSource?.VideoType == VideoType.BluRay) + && (PlayMethod == PlayMethod.DirectStream + || PlayMethod == PlayMethod.DirectPlay); /// <summary> /// Gets the audio stream that will be used. From 0da5255f1291ba510f829d36a3ca1a9eb65590dc Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Sat, 18 Feb 2023 14:42:35 +0100 Subject: [PATCH 152/858] Apply review suggestions --- Jellyfin.Api/Helpers/TranscodingJobHelper.cs | 2 +- .../MediaEncoding/EncodingHelper.cs | 23 +- .../MediaEncoding/IMediaEncoder.cs | 4 +- .../BdInfo/BdInfoDirectoryInfo.cs | 174 ++++++---- .../BdInfo/BdInfoExaminer.cs | 317 +++++++++--------- .../BdInfo/BdInfoFileInfo.cs | 81 +++-- .../Encoder/MediaEncoder.cs | 77 +++-- MediaBrowser.Model/Dlna/StreamInfo.cs | 7 +- .../MediaInfo/BlurayDiscInfo.cs | 56 ++-- .../MediaInfo/IBlurayExaminer.cs | 21 +- .../MediaInfo/FFProbeVideoInfo.cs | 30 +- 11 files changed, 420 insertions(+), 372 deletions(-) diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index ee210117e6..73e8f34ada 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -329,7 +329,7 @@ public class TranscodingJobHelper : IDisposable if (File.Exists(concatFilePath)) { _logger.LogInformation("Deleting ffmpeg concat configuration at {Path}", concatFilePath); - _fileSystem.DeleteFile(concatFilePath); + File.Delete(concatFilePath); } } } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 2b39c74e23..a4e4648b10 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -533,19 +533,12 @@ namespace MediaBrowser.Controller.MediaEncoding public string GetInputPathArgument(EncodingJobInfo state) { - var mediaPath = state.MediaPath ?? string.Empty; - - if (state.MediaSource.VideoType == VideoType.Dvd) + return state.MediaSource.VideoType switch { - return _mediaEncoder.GetInputArgument(_mediaEncoder.GetPrimaryPlaylistVobFiles(state.MediaPath, null).ToList(), state.MediaSource); - } - - if (state.MediaSource.VideoType == VideoType.BluRay) - { - return _mediaEncoder.GetInputArgument(_mediaEncoder.GetPrimaryPlaylistM2tsFiles(state.MediaPath).ToList(), state.MediaSource); - } - - return _mediaEncoder.GetInputArgument(mediaPath, state.MediaSource); + VideoType.Dvd => _mediaEncoder.GetInputArgument(_mediaEncoder.GetPrimaryPlaylistVobFiles(state.MediaPath, null).ToList(), state.MediaSource), + VideoType.BluRay => _mediaEncoder.GetInputArgument(_mediaEncoder.GetPrimaryPlaylistM2tsFiles(state.MediaPath).ToList(), state.MediaSource), + _ => _mediaEncoder.GetInputArgument(state.MediaPath, state.MediaSource) + }; } /// <summary> @@ -945,10 +938,8 @@ namespace MediaBrowser.Controller.MediaEncoding { var tmpConcatPath = Path.Join(options.TranscodingTempPath, state.MediaSource.Id + ".concat"); _mediaEncoder.GenerateConcatConfig(state.MediaSource, tmpConcatPath); - arg.Append(" -f concat -safe 0 ") - .Append(" -i ") - .Append(tmpConcatPath) - .Append(' '); + arg.Append(" -f concat -safe 0 -i ") + .Append(tmpConcatPath); } else { diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index 83be267a7a..f830b9f298 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -202,14 +202,14 @@ namespace MediaBrowser.Controller.MediaEncoding /// <param name="path">The to the .vob files.</param> /// <param name="titleNumber">The title number to start with.</param> /// <returns>A playlist.</returns> - IEnumerable<string> GetPrimaryPlaylistVobFiles(string path, uint? titleNumber); + IReadOnlyList<string> GetPrimaryPlaylistVobFiles(string path, uint? titleNumber); /// <summary> /// Gets the primary playlist of .m2ts files. /// </summary> /// <param name="path">The to the .m2ts files.</param> /// <returns>A playlist.</returns> - IEnumerable<string> GetPrimaryPlaylistM2tsFiles(string path); + IReadOnlyList<string> GetPrimaryPlaylistM2tsFiles(string path); /// <summary> /// Generates a FFmpeg concat config for the source. diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs index 7e026b42e3..ea520b1d6b 100644 --- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs +++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs @@ -1,83 +1,123 @@ -#pragma warning disable CS1591 - using System; +using System.IO; using System.Linq; using BDInfo.IO; using MediaBrowser.Model.IO; -namespace MediaBrowser.MediaEncoding.BdInfo +namespace MediaBrowser.MediaEncoding.BdInfo; + +/// <summary> +/// Class BdInfoDirectoryInfo. +/// </summary> +public class BdInfoDirectoryInfo : IDirectoryInfo { - public class BdInfoDirectoryInfo : IDirectoryInfo + private readonly IFileSystem _fileSystem; + + private readonly FileSystemMetadata _impl; + + /// <summary> + /// Initializes a new instance of the <see cref="BdInfoDirectoryInfo" /> class. + /// </summary> + /// <param name="fileSystem">The filesystem.</param> + /// <param name="path">The path.</param> + public BdInfoDirectoryInfo(IFileSystem fileSystem, string path) { - private readonly IFileSystem _fileSystem; + _fileSystem = fileSystem; + _impl = _fileSystem.GetDirectoryInfo(path); + } - private readonly FileSystemMetadata _impl; + private BdInfoDirectoryInfo(IFileSystem fileSystem, FileSystemMetadata impl) + { + _fileSystem = fileSystem; + _impl = impl; + } - public BdInfoDirectoryInfo(IFileSystem fileSystem, string path) + /// <summary> + /// Gets the name. + /// </summary> + public string Name => _impl.Name; + + /// <summary> + /// Gets the full name. + /// </summary> + public string FullName => _impl.FullName; + + /// <summary> + /// Gets the parent directory information. + /// </summary> + public IDirectoryInfo? Parent + { + get { - _fileSystem = fileSystem; - _impl = _fileSystem.GetDirectoryInfo(path); - } - - private BdInfoDirectoryInfo(IFileSystem fileSystem, FileSystemMetadata impl) - { - _fileSystem = fileSystem; - _impl = impl; - } - - public string Name => _impl.Name; - - public string FullName => _impl.FullName; - - public IDirectoryInfo? Parent - { - get + var parentFolder = Path.GetDirectoryName(_impl.FullName); + if (parentFolder is not null) { - var parentFolder = System.IO.Path.GetDirectoryName(_impl.FullName); - if (parentFolder is not null) - { - return new BdInfoDirectoryInfo(_fileSystem, parentFolder); - } - - return null; + return new BdInfoDirectoryInfo(_fileSystem, parentFolder); } - } - public IDirectoryInfo[] GetDirectories() - { - return Array.ConvertAll( - _fileSystem.GetDirectories(_impl.FullName).ToArray(), - x => new BdInfoDirectoryInfo(_fileSystem, x)); - } - - public IFileInfo[] GetFiles() - { - return Array.ConvertAll( - _fileSystem.GetFiles(_impl.FullName).ToArray(), - x => new BdInfoFileInfo(x)); - } - - public IFileInfo[] GetFiles(string searchPattern) - { - return Array.ConvertAll( - _fileSystem.GetFiles(_impl.FullName, new[] { searchPattern }, false, false).ToArray(), - x => new BdInfoFileInfo(x)); - } - - public IFileInfo[] GetFiles(string searchPattern, System.IO.SearchOption searchOption) - { - return Array.ConvertAll( - _fileSystem.GetFiles( - _impl.FullName, - new[] { searchPattern }, - false, - (searchOption & System.IO.SearchOption.AllDirectories) == System.IO.SearchOption.AllDirectories).ToArray(), - x => new BdInfoFileInfo(x)); - } - - public static IDirectoryInfo FromFileSystemPath(IFileSystem fs, string path) - { - return new BdInfoDirectoryInfo(fs, path); + return null; } } + + /// <summary> + /// Gets the directories. + /// </summary> + /// <returns>An array with all directories.</returns> + public IDirectoryInfo[] GetDirectories() + { + return _fileSystem.GetDirectories(_impl.FullName) + .Select(x => new BdInfoDirectoryInfo(_fileSystem, x)) + .ToArray(); + } + + /// <summary> + /// Gets the files. + /// </summary> + /// <returns>All files of the directory.</returns> + public IFileInfo[] GetFiles() + { + return _fileSystem.GetFiles(_impl.FullName) + .Select(x => new BdInfoFileInfo(x)) + .ToArray(); + } + + /// <summary> + /// Gets the files matching a pattern. + /// </summary> + /// <param name="searchPattern">The search pattern.</param> + /// <returns>All files of the directory matchign the search pattern.</returns> + public IFileInfo[] GetFiles(string searchPattern) + { + return _fileSystem.GetFiles(_impl.FullName, new[] { searchPattern }, false, false) + .Select(x => new BdInfoFileInfo(x)) + .ToArray(); + } + + /// <summary> + /// Gets the files matching a pattern and search options. + /// </summary> + /// <param name="searchPattern">The search pattern.</param> + /// <param name="searchOption">The search optin.</param> + /// <returns>All files of the directory matchign the search pattern and options.</returns> + public IFileInfo[] GetFiles(string searchPattern, SearchOption searchOption) + { + return _fileSystem.GetFiles( + _impl.FullName, + new[] { searchPattern }, + false, + (searchOption & SearchOption.AllDirectories) == SearchOption.AllDirectories) + .Select(x => new BdInfoFileInfo(x)) + .ToArray(); + } + + /// <summary> + /// Gets the bdinfo of a file system path. + /// </summary> + /// <param name="fs">The file system.</param> + /// <param name="path">The path.</param> + /// <returns>The BD directory information of the path on the file system.</returns> + public static IDirectoryInfo FromFileSystemPath(IFileSystem fs, string path) + { + return new BdInfoDirectoryInfo(fs, path); + } } diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs index 3e53cbf29f..8ebb59c59e 100644 --- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs +++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs @@ -6,189 +6,182 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; -namespace MediaBrowser.MediaEncoding.BdInfo -{ - /// <summary> - /// Class BdInfoExaminer. - /// </summary> - public class BdInfoExaminer : IBlurayExaminer - { - private readonly IFileSystem _fileSystem; +namespace MediaBrowser.MediaEncoding.BdInfo; - /// <summary> - /// Initializes a new instance of the <see cref="BdInfoExaminer" /> class. - /// </summary> - /// <param name="fileSystem">The filesystem.</param> - public BdInfoExaminer(IFileSystem fileSystem) +/// <summary> +/// Class BdInfoExaminer. +/// </summary> +public class BdInfoExaminer : IBlurayExaminer +{ + private readonly IFileSystem _fileSystem; + + /// <summary> + /// Initializes a new instance of the <see cref="BdInfoExaminer" /> class. + /// </summary> + /// <param name="fileSystem">The filesystem.</param> + public BdInfoExaminer(IFileSystem fileSystem) + { + _fileSystem = fileSystem; + } + + /// <summary> + /// Gets the disc info. + /// </summary> + /// <param name="path">The path.</param> + /// <returns>BlurayDiscInfo.</returns> + public BlurayDiscInfo GetDiscInfo(string path) + { + if (string.IsNullOrWhiteSpace(path)) { - _fileSystem = fileSystem; + throw new ArgumentNullException(nameof(path)); } - /// <summary> - /// Gets the disc info. - /// </summary> - /// <param name="path">The path.</param> - /// <returns>BlurayDiscInfo.</returns> - public BlurayDiscInfo GetDiscInfo(string path) + var bdrom = new BDROM(BdInfoDirectoryInfo.FromFileSystemPath(_fileSystem, path)); + + bdrom.Scan(); + + // Get the longest playlist + var playlist = bdrom.PlaylistFiles.Values.OrderByDescending(p => p.TotalLength).FirstOrDefault(p => p.IsValid); + + var outputStream = new BlurayDiscInfo { - if (string.IsNullOrWhiteSpace(path)) - { - throw new ArgumentNullException(nameof(path)); - } - - var bdrom = new BDROM(BdInfoDirectoryInfo.FromFileSystemPath(_fileSystem, path)); - - bdrom.Scan(); - - // Get the longest playlist - var playlist = bdrom.PlaylistFiles.Values.OrderByDescending(p => p.TotalLength).FirstOrDefault(p => p.IsValid); - - var outputStream = new BlurayDiscInfo - { - MediaStreams = Array.Empty<MediaStream>() - }; - - if (playlist is null) - { - return outputStream; - } - - outputStream.Chapters = playlist.Chapters.ToArray(); - - outputStream.RunTimeTicks = TimeSpan.FromSeconds(playlist.TotalLength).Ticks; - - var mediaStreams = new List<MediaStream>(); - - foreach (var stream in playlist.SortedStreams) - { - if (stream is TSVideoStream videoStream) - { - AddVideoStream(mediaStreams, videoStream); - continue; - } - - if (stream is TSAudioStream audioStream) - { - AddAudioStream(mediaStreams, audioStream); - continue; - } - - if (stream is TSTextStream textStream) - { - AddSubtitleStream(mediaStreams, textStream); - continue; - } - - if (stream is TSGraphicsStream graphicsStream) - { - AddSubtitleStream(mediaStreams, graphicsStream); - } - } - - outputStream.MediaStreams = mediaStreams.ToArray(); - - outputStream.PlaylistName = playlist.Name; - - if (playlist.StreamClips is not null && playlist.StreamClips.Any()) - { - // Get the files in the playlist - outputStream.Files = playlist.StreamClips.Select(i => i.StreamFile.Name).ToArray(); - } + MediaStreams = Array.Empty<MediaStream>() + }; + if (playlist is null) + { return outputStream; } - /// <summary> - /// Adds the video stream. - /// </summary> - /// <param name="streams">The streams.</param> - /// <param name="videoStream">The video stream.</param> - private void AddVideoStream(List<MediaStream> streams, TSVideoStream videoStream) + outputStream.Chapters = playlist.Chapters.ToArray(); + + outputStream.RunTimeTicks = TimeSpan.FromSeconds(playlist.TotalLength).Ticks; + + var sortedStreams = playlist.SortedStreams; + var mediaStreams = new List<MediaStream>(sortedStreams.Count); + + foreach (var stream in sortedStreams) { - var mediaStream = new MediaStream + switch (stream) { - BitRate = Convert.ToInt32(videoStream.BitRate), - Width = videoStream.Width, - Height = videoStream.Height, - Codec = videoStream.CodecShortName, - IsInterlaced = videoStream.IsInterlaced, - Type = MediaStreamType.Video, - Index = streams.Count - }; - - if (videoStream.FrameRateDenominator > 0) - { - float frameRateEnumerator = videoStream.FrameRateEnumerator; - float frameRateDenominator = videoStream.FrameRateDenominator; - - mediaStream.AverageFrameRate = mediaStream.RealFrameRate = frameRateEnumerator / frameRateDenominator; + case TSVideoStream videoStream: + AddVideoStream(mediaStreams, videoStream); + break; + case TSAudioStream audioStream: + AddAudioStream(mediaStreams, audioStream); + break; + case TSTextStream textStream: + AddSubtitleStream(mediaStreams, textStream); + break; + case TSGraphicsStream graphicStream: + AddSubtitleStream(mediaStreams, graphicStream); + break; } - - streams.Add(mediaStream); } - /// <summary> - /// Adds the audio stream. - /// </summary> - /// <param name="streams">The streams.</param> - /// <param name="audioStream">The audio stream.</param> - private void AddAudioStream(List<MediaStream> streams, TSAudioStream audioStream) + outputStream.MediaStreams = mediaStreams.ToArray(); + + outputStream.PlaylistName = playlist.Name; + + if (playlist.StreamClips is not null && playlist.StreamClips.Count > 0) { - var stream = new MediaStream - { - Codec = audioStream.CodecShortName, - Language = audioStream.LanguageCode, - Channels = audioStream.ChannelCount, - SampleRate = audioStream.SampleRate, - Type = MediaStreamType.Audio, - Index = streams.Count - }; - - var bitrate = Convert.ToInt32(audioStream.BitRate); - - if (bitrate > 0) - { - stream.BitRate = bitrate; - } - - if (audioStream.LFE > 0) - { - stream.Channels = audioStream.ChannelCount + 1; - } - - streams.Add(stream); + // Get the files in the playlist + outputStream.Files = playlist.StreamClips.Select(i => i.StreamFile.Name).ToArray(); } - /// <summary> - /// Adds the subtitle stream. - /// </summary> - /// <param name="streams">The streams.</param> - /// <param name="textStream">The text stream.</param> - private void AddSubtitleStream(List<MediaStream> streams, TSTextStream textStream) + return outputStream; + } + + /// <summary> + /// Adds the video stream. + /// </summary> + /// <param name="streams">The streams.</param> + /// <param name="videoStream">The video stream.</param> + private void AddVideoStream(List<MediaStream> streams, TSVideoStream videoStream) + { + var mediaStream = new MediaStream { - streams.Add(new MediaStream - { - Language = textStream.LanguageCode, - Codec = textStream.CodecShortName, - Type = MediaStreamType.Subtitle, - Index = streams.Count - }); + BitRate = Convert.ToInt32(videoStream.BitRate), + Width = videoStream.Width, + Height = videoStream.Height, + Codec = videoStream.CodecShortName, + IsInterlaced = videoStream.IsInterlaced, + Type = MediaStreamType.Video, + Index = streams.Count + }; + + if (videoStream.FrameRateDenominator > 0) + { + float frameRateEnumerator = videoStream.FrameRateEnumerator; + float frameRateDenominator = videoStream.FrameRateDenominator; + + mediaStream.AverageFrameRate = mediaStream.RealFrameRate = frameRateEnumerator / frameRateDenominator; } - /// <summary> - /// Adds the subtitle stream. - /// </summary> - /// <param name="streams">The streams.</param> - /// <param name="textStream">The text stream.</param> - private void AddSubtitleStream(List<MediaStream> streams, TSGraphicsStream textStream) + streams.Add(mediaStream); + } + + /// <summary> + /// Adds the audio stream. + /// </summary> + /// <param name="streams">The streams.</param> + /// <param name="audioStream">The audio stream.</param> + private void AddAudioStream(List<MediaStream> streams, TSAudioStream audioStream) + { + var stream = new MediaStream { - streams.Add(new MediaStream - { - Language = textStream.LanguageCode, - Codec = textStream.CodecShortName, - Type = MediaStreamType.Subtitle, - Index = streams.Count - }); + Codec = audioStream.CodecShortName, + Language = audioStream.LanguageCode, + Channels = audioStream.ChannelCount, + SampleRate = audioStream.SampleRate, + Type = MediaStreamType.Audio, + Index = streams.Count + }; + + var bitrate = Convert.ToInt32(audioStream.BitRate); + + if (bitrate > 0) + { + stream.BitRate = bitrate; } + + if (audioStream.LFE > 0) + { + stream.Channels = audioStream.ChannelCount + 1; + } + + streams.Add(stream); + } + + /// <summary> + /// Adds the subtitle stream. + /// </summary> + /// <param name="streams">The streams.</param> + /// <param name="textStream">The text stream.</param> + private void AddSubtitleStream(List<MediaStream> streams, TSTextStream textStream) + { + streams.Add(new MediaStream + { + Language = textStream.LanguageCode, + Codec = textStream.CodecShortName, + Type = MediaStreamType.Subtitle, + Index = streams.Count + }); + } + + /// <summary> + /// Adds the subtitle stream. + /// </summary> + /// <param name="streams">The streams.</param> + /// <param name="textStream">The text stream.</param> + private void AddSubtitleStream(List<MediaStream> streams, TSGraphicsStream textStream) + { + streams.Add(new MediaStream + { + Language = textStream.LanguageCode, + Codec = textStream.CodecShortName, + Type = MediaStreamType.Subtitle, + Index = streams.Count + }); } } diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs index d55688e3df..9e7a1d50a3 100644 --- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs +++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs @@ -1,41 +1,68 @@ -#pragma warning disable CS1591 - using System.IO; using MediaBrowser.Model.IO; -namespace MediaBrowser.MediaEncoding.BdInfo +namespace MediaBrowser.MediaEncoding.BdInfo; + +/// <summary> +/// Class BdInfoFileInfo. +/// </summary> +public class BdInfoFileInfo : BDInfo.IO.IFileInfo { - public class BdInfoFileInfo : BDInfo.IO.IFileInfo + private FileSystemMetadata _impl; + + /// <summary> + /// Initializes a new instance of the <see cref="BdInfoFileInfo" /> class. + /// </summary> + /// <param name="impl">The <see cref="FileSystemMetadata" />.</param> + public BdInfoFileInfo(FileSystemMetadata impl) { - private FileSystemMetadata _impl; + _impl = impl; + } - public BdInfoFileInfo(FileSystemMetadata impl) - { - _impl = impl; - } + /// <summary> + /// Gets the name. + /// </summary> + public string Name => _impl.Name; - public string Name => _impl.Name; + /// <summary> + /// Gets the full name. + /// </summary> + public string FullName => _impl.FullName; - public string FullName => _impl.FullName; + /// <summary> + /// Gets the extension. + /// </summary> + public string Extension => _impl.Extension; - public string Extension => _impl.Extension; + /// <summary> + /// Gets the length. + /// </summary> + public long Length => _impl.Length; - public long Length => _impl.Length; + /// <summary> + /// Gets a value indicating whether this is a directory. + /// </summary> + public bool IsDir => _impl.IsDirectory; - public bool IsDir => _impl.IsDirectory; + /// <summary> + /// Gets a file as file stream. + /// </summary> + /// <returns>A <see cref="FileStream" /> for the file.</returns> + public Stream OpenRead() + { + return new FileStream( + FullName, + FileMode.Open, + FileAccess.Read, + FileShare.Read); + } - public Stream OpenRead() - { - return new FileStream( - FullName, - FileMode.Open, - FileAccess.Read, - FileShare.Read); - } - - public StreamReader OpenText() - { - return new StreamReader(OpenRead()); - } + /// <summary> + /// Gets a files's content with a stream reader. + /// </summary> + /// <returns>A <see cref="StreamReader" /> for the file's content.</returns> + public StreamReader OpenText() + { + return new StreamReader(OpenRead()); } } diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 05ea7a86d4..d2112e5dc2 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -871,7 +871,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } /// <inheritdoc /> - public IEnumerable<string> GetPrimaryPlaylistVobFiles(string path, uint? titleNumber) + public IReadOnlyList<string> GetPrimaryPlaylistVobFiles(string path, uint? titleNumber) { // Eliminate menus and intros by omitting VIDEO_TS.VOB and all subsequent title .vob files ending with _0.VOB var allVobs = _fileSystem.GetFiles(path, true) @@ -888,7 +888,7 @@ namespace MediaBrowser.MediaEncoding.Encoder if (vobs.Count > 0) { - return vobs.Select(i => i.FullName); + return vobs.Select(i => i.FullName).ToList(); } _logger.LogWarning("Could not determine .vob files for title {Title} of {Path}.", titleNumber, path); @@ -898,12 +898,11 @@ namespace MediaBrowser.MediaEncoding.Encoder var titles = allVobs .Where(vob => vob.Length >= 900 * 1024 * 1024) .Select(vob => _fileSystem.GetFileNameWithoutExtension(vob).AsSpan().RightPart('_').ToString()) - .GroupBy(x => x) - .Select(y => y.First()) + .Distinct() .ToList(); // Fall back to first title if no big title is found - if (titles.FirstOrDefault() == null) + if (titles.Count == 0) { titles.Add(_fileSystem.GetFileNameWithoutExtension(allVobs[0]).AsSpan().RightPart('_').ToString()); } @@ -915,7 +914,8 @@ namespace MediaBrowser.MediaEncoding.Encoder .ToList(); } - public IEnumerable<string> GetPrimaryPlaylistM2tsFiles(string path) + /// <inheritdoc /> + public IReadOnlyList<string> GetPrimaryPlaylistM2tsFiles(string path) { // Get all playable .m2ts files var validPlaybackFiles = _blurayExaminer.GetDiscInfo(path).Files; @@ -926,51 +926,56 @@ namespace MediaBrowser.MediaEncoding.Encoder // Only return playable local .m2ts files return directoryFiles .Where(f => validPlaybackFiles.Contains(f.Name, StringComparer.OrdinalIgnoreCase)) - .Select(f => f.FullName); + .Select(f => f.FullName) + .ToList(); } + /// <inheritdoc /> public void GenerateConcatConfig(MediaSourceInfo source, string concatFilePath) { // Get all playable files - var files = new List<string>(); + IReadOnlyList<string> files; var videoType = source.VideoType; if (videoType == VideoType.Dvd) { - files = GetPrimaryPlaylistVobFiles(source.Path, null).ToList(); + files = GetPrimaryPlaylistVobFiles(source.Path, null); } else if (videoType == VideoType.BluRay) { - files = GetPrimaryPlaylistM2tsFiles(source.Path).ToList(); + files = GetPrimaryPlaylistM2tsFiles(source.Path); } - - // Generate concat configuration entries for each file - var lines = new List<string>(); - foreach (var path in files) + else { - var mediaInfoResult = GetMediaInfo( - new MediaInfoRequest - { - MediaType = DlnaProfileType.Video, - MediaSource = new MediaSourceInfo - { - Path = path, - Protocol = MediaProtocol.File, - VideoType = videoType - } - }, - CancellationToken.None).GetAwaiter().GetResult(); - - var duration = TimeSpan.FromTicks(mediaInfoResult.RunTimeTicks.Value).TotalSeconds; - - // Add file path stanza to concat configuration - lines.Add("file " + "'" + path + "'"); - - // Add duration stanza to concat configuration - lines.Add("duration " + duration); + return; } - // Write concat configuration - File.WriteAllLines(concatFilePath, lines); + // Generate concat configuration entries for each file and write to file + using (StreamWriter sw = new StreamWriter(concatFilePath)) + { + foreach (var path in files) + { + var mediaInfoResult = GetMediaInfo( + new MediaInfoRequest + { + MediaType = DlnaProfileType.Video, + MediaSource = new MediaSourceInfo + { + Path = path, + Protocol = MediaProtocol.File, + VideoType = videoType + } + }, + CancellationToken.None).GetAwaiter().GetResult(); + + var duration = TimeSpan.FromTicks(mediaInfoResult.RunTimeTicks.Value).TotalSeconds; + + // Add file path stanza to concat configuration + sw.WriteLine("file '{0}'", path); + + // Add duration stanza to concat configuration + sw.WriteLine("duration {0}", duration); + } + } } public bool CanExtractSubtitles(string codec) diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index fdf3afe973..0e814f036e 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -107,11 +107,8 @@ namespace MediaBrowser.Model.Dlna public string MediaSourceId => MediaSource?.Id; - public bool IsDirectStream => - !(MediaSource?.VideoType == VideoType.Dvd - || MediaSource?.VideoType == VideoType.BluRay) - && (PlayMethod == PlayMethod.DirectStream - || PlayMethod == PlayMethod.DirectPlay); + public bool IsDirectStream => MediaSource?.VideoType is not (VideoType.Dvd or VideoType.BluRay) + && PlayMethod is PlayMethod.DirectStream or PlayMethod.DirectPlay; /// <summary> /// Gets the audio stream that will be used. diff --git a/MediaBrowser.Model/MediaInfo/BlurayDiscInfo.cs b/MediaBrowser.Model/MediaInfo/BlurayDiscInfo.cs index 83f982a5c8..d546ffccdc 100644 --- a/MediaBrowser.Model/MediaInfo/BlurayDiscInfo.cs +++ b/MediaBrowser.Model/MediaInfo/BlurayDiscInfo.cs @@ -1,39 +1,41 @@ #nullable disable -#pragma warning disable CS1591 using MediaBrowser.Model.Entities; -namespace MediaBrowser.Model.MediaInfo +namespace MediaBrowser.Model.MediaInfo; + +/// <summary> +/// Represents the result of BDInfo output. +/// </summary> +public class BlurayDiscInfo { /// <summary> - /// Represents the result of BDInfo output. + /// Gets or sets the media streams. /// </summary> - public class BlurayDiscInfo - { - /// <summary> - /// Gets or sets the media streams. - /// </summary> - /// <value>The media streams.</value> - public MediaStream[] MediaStreams { get; set; } + /// <value>The media streams.</value> + public MediaStream[] MediaStreams { get; set; } - /// <summary> - /// Gets or sets the run time ticks. - /// </summary> - /// <value>The run time ticks.</value> - public long? RunTimeTicks { get; set; } + /// <summary> + /// Gets or sets the run time ticks. + /// </summary> + /// <value>The run time ticks.</value> + public long? RunTimeTicks { get; set; } - /// <summary> - /// Gets or sets the files. - /// </summary> - /// <value>The files.</value> - public string[] Files { get; set; } + /// <summary> + /// Gets or sets the files. + /// </summary> + /// <value>The files.</value> + public string[] Files { get; set; } - public string PlaylistName { get; set; } + /// <summary> + /// Gets or sets the playlist name. + /// </summary> + /// <value>The playlist name.</value> + public string PlaylistName { get; set; } - /// <summary> - /// Gets or sets the chapters. - /// </summary> - /// <value>The chapters.</value> - public double[] Chapters { get; set; } - } + /// <summary> + /// Gets or sets the chapters. + /// </summary> + /// <value>The chapters.</value> + public double[] Chapters { get; set; } } diff --git a/MediaBrowser.Model/MediaInfo/IBlurayExaminer.cs b/MediaBrowser.Model/MediaInfo/IBlurayExaminer.cs index 5b7d1d03c3..d397253010 100644 --- a/MediaBrowser.Model/MediaInfo/IBlurayExaminer.cs +++ b/MediaBrowser.Model/MediaInfo/IBlurayExaminer.cs @@ -1,15 +1,14 @@ -namespace MediaBrowser.Model.MediaInfo +namespace MediaBrowser.Model.MediaInfo; + +/// <summary> +/// Interface IBlurayExaminer. +/// </summary> +public interface IBlurayExaminer { /// <summary> - /// Interface IBlurayExaminer. + /// Gets the disc info. /// </summary> - public interface IBlurayExaminer - { - /// <summary> - /// Gets the disc info. - /// </summary> - /// <param name="path">The path.</param> - /// <returns>BlurayDiscInfo.</returns> - BlurayDiscInfo GetDiscInfo(string path); - } + /// <param name="path">The path.</param> + /// <returns>BlurayDiscInfo.</returns> + BlurayDiscInfo GetDiscInfo(string path); } diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index 7200d674c2..e199db7f2a 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -92,7 +92,7 @@ namespace MediaBrowser.Providers.MediaInfo if (item.VideoType == VideoType.Dvd) { // Get list of playable .vob files - var vobs = _mediaEncoder.GetPrimaryPlaylistVobFiles(item.Path, null).ToList(); + var vobs = _mediaEncoder.GetPrimaryPlaylistVobFiles(item.Path, null); // Return if no playable .vob files are found if (vobs.Count == 0) @@ -105,22 +105,19 @@ namespace MediaBrowser.Providers.MediaInfo mediaInfoResult = await GetMediaInfo( new Video { - Path = vobs.First() + Path = vobs[0] }, cancellationToken).ConfigureAwait(false); - // Remove first .vob file - vobs.RemoveAt(0); - - // Sum up the runtime of all .vob files - foreach (var vob in vobs) + // Sum up the runtime of all .vob files skipping the first .vob + for (var i = 1; i < vobs.Count; i++) { var tmpMediaInfo = await GetMediaInfo( - new Video - { - Path = vob - }, - cancellationToken).ConfigureAwait(false); + new Video + { + Path = vobs[i] + }, + cancellationToken).ConfigureAwait(false); mediaInfoResult.RunTimeTicks += tmpMediaInfo.RunTimeTicks; } @@ -131,7 +128,7 @@ namespace MediaBrowser.Providers.MediaInfo blurayDiscInfo = GetBDInfo(item.Path); // Get playable .m2ts files - var m2ts = _mediaEncoder.GetPrimaryPlaylistM2tsFiles(item.Path).ToList(); + var m2ts = _mediaEncoder.GetPrimaryPlaylistM2tsFiles(item.Path); // Return if no playable .m2ts files are found if (blurayDiscInfo.Files.Length == 0 || m2ts.Count == 0) @@ -144,14 +141,13 @@ namespace MediaBrowser.Providers.MediaInfo mediaInfoResult = await GetMediaInfo( new Video { - Path = m2ts.First() + Path = m2ts[0] }, cancellationToken).ConfigureAwait(false); } else { mediaInfoResult = await GetMediaInfo(item, cancellationToken).ConfigureAwait(false); - cancellationToken.ThrowIfCancellationRequested(); } cancellationToken.ThrowIfCancellationRequested(); @@ -339,10 +335,8 @@ namespace MediaBrowser.Providers.MediaInfo } } - private void FetchBdInfo(BaseItem item, ref ChapterInfo[] chapters, List<MediaStream> mediaStreams, BlurayDiscInfo blurayInfo) + private void FetchBdInfo(Video video, ref ChapterInfo[] chapters, List<MediaStream> mediaStreams, BlurayDiscInfo blurayInfo) { - var video = (Video)item; - if (blurayInfo.Files.Length <= 1) { return; From 369c7f14514c19b2ab4948be26446e8e12ca78d7 Mon Sep 17 00:00:00 2001 From: SenorSmartyPants <senorsmartypants@gmail.com> Date: Fri, 10 Mar 2023 11:03:11 -0600 Subject: [PATCH 153/858] Save TVChannel Height if set (#8777) Co-authored-by: Cody Robibero <cody@robibe.ro> --- Jellyfin.Api/Controllers/ItemUpdateController.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs index 9c71482413..ece053a9a7 100644 --- a/Jellyfin.Api/Controllers/ItemUpdateController.cs +++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs @@ -246,6 +246,11 @@ public class ItemUpdateController : BaseJellyfinApiController episode.AirsBeforeSeasonNumber = request.AirsBeforeSeasonNumber; } + if (request.Height is not null && item is LiveTvChannel channel) + { + channel.Height = request.Height.Value; + } + item.Tags = request.Tags; if (request.Taglines is not null) From 76ae599bd3ccfd9808f50dbf5d935aa430783e60 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Fri, 10 Mar 2023 17:46:59 +0100 Subject: [PATCH 154/858] Fix playlist creation and removal --- .../Playlists/PlaylistManager.cs | 46 +++++++++++++++---- Jellyfin.Api/Controllers/UserController.cs | 8 +++- .../Entities/IHasShares.cs | 11 ----- MediaBrowser.Controller/Entities/Share.cs | 13 ------ .../Playlists/IPlaylistManager.cs | 8 ++++ MediaBrowser.Controller/Playlists/Playlist.cs | 1 + .../Parsers/BaseItemXmlParser.cs | 16 +++++++ .../Savers/BaseXmlSaver.cs | 20 ++++---- MediaBrowser.Model/Entities/IHasShares.cs | 12 +++++ MediaBrowser.Model/Entities/Share.cs | 17 +++++++ .../Playlists/PlaylistCreationRequest.cs | 39 +++++++++++----- 11 files changed, 137 insertions(+), 54 deletions(-) delete mode 100644 MediaBrowser.Controller/Entities/IHasShares.cs delete mode 100644 MediaBrowser.Controller/Entities/Share.cs create mode 100644 MediaBrowser.Model/Entities/IHasShares.cs create mode 100644 MediaBrowser.Model/Entities/Share.cs diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 2717c392b2..8cba652e7b 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -135,16 +135,8 @@ namespace Emby.Server.Implementations.Playlists { Name = name, Path = path, - Shares = new[] - { - new Share - { - UserId = options.UserId.Equals(default) - ? null - : options.UserId.ToString("N", CultureInfo.InvariantCulture), - CanEdit = true - } - } + OwnerUserId = options.UserId, + Shares = options.Shares }; playlist.SetMediaType(options.MediaType); @@ -537,5 +529,39 @@ namespace Emby.Server.Implementations.Playlists return _libraryManager.RootFolder.Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, TypeName, StringComparison.Ordinal)) ?? _libraryManager.GetUserRootFolder().Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, TypeName, StringComparison.Ordinal)); } + + public async Task RemovePlaylists(Guid userId) + { + var playlists = GetPlaylists(userId); + foreach (var playlist in playlists) + { + // Update owner if shared + var rankedShares = playlist.Shares.OrderByDescending(x => x.CanEdit).ToArray(); + if (rankedShares.Length > 0 && Guid.TryParse(rankedShares[0].UserId, out var guid)) + { + playlist.OwnerUserId = guid; + playlist.Shares = rankedShares.Skip(1).ToArray(); + await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); + + if (playlist.IsFile) + { + SavePlaylistFile(playlist); + } + } + else + { + // Remove playlist if not shared + _libraryManager.DeleteItem( + playlist, + new DeleteOptions + { + DeleteFileLocation = false, + DeleteFromExternalProvider = false + }, + playlist.GetParent(), + false); + } + } + } } } diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index b0973b8a14..09bf895505 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -15,6 +15,7 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.QuickConnect; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Configuration; @@ -41,6 +42,7 @@ public class UserController : BaseJellyfinApiController private readonly IServerConfigurationManager _config; private readonly ILogger _logger; private readonly IQuickConnect _quickConnectManager; + private readonly IPlaylistManager _playlistManager; /// <summary> /// Initializes a new instance of the <see cref="UserController"/> class. @@ -53,6 +55,7 @@ public class UserController : BaseJellyfinApiController /// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param> /// <param name="quickConnectManager">Instance of the <see cref="IQuickConnect"/> interface.</param> + /// <param name="playlistManager">Instance of the <see cref="IPlaylistManager"/> interface.</param> public UserController( IUserManager userManager, ISessionManager sessionManager, @@ -61,7 +64,8 @@ public class UserController : BaseJellyfinApiController IAuthorizationContext authContext, IServerConfigurationManager config, ILogger<UserController> logger, - IQuickConnect quickConnectManager) + IQuickConnect quickConnectManager, + IPlaylistManager playlistManager) { _userManager = userManager; _sessionManager = sessionManager; @@ -71,6 +75,7 @@ public class UserController : BaseJellyfinApiController _config = config; _logger = logger; _quickConnectManager = quickConnectManager; + _playlistManager = playlistManager; } /// <summary> @@ -153,6 +158,7 @@ public class UserController : BaseJellyfinApiController } await _sessionManager.RevokeUserTokens(user.Id, null).ConfigureAwait(false); + await _playlistManager.RemovePlaylists(userId).ConfigureAwait(false); await _userManager.DeleteUserAsync(userId).ConfigureAwait(false); return NoContent(); } diff --git a/MediaBrowser.Controller/Entities/IHasShares.cs b/MediaBrowser.Controller/Entities/IHasShares.cs deleted file mode 100644 index e6fa27703b..0000000000 --- a/MediaBrowser.Controller/Entities/IHasShares.cs +++ /dev/null @@ -1,11 +0,0 @@ -#nullable disable - -#pragma warning disable CA1819, CS1591 - -namespace MediaBrowser.Controller.Entities -{ - public interface IHasShares - { - Share[] Shares { get; set; } - } -} diff --git a/MediaBrowser.Controller/Entities/Share.cs b/MediaBrowser.Controller/Entities/Share.cs deleted file mode 100644 index 64f446eef2..0000000000 --- a/MediaBrowser.Controller/Entities/Share.cs +++ /dev/null @@ -1,13 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -namespace MediaBrowser.Controller.Entities -{ - public class Share - { - public string UserId { get; set; } - - public bool CanEdit { get; set; } - } -} diff --git a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs index f6c5920709..201dadb3d2 100644 --- a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs +++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs @@ -56,5 +56,13 @@ namespace MediaBrowser.Controller.Playlists /// <param name="newIndex">The new index.</param> /// <returns>Task.</returns> Task MoveItemAsync(string playlistId, string entryId, int newIndex); + + /// <summary> + /// Removed all playlists of a user. + /// If the playlist is shared, ownership is transferred. + /// </summary> + /// <param name="userId">The user id.</param> + /// <returns>Task.</returns> + Task RemovePlaylists(Guid userId); } } diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs index e6bcc9ea85..2ed05a32c0 100644 --- a/MediaBrowser.Controller/Playlists/Playlist.cs +++ b/MediaBrowser.Controller/Playlists/Playlist.cs @@ -15,6 +15,7 @@ using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; namespace MediaBrowser.Controller.Playlists diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs index c8912807ea..a01490c96c 100644 --- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Xml; using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using Microsoft.Extensions.Logging; @@ -636,6 +637,21 @@ namespace MediaBrowser.LocalMetadata.Parsers break; } + case "OwnerUserId": + { + var val = reader.ReadElementContentAsString(); + + if (Guid.TryParse(val, out var guid) && !guid.Equals(Guid.Empty)) + { + if (item is Playlist playlist) + { + playlist.OwnerUserId = guid; + } + } + + break; + } + case "Format3D": { var val = reader.ReadElementContentAsString(); diff --git a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs index d92b504740..1f9c562bac 100644 --- a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs +++ b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs @@ -395,6 +395,7 @@ namespace MediaBrowser.LocalMetadata.Savers if (item is Playlist playlist && !Playlist.IsPlaylistFile(playlist.Path)) { + await writer.WriteElementStringAsync(null, "OwnerUserId", null, playlist.OwnerUserId.ToString("N")).ConfigureAwait(false); await AddLinkedChildren(playlist, writer, "PlaylistItems", "PlaylistItem").ConfigureAwait(false); } @@ -418,16 +419,19 @@ namespace MediaBrowser.LocalMetadata.Savers foreach (var share in item.Shares) { - await writer.WriteStartElementAsync(null, "Share", null).ConfigureAwait(false); + if (share.UserId is not null) + { + await writer.WriteStartElementAsync(null, "Share", null).ConfigureAwait(false); - await writer.WriteElementStringAsync(null, "UserId", null, share.UserId).ConfigureAwait(false); - await writer.WriteElementStringAsync( - null, - "CanEdit", - null, - share.CanEdit.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()).ConfigureAwait(false); + await writer.WriteElementStringAsync(null, "UserId", null, share.UserId).ConfigureAwait(false); + await writer.WriteElementStringAsync( + null, + "CanEdit", + null, + share.CanEdit.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()).ConfigureAwait(false); - await writer.WriteEndElementAsync().ConfigureAwait(false); + await writer.WriteEndElementAsync().ConfigureAwait(false); + } } await writer.WriteEndElementAsync().ConfigureAwait(false); diff --git a/MediaBrowser.Model/Entities/IHasShares.cs b/MediaBrowser.Model/Entities/IHasShares.cs new file mode 100644 index 0000000000..b34d1a0376 --- /dev/null +++ b/MediaBrowser.Model/Entities/IHasShares.cs @@ -0,0 +1,12 @@ +namespace MediaBrowser.Model.Entities; + +/// <summary> +/// Interface for access to shares. +/// </summary> +public interface IHasShares +{ + /// <summary> + /// Gets or sets the shares. + /// </summary> + Share[] Shares { get; set; } +} diff --git a/MediaBrowser.Model/Entities/Share.cs b/MediaBrowser.Model/Entities/Share.cs new file mode 100644 index 0000000000..186aad1892 --- /dev/null +++ b/MediaBrowser.Model/Entities/Share.cs @@ -0,0 +1,17 @@ +namespace MediaBrowser.Model.Entities; + +/// <summary> +/// Class to hold data on sharing permissions. +/// </summary> +public class Share +{ + /// <summary> + /// Gets or sets the user id. + /// </summary> + public string? UserId { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether the user has edit permissions. + /// </summary> + public bool CanEdit { get; set; } +} diff --git a/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs b/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs index e8ee494034..8472697164 100644 --- a/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs +++ b/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs @@ -1,19 +1,36 @@ -#nullable disable -#pragma warning disable CS1591 - using System; using System.Collections.Generic; +using MediaBrowser.Model.Entities; -namespace MediaBrowser.Model.Playlists +namespace MediaBrowser.Model.Playlists; + +/// <summary> +/// A playlist creation request. +/// </summary> +public class PlaylistCreationRequest { - public class PlaylistCreationRequest - { - public string Name { get; set; } + /// <summary> + /// Gets or sets the name. + /// </summary> + public string? Name { get; set; } - public IReadOnlyList<Guid> ItemIdList { get; set; } = Array.Empty<Guid>(); + /// <summary> + /// Gets or sets the list of items. + /// </summary> + public IReadOnlyList<Guid> ItemIdList { get; set; } = Array.Empty<Guid>(); - public string MediaType { get; set; } + /// <summary> + /// Gets or sets the media type. + /// </summary> + public string? MediaType { get; set; } - public Guid UserId { get; set; } - } + /// <summary> + /// Gets or sets the user id. + /// </summary> + public Guid UserId { get; set; } + + /// <summary> + /// Gets or sets the shares. + /// </summary> + public Share[]? Shares { get; set; } } From 8d158df67856bbb9de696f5caa0ec475b172ced2 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Fri, 10 Mar 2023 19:16:57 +0100 Subject: [PATCH 155/858] Add migration to properly set playlist owner --- .../Playlists/PlaylistManager.cs | 14 ++++ Jellyfin.Server/Migrations/MigrationRunner.cs | 3 +- .../Migrations/Routines/FixPlaylistOwner.cs | 67 +++++++++++++++++++ .../Playlists/IPlaylistManager.cs | 7 ++ 4 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 8cba652e7b..c15c2c4008 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -563,5 +563,19 @@ namespace Emby.Server.Implementations.Playlists } } } + + public async Task UpdatePlaylist(Playlist playlist) + { + var currentPlaylist = (Playlist)_libraryManager.GetItemById(playlist.Id); + currentPlaylist.OwnerUserId = playlist.OwnerUserId; + currentPlaylist.Shares = playlist.Shares; + + await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); + + if (playlist.IsFile) + { + SavePlaylistFile(currentPlaylist); + } + } } } diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs index d4bf81f10b..866262d22f 100644 --- a/Jellyfin.Server/Migrations/MigrationRunner.cs +++ b/Jellyfin.Server/Migrations/MigrationRunner.cs @@ -40,7 +40,8 @@ namespace Jellyfin.Server.Migrations typeof(Routines.ReaddDefaultPluginRepository), typeof(Routines.MigrateDisplayPreferencesDb), typeof(Routines.RemoveDownloadImagesInAdvance), - typeof(Routines.MigrateAuthenticationDb) + typeof(Routines.MigrateAuthenticationDb), + typeof(Routines.FixPlaylistOwner) }; /// <summary> diff --git a/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs b/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs new file mode 100644 index 0000000000..d5736fd3c3 --- /dev/null +++ b/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs @@ -0,0 +1,67 @@ +using System; +using System.Linq; + +using Jellyfin.Data.Enums; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Playlists; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Migrations.Routines; + +/// <summary> +/// Properly set playlist owner. +/// </summary> +internal class FixPlaylistOwner : IMigrationRoutine +{ + private readonly ILogger<RemoveDuplicateExtras> _logger; + private readonly ILibraryManager _libraryManager; + private readonly IPlaylistManager _playlistManager; + + public FixPlaylistOwner( + ILogger<RemoveDuplicateExtras> logger, + ILibraryManager libraryManager, + IPlaylistManager playlistManager) + { + _logger = logger; + _libraryManager = libraryManager; + _playlistManager = playlistManager; + } + + /// <inheritdoc/> + public Guid Id => Guid.Parse("{615DFA9E-2497-4DBB-A472-61938B752C5B}"); + + /// <inheritdoc/> + public string Name => "FixPlaylistOwner"; + + /// <inheritdoc/> + public bool PerformOnNewInstall => false; + + /// <inheritdoc/> + public void Perform() + { + var playlists = _libraryManager.GetItemList(new InternalItemsQuery + { + IncludeItemTypes = new[] { BaseItemKind.Playlist } + }) + .Cast<Playlist>() + .Where(x => x.OwnerUserId.Equals(Guid.Empty)) + .ToArray(); + + if (playlists.Length > 0) + { + foreach (var playlist in playlists) + { + var shares = playlist.Shares; + var firstEditShare = shares.First(x => x.CanEdit); + if (firstEditShare is not null && Guid.TryParse(firstEditShare.UserId, out var guid)) + { + playlist.OwnerUserId = guid; + playlist.Shares = shares.Where(x => x != firstEditShare).ToArray(); + + _playlistManager.UpdatePlaylist(playlist).GetAwaiter().GetResult(); + } + } + } + } +} diff --git a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs index 201dadb3d2..22f974420f 100644 --- a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs +++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs @@ -64,5 +64,12 @@ namespace MediaBrowser.Controller.Playlists /// <param name="userId">The user id.</param> /// <returns>Task.</returns> Task RemovePlaylists(Guid userId); + + /// <summary> + /// Updates a playlist. + /// </summary> + /// <param name="playlist">The updated playlist.</param> + /// <returns>Task.</returns> + Task UpdatePlaylist(Playlist playlist); } } From 38967ad47f5cde187c2fe4d82cbc7d275858f7ce Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 10 Mar 2023 21:18:34 +0000 Subject: [PATCH 156/858] Update github/codeql-action digest to 16964e9 --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 6d87af538c..996a849b8c 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@32dc499307d133bb5085bae78498c0ac2cf762d5 # v2 + uses: github/codeql-action/init@16964e90ba004cdf0cd845b866b5df21038b7723 # v2 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@32dc499307d133bb5085bae78498c0ac2cf762d5 # v2 + uses: github/codeql-action/autobuild@16964e90ba004cdf0cd845b866b5df21038b7723 # v2 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@32dc499307d133bb5085bae78498c0ac2cf762d5 # v2 + uses: github/codeql-action/analyze@16964e90ba004cdf0cd845b866b5df21038b7723 # v2 From 196c7b3bbf392cf2e75bbda80b87025b21d96b18 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 11 Mar 2023 12:26:05 +0000 Subject: [PATCH 157/858] Update dependency libse to v3.6.11 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 48c766edb5..2a026d9478 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -20,7 +20,7 @@ <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.8.5" /> <PackageVersion Include="FsCheck.Xunit" Version="2.16.5" /> <PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" /> - <PackageVersion Include="libse" Version="3.6.10" /> + <PackageVersion Include="libse" Version="3.6.11" /> <PackageVersion Include="LrcParser" Version="2023.308.0" /> <PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" /> <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.3" /> From b7fe81551f43b08f7991979be16bdbd1dddcb636 Mon Sep 17 00:00:00 2001 From: Bill Thornton <thornbill@users.noreply.github.com> Date: Sat, 11 Mar 2023 12:14:00 -0500 Subject: [PATCH 158/858] Add manual web builds (#9468) --- Dockerfile | 1 + Dockerfile.arm | 1 + Dockerfile.arm64 | 1 + 3 files changed, 3 insertions(+) diff --git a/Dockerfile b/Dockerfile index f5f5787bec..e51d285e12 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,6 +10,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine- && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && cd jellyfin-web-* \ && npm ci --no-audit --unsafe-perm \ + && npm run build:production \ && mv dist /dist FROM debian:stable-slim as app diff --git a/Dockerfile.arm b/Dockerfile.arm index bbb84a461c..46a3e9b998 100644 --- a/Dockerfile.arm +++ b/Dockerfile.arm @@ -11,6 +11,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine- && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && cd jellyfin-web-* \ && npm ci --no-audit --unsafe-perm \ + && npm run build:production \ && mv dist /dist FROM multiarch/qemu-user-static:x86_64-arm as qemu diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 index 5572586ae9..4f9d5e1fdc 100644 --- a/Dockerfile.arm64 +++ b/Dockerfile.arm64 @@ -11,6 +11,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine- && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && cd jellyfin-web-* \ && npm ci --no-audit --unsafe-perm \ + && npm run build:production \ && mv dist /dist FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu From af611367c14e2d56e6346b03bda7d75ab6f04b96 Mon Sep 17 00:00:00 2001 From: gitteric <eschewe@gmx.net> Date: Sun, 12 Mar 2023 16:45:48 +0100 Subject: [PATCH 159/858] Fall back to using "logo" attrib if "tvg-logo" is mssing in M3U-tuner (#9475) Co-authored-by: gitteric <you@example.com> --- .../LiveTv/TunerHosts/M3uParser.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs index 046be7c5c7..f2020e05fe 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs @@ -122,9 +122,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts var attributes = ParseExtInf(extInf, out string remaining); extInf = remaining; - if (attributes.TryGetValue("tvg-logo", out string value)) + if (attributes.TryGetValue("tvg-logo", out string tvgLogo)) { - channel.ImageUrl = value; + channel.ImageUrl = tvgLogo; + } + else if (attributes.TryGetValue("logo", out string logo)) + { + channel.ImageUrl = logo; } if (attributes.TryGetValue("group-title", out string groupTitle)) From d8ec3a5470fe602fab356c37720d38190aa713ef Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Wed, 1 Mar 2023 18:57:23 +0100 Subject: [PATCH 160/858] Reduce usage of GetAwaiter().GetResult() --- .../Channels/ChannelManager.cs | 24 ++++++++------ .../EntryPoints/LibraryChangedNotifier.cs | 18 ++++++++--- .../EntryPoints/UserDataChangeNotifier.cs | 13 ++++---- .../Library/UserViewManager.cs | 4 +-- .../LiveTv/LiveTvManager.cs | 32 +++++++++---------- .../Controllers/ChannelsController.cs | 6 ++-- Jellyfin.Api/Controllers/LiveTvController.cs | 10 +++--- .../ActivityLogWebSocketListener.cs | 4 +-- .../Channels/IChannelManager.cs | 4 +-- .../LiveTv/ILiveTvManager.cs | 4 +-- MediaBrowser.Model/Dlna/StreamBuilder.cs | 23 ++++--------- .../StudioImages/StudiosImageProvider.cs | 2 +- 12 files changed, 74 insertions(+), 70 deletions(-) diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs index 1e3c4dea14..961e225e9e 100644 --- a/Emby.Server.Implementations/Channels/ChannelManager.cs +++ b/Emby.Server.Implementations/Channels/ChannelManager.cs @@ -157,16 +157,16 @@ namespace Emby.Server.Implementations.Channels } /// <inheritdoc /> - public QueryResult<Channel> GetChannelsInternal(ChannelQuery query) + public async Task<QueryResult<Channel>> GetChannelsInternalAsync(ChannelQuery query) { var user = query.UserId.Equals(default) ? null : _userManager.GetUserById(query.UserId); - var channels = GetAllChannels() - .Select(GetChannelEntity) + var channels = await GetAllChannelEntitiesAsync() .OrderBy(i => i.SortName) - .ToList(); + .ToListAsync() + .ConfigureAwait(false); if (query.IsRecordingsFolder.HasValue) { @@ -226,6 +226,7 @@ namespace Emby.Server.Implementations.Channels if (user is not null) { + var userId = user.Id.ToString("N", CultureInfo.InvariantCulture); channels = channels.Where(i => { if (!i.IsVisible(user)) @@ -235,7 +236,7 @@ namespace Emby.Server.Implementations.Channels try { - return GetChannelProvider(i).IsEnabledFor(user.Id.ToString("N", CultureInfo.InvariantCulture)); + return GetChannelProvider(i).IsEnabledFor(userId); } catch { @@ -258,7 +259,7 @@ namespace Emby.Server.Implementations.Channels { foreach (var item in all) { - RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).GetAwaiter().GetResult(); + await RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).ConfigureAwait(false); } } @@ -269,13 +270,13 @@ namespace Emby.Server.Implementations.Channels } /// <inheritdoc /> - public QueryResult<BaseItemDto> GetChannels(ChannelQuery query) + public async Task<QueryResult<BaseItemDto>> GetChannelsAsync(ChannelQuery query) { var user = query.UserId.Equals(default) ? null : _userManager.GetUserById(query.UserId); - var internalResult = GetChannelsInternal(query); + var internalResult = await GetChannelsInternalAsync(query).ConfigureAwait(false); var dtoOptions = new DtoOptions(); @@ -327,9 +328,12 @@ namespace Emby.Server.Implementations.Channels progress.Report(100); } - private Channel GetChannelEntity(IChannel channel) + private async IAsyncEnumerable<Channel> GetAllChannelEntitiesAsync() { - return GetChannel(GetInternalChannelId(channel.Name)) ?? GetChannel(channel, CancellationToken.None).GetAwaiter().GetResult(); + foreach (IChannel channel in GetAllChannels()) + { + yield return GetChannel(GetInternalChannelId(channel.Name)) ?? await GetChannel(channel, CancellationToken.None).ConfigureAwait(false); + } } private MediaSourceInfo[] GetSavedMediaSources(BaseItem item) diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs index 05d0a9b794..2e3988f9eb 100644 --- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs @@ -276,25 +276,31 @@ namespace Emby.Server.Implementations.EntryPoints /// Libraries the update timer callback. /// </summary> /// <param name="state">The state.</param> - private void LibraryUpdateTimerCallback(object state) + private async void LibraryUpdateTimerCallback(object state) { + List<Folder> foldersAddedTo; + List<Folder> foldersRemovedFrom; + List<BaseItem> itemsUpdated; + List<BaseItem> itemsAdded; + List<BaseItem> itemsRemoved; lock (_libraryChangedSyncLock) { // Remove dupes in case some were saved multiple times - var foldersAddedTo = _foldersAddedTo + foldersAddedTo = _foldersAddedTo .DistinctBy(x => x.Id) .ToList(); - var foldersRemovedFrom = _foldersRemovedFrom + foldersRemovedFrom = _foldersRemovedFrom .DistinctBy(x => x.Id) .ToList(); - var itemsUpdated = _itemsUpdated + itemsUpdated = _itemsUpdated .Where(i => !_itemsAdded.Contains(i)) .DistinctBy(x => x.Id) .ToList(); - SendChangeNotifications(_itemsAdded.ToList(), itemsUpdated, _itemsRemoved.ToList(), foldersAddedTo, foldersRemovedFrom, CancellationToken.None).GetAwaiter().GetResult(); + itemsAdded = _itemsAdded.ToList(); + itemsRemoved = _itemsRemoved.ToList(); if (LibraryUpdateTimer is not null) { @@ -308,6 +314,8 @@ namespace Emby.Server.Implementations.EntryPoints _foldersAddedTo.Clear(); _foldersRemovedFrom.Clear(); } + + await SendChangeNotifications(itemsAdded, itemsUpdated, itemsRemoved, foldersAddedTo, foldersRemovedFrom, CancellationToken.None).ConfigureAwait(false); } /// <summary> diff --git a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs index e724618b3a..d32759017d 100644 --- a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs @@ -87,29 +87,30 @@ namespace Emby.Server.Implementations.EntryPoints } } - private void UpdateTimerCallback(object? state) + private async void UpdateTimerCallback(object? state) { + List<KeyValuePair<Guid, List<BaseItem>>> changes; lock (_syncLock) { // Remove dupes in case some were saved multiple times - var changes = _changedItems.ToList(); + changes = _changedItems.ToList(); _changedItems.Clear(); - SendNotifications(changes, CancellationToken.None).GetAwaiter().GetResult(); - if (_updateTimer is not null) { _updateTimer.Dispose(); _updateTimer = null; } } + + await SendNotifications(changes, CancellationToken.None).ConfigureAwait(false); } private async Task SendNotifications(List<KeyValuePair<Guid, List<BaseItem>>> changes, CancellationToken cancellationToken) { - foreach (var pair in changes) + foreach ((var key, var value) in changes) { - await SendNotifications(pair.Key, pair.Value, cancellationToken).ConfigureAwait(false); + await SendNotifications(key, value, cancellationToken).ConfigureAwait(false); } } diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs index 0e2d34d39c..17f1d1905f 100644 --- a/Emby.Server.Implementations/Library/UserViewManager.cs +++ b/Emby.Server.Implementations/Library/UserViewManager.cs @@ -111,10 +111,10 @@ namespace Emby.Server.Implementations.Library if (query.IncludeExternalContent) { - var channelResult = _channelManager.GetChannelsInternal(new ChannelQuery + var channelResult = _channelManager.GetChannelsInternalAsync(new ChannelQuery { UserId = query.UserId - }); + }).GetAwaiter().GetResult(); var channels = channelResult.Items; diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs index 4003468d0d..ee039ff0f7 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs @@ -1312,20 +1312,19 @@ namespace Emby.Server.Implementations.LiveTv return 7; } - private QueryResult<BaseItem> GetEmbyRecordings(RecordingQuery query, DtoOptions dtoOptions, User user) + private async Task<QueryResult<BaseItem>> GetEmbyRecordingsAsync(RecordingQuery query, DtoOptions dtoOptions, User user) { if (user is null) { return new QueryResult<BaseItem>(); } - var folderIds = GetRecordingFolders(user, true) - .Select(i => i.Id) - .ToList(); + var folders = await GetRecordingFoldersAsync(user, true).ConfigureAwait(false); + var folderIds = Array.ConvertAll(folders, x => x.Id); var excludeItemTypes = new List<BaseItemKind>(); - if (folderIds.Count == 0) + if (folderIds.Length == 0) { return new QueryResult<BaseItem>(); } @@ -1392,7 +1391,7 @@ namespace Emby.Server.Implementations.LiveTv { MediaTypes = new[] { MediaType.Video }, Recursive = true, - AncestorIds = folderIds.ToArray(), + AncestorIds = folderIds, IsFolder = false, IsVirtualItem = false, Limit = limit, @@ -1528,7 +1527,7 @@ namespace Emby.Server.Implementations.LiveTv } } - public QueryResult<BaseItemDto> GetRecordings(RecordingQuery query, DtoOptions options) + public async Task<QueryResult<BaseItemDto>> GetRecordingsAsync(RecordingQuery query, DtoOptions options) { var user = query.UserId.Equals(default) ? null @@ -1536,7 +1535,7 @@ namespace Emby.Server.Implementations.LiveTv RemoveFields(options); - var internalResult = GetEmbyRecordings(query, options, user); + var internalResult = await GetEmbyRecordingsAsync(query, options, user).ConfigureAwait(false); var returnArray = _dtoService.GetBaseItemDtos(internalResult.Items, options, user); @@ -2379,12 +2378,11 @@ namespace Emby.Server.Implementations.LiveTv return _tvDtoService.GetInternalProgramId(externalId); } - public List<BaseItem> GetRecordingFolders(User user) - { - return GetRecordingFolders(user, false); - } + /// <inheritdoc /> + public Task<BaseItem[]> GetRecordingFoldersAsync(User user) + => GetRecordingFoldersAsync(user, false); - private List<BaseItem> GetRecordingFolders(User user, bool refreshChannels) + private async Task<BaseItem[]> GetRecordingFoldersAsync(User user, bool refreshChannels) { var folders = EmbyTV.EmbyTV.Current.GetRecordingFolders() .SelectMany(i => i.Locations) @@ -2396,14 +2394,16 @@ namespace Emby.Server.Implementations.LiveTv .OrderBy(i => i.SortName) .ToList(); - folders.AddRange(_channelManager.GetChannelsInternal(new MediaBrowser.Model.Channels.ChannelQuery + var channels = await _channelManager.GetChannelsInternalAsync(new MediaBrowser.Model.Channels.ChannelQuery { UserId = user.Id, IsRecordingsFolder = true, RefreshLatestChannelItems = refreshChannels - }).Items); + }).ConfigureAwait(false); - return folders.Cast<BaseItem>().ToList(); + folders.AddRange(channels.Items); + + return folders.Cast<BaseItem>().ToArray(); } } } diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs index b5c4d83462..11c4ac3768 100644 --- a/Jellyfin.Api/Controllers/ChannelsController.cs +++ b/Jellyfin.Api/Controllers/ChannelsController.cs @@ -52,7 +52,7 @@ public class ChannelsController : BaseJellyfinApiController /// <returns>An <see cref="OkResult"/> containing the channels.</returns> [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<QueryResult<BaseItemDto>> GetChannels( + public async Task<ActionResult<QueryResult<BaseItemDto>>> GetChannels( [FromQuery] Guid? userId, [FromQuery] int? startIndex, [FromQuery] int? limit, @@ -61,7 +61,7 @@ public class ChannelsController : BaseJellyfinApiController [FromQuery] bool? isFavorite) { userId = RequestHelpers.GetUserId(User, userId); - return _channelManager.GetChannels(new ChannelQuery + return await _channelManager.GetChannelsAsync(new ChannelQuery { Limit = limit, StartIndex = startIndex, @@ -69,7 +69,7 @@ public class ChannelsController : BaseJellyfinApiController SupportsLatestItems = supportsLatestItems, SupportsMediaDeletion = supportsMediaDeletion, IsFavorite = isFavorite - }); + }).ConfigureAwait(false); } /// <summary> diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 96fc91f93c..267ba4afb4 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -252,7 +252,7 @@ public class LiveTvController : BaseJellyfinApiController [HttpGet("Recordings")] [ProducesResponseType(StatusCodes.Status200OK)] [Authorize(Policy = Policies.LiveTvAccess)] - public ActionResult<QueryResult<BaseItemDto>> GetRecordings( + public async Task<ActionResult<QueryResult<BaseItemDto>>> GetRecordings( [FromQuery] string? channelId, [FromQuery] Guid? userId, [FromQuery] int? startIndex, @@ -278,7 +278,7 @@ public class LiveTvController : BaseJellyfinApiController .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - return _liveTvManager.GetRecordings( + return await _liveTvManager.GetRecordingsAsync( new RecordingQuery { ChannelId = channelId, @@ -299,7 +299,7 @@ public class LiveTvController : BaseJellyfinApiController ImageTypeLimit = imageTypeLimit, EnableImages = enableImages }, - dtoOptions); + dtoOptions).ConfigureAwait(false); } /// <summary> @@ -383,13 +383,13 @@ public class LiveTvController : BaseJellyfinApiController [HttpGet("Recordings/Folders")] [ProducesResponseType(StatusCodes.Status200OK)] [Authorize(Policy = Policies.LiveTvAccess)] - public ActionResult<QueryResult<BaseItemDto>> GetRecordingFolders([FromQuery] Guid? userId) + public async Task<ActionResult<QueryResult<BaseItemDto>>> GetRecordingFolders([FromQuery] Guid? userId) { userId = RequestHelpers.GetUserId(User, userId); var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); - var folders = _liveTvManager.GetRecordingFolders(user); + var folders = await _liveTvManager.GetRecordingFoldersAsync(user).ConfigureAwait(false); var returnArray = _dtoService.GetBaseItemDtos(folders, new DtoOptions(), user); diff --git a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs index 3eac814199..4a5e0ecd4f 100644 --- a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs +++ b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs @@ -56,8 +56,8 @@ public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<Activi base.Dispose(dispose); } - private void OnEntryCreated(object? sender, GenericEventArgs<ActivityLogEntry> e) + private async void OnEntryCreated(object? sender, GenericEventArgs<ActivityLogEntry> e) { - SendData(true).GetAwaiter().GetResult(); + await SendData(true).ConfigureAwait(false); } } diff --git a/MediaBrowser.Controller/Channels/IChannelManager.cs b/MediaBrowser.Controller/Channels/IChannelManager.cs index e392a3493e..8eb27888ab 100644 --- a/MediaBrowser.Controller/Channels/IChannelManager.cs +++ b/MediaBrowser.Controller/Channels/IChannelManager.cs @@ -46,14 +46,14 @@ namespace MediaBrowser.Controller.Channels /// </summary> /// <param name="query">The query.</param> /// <returns>The channels.</returns> - QueryResult<Channel> GetChannelsInternal(ChannelQuery query); + Task<QueryResult<Channel>> GetChannelsInternalAsync(ChannelQuery query); /// <summary> /// Gets the channels. /// </summary> /// <param name="query">The query.</param> /// <returns>The channels.</returns> - QueryResult<BaseItemDto> GetChannels(ChannelQuery query); + Task<QueryResult<BaseItemDto>> GetChannelsAsync(ChannelQuery query); /// <summary> /// Gets the latest channel items. diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs index 46bdca3027..3b6a16dee3 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs +++ b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs @@ -97,7 +97,7 @@ namespace MediaBrowser.Controller.LiveTv /// <param name="query">The query.</param> /// <param name="options">The options.</param> /// <returns>A recording.</returns> - QueryResult<BaseItemDto> GetRecordings(RecordingQuery query, DtoOptions options); + Task<QueryResult<BaseItemDto>> GetRecordingsAsync(RecordingQuery query, DtoOptions options); /// <summary> /// Gets the timers. @@ -308,6 +308,6 @@ namespace MediaBrowser.Controller.LiveTv void AddInfoToRecordingDto(BaseItem item, BaseItemDto dto, ActiveRecordingInfo activeRecordingInfo, User user = null); - List<BaseItem> GetRecordingFolders(User user); + Task<BaseItem[]> GetRecordingFoldersAsync(User user); } } diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 6f99bbc13e..84c8c012c1 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -44,32 +44,24 @@ namespace MediaBrowser.Model.Dlna { ValidateMediaOptions(options, false); - var mediaSources = new List<MediaSourceInfo>(); + var streams = new List<StreamInfo>(); foreach (var mediaSource in options.MediaSources) { - if (string.IsNullOrEmpty(options.MediaSourceId) - || string.Equals(mediaSource.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase)) + if (!(string.IsNullOrEmpty(options.MediaSourceId) + || string.Equals(mediaSource.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase))) { - mediaSources.Add(mediaSource); + continue; } - } - var streams = new List<StreamInfo>(); - foreach (var mediaSourceInfo in mediaSources) - { - StreamInfo? streamInfo = GetOptimalAudioStream(mediaSourceInfo, options); + StreamInfo? streamInfo = GetOptimalAudioStream(mediaSource, options); if (streamInfo is not null) { + streamInfo.DeviceId = options.DeviceId; + streamInfo.DeviceProfileId = options.Profile.Id; streams.Add(streamInfo); } } - foreach (var stream in streams) - { - stream.DeviceId = options.DeviceId; - stream.DeviceProfileId = options.Profile.Id; - } - return GetOptimalStream(streams, options.GetMaxBitrate(true) ?? 0); } @@ -399,7 +391,6 @@ namespace MediaBrowser.Model.Dlna return (null, null, GetTranscodeReasonsFromDirectPlayProfile(item, null, audioStream, options.Profile.DirectPlayProfiles)); } - var playMethods = new List<PlayMethod>(); TranscodeReason transcodeReasons = 0; // The profile describes what the device supports diff --git a/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs b/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs index 0fb9d30a62..ae244da19b 100644 --- a/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs @@ -53,7 +53,7 @@ namespace MediaBrowser.Providers.Plugins.StudioImages /// <inheritdoc /> public IEnumerable<ImageType> GetSupportedImages(BaseItem item) { - return new List<ImageType> + return new ImageType[] { ImageType.Thumb }; From 28562bcaddbed4287557063d03623c7d38415175 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 14 Mar 2023 16:19:00 -0600 Subject: [PATCH 161/858] Update dotnet monorepo to v7.0.4 (#9490) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Cody Robibero <cody@robibe.ro> --- Directory.Packages.props | 18 +++++++++--------- deployment/Dockerfile.centos.amd64 | 2 +- deployment/Dockerfile.fedora.amd64 | 2 +- deployment/Dockerfile.ubuntu.amd64 | 2 +- deployment/Dockerfile.ubuntu.arm64 | 2 +- deployment/Dockerfile.ubuntu.armhf | 2 +- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 2a026d9478..cfbe2abbb0 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -23,23 +23,23 @@ <PackageVersion Include="libse" Version="3.6.11" /> <PackageVersion Include="LrcParser" Version="2023.308.0" /> <PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" /> - <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.3" /> - <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.3" /> + <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.4" /> + <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.4" /> <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.3" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.3" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.3" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.3" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.4" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.4" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.4" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.4" /> <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" /> - <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.3" /> + <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.4" /> <PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" /> - <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.3" /> - <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.3" /> + <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.4" /> + <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.4" /> <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Http" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" /> diff --git a/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64 index 95b08eb052..36435194ff 100644 --- a/deployment/Dockerfile.centos.amd64 +++ b/deployment/Dockerfile.centos.amd64 @@ -13,7 +13,7 @@ RUN yum update -yq \ && yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget # Install DotNET SDK -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/794cd64a-31ac-4070-ac39-34858e8c00da/9568dfe47bd2d22de99268ceac5b2bef/dotnet-sdk-7.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/bda88810-e1a6-4cf0-8139-7fd7fe7b2c7a/7a9ffa3e12e5f1c3d8b640e326c1eb14/dotnet-sdk-7.0.202-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.fedora.amd64 b/deployment/Dockerfile.fedora.amd64 index 18fb7bebe3..c8943a3993 100644 --- a/deployment/Dockerfile.fedora.amd64 +++ b/deployment/Dockerfile.fedora.amd64 @@ -12,7 +12,7 @@ RUN dnf update -yq \ && dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd wget make # Install DotNET SDK -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/794cd64a-31ac-4070-ac39-34858e8c00da/9568dfe47bd2d22de99268ceac5b2bef/dotnet-sdk-7.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/bda88810-e1a6-4cf0-8139-7fd7fe7b2c7a/7a9ffa3e12e5f1c3d8b640e326c1eb14/dotnet-sdk-7.0.202-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64 index e0555cd220..7b9a3de4e3 100644 --- a/deployment/Dockerfile.ubuntu.amd64 +++ b/deployment/Dockerfile.ubuntu.amd64 @@ -17,7 +17,7 @@ RUN apt-get update -yqq \ libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/794cd64a-31ac-4070-ac39-34858e8c00da/9568dfe47bd2d22de99268ceac5b2bef/dotnet-sdk-7.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/bda88810-e1a6-4cf0-8139-7fd7fe7b2c7a/7a9ffa3e12e5f1c3d8b640e326c1eb14/dotnet-sdk-7.0.202-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.arm64 b/deployment/Dockerfile.ubuntu.arm64 index ad5a0890b4..32695e3f12 100644 --- a/deployment/Dockerfile.ubuntu.arm64 +++ b/deployment/Dockerfile.ubuntu.arm64 @@ -16,7 +16,7 @@ RUN apt-get update -yqq \ mmv build-essential lsb-release # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/794cd64a-31ac-4070-ac39-34858e8c00da/9568dfe47bd2d22de99268ceac5b2bef/dotnet-sdk-7.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/bda88810-e1a6-4cf0-8139-7fd7fe7b2c7a/7a9ffa3e12e5f1c3d8b640e326c1eb14/dotnet-sdk-7.0.202-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.armhf b/deployment/Dockerfile.ubuntu.armhf index 2d8be18351..8ffbeafade 100644 --- a/deployment/Dockerfile.ubuntu.armhf +++ b/deployment/Dockerfile.ubuntu.armhf @@ -16,7 +16,7 @@ RUN apt-get update -yqq \ mmv build-essential lsb-release # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/794cd64a-31ac-4070-ac39-34858e8c00da/9568dfe47bd2d22de99268ceac5b2bef/dotnet-sdk-7.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/bda88810-e1a6-4cf0-8139-7fd7fe7b2c7a/7a9ffa3e12e5f1c3d8b640e326c1eb14/dotnet-sdk-7.0.202-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet From 21dcf775bee6459707435cc64bc3864f9e0eb0fe Mon Sep 17 00:00:00 2001 From: Shadowghost <Shadowghost@users.noreply.github.com> Date: Tue, 14 Mar 2023 23:20:12 +0100 Subject: [PATCH 162/858] Add config option to disable dummy chapter generation (#9410) --- .../Configuration/ServerConfiguration.cs | 10 +---- .../MediaInfo/FFProbeVideoInfo.cs | 40 +++++++++---------- 2 files changed, 22 insertions(+), 28 deletions(-) diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index c39162250a..7cb07a2f7a 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -243,16 +243,10 @@ namespace MediaBrowser.Model.Configuration public bool AllowClientLogUpload { get; set; } = true; /// <summary> - /// Gets or sets the dummy chapters duration in seconds. + /// Gets or sets the dummy chapter duration in seconds, use 0 (zero) or less to disable generation alltogether. /// </summary> /// <value>The dummy chapters duration.</value> - public int DummyChapterDuration { get; set; } = 300; - - /// <summary> - /// Gets or sets the dummy chapter count. - /// </summary> - /// <value>The dummy chapter count.</value> - public int DummyChapterCount { get; set; } = 100; + public int DummyChapterDuration { get; set; } = 0; /// <summary> /// Gets or sets the chapter image resolution. diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index e199db7f2a..213639371a 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -298,7 +298,7 @@ namespace MediaBrowser.Providers.MediaInfo if (options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh || options.MetadataRefreshMode == MetadataRefreshMode.Default) { - if (chapters.Length == 0 && mediaStreams.Any(i => i.Type == MediaStreamType.Video)) + if (_config.Configuration.DummyChapterDuration > 0 && chapters.Length == 0 && mediaStreams.Any(i => i.Type == MediaStreamType.Video)) { chapters = CreateDummyChapters(video); } @@ -662,39 +662,39 @@ namespace MediaBrowser.Providers.MediaInfo private ChapterInfo[] CreateDummyChapters(Video video) { var runtime = video.RunTimeTicks ?? 0; - long dummyChapterDuration = TimeSpan.FromSeconds(_config.Configuration.DummyChapterDuration).Ticks; - if (runtime < 0) + // Only process files with a runtime higher than 0 and lower than 12h. The latter are likely corrupted. + if (runtime < 0 || runtime > TimeSpan.FromHours(12).Ticks) { throw new ArgumentException( string.Format( CultureInfo.InvariantCulture, - "{0} has invalid runtime of {1}", + "{0} has an invalid runtime of {1} minutes", video.Name, - runtime)); + TimeSpan.FromTicks(runtime).Minutes)); } - if (runtime < dummyChapterDuration) + long dummyChapterDuration = TimeSpan.FromSeconds(_config.Configuration.DummyChapterDuration).Ticks; + if (runtime > dummyChapterDuration) { - return Array.Empty<ChapterInfo>(); - } + int chapterCount = (int)(runtime / dummyChapterDuration); + var chapters = new ChapterInfo[chapterCount]; - // Limit the chapters just in case there's some incorrect metadata here - int chapterCount = (int)Math.Min(runtime / dummyChapterDuration, _config.Configuration.DummyChapterCount); - var chapters = new ChapterInfo[chapterCount]; - - long currentChapterTicks = 0; - for (int i = 0; i < chapterCount; i++) - { - chapters[i] = new ChapterInfo + long currentChapterTicks = 0; + for (int i = 0; i < chapterCount; i++) { - StartPositionTicks = currentChapterTicks - }; + chapters[i] = new ChapterInfo + { + StartPositionTicks = currentChapterTicks + }; - currentChapterTicks += dummyChapterDuration; + currentChapterTicks += dummyChapterDuration; + } + + return chapters; } - return chapters; + return Array.Empty<ChapterInfo>(); } } } From 21dc3fa042ccdcc096c32506c6950f3fcb180ed6 Mon Sep 17 00:00:00 2001 From: Shadowghost <Shadowghost@users.noreply.github.com> Date: Tue, 14 Mar 2023 18:21:01 -0400 Subject: [PATCH 163/858] Backport pull request #9485 from jellyfin/release-10.8.z Fix the bitrate scale factor for h264-to-hevc transcoding Original-merge: 173a963dbf6072897b1086abf3f378ddfa7fda5a Merged-by: Nyanmisaka <nst799610810@gmail.com> Backported-by: crobibero <cody@robibe.ro> --- .../MediaEncoding/EncodingHelper.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 3e338e8714..5de57917e7 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -2116,14 +2116,20 @@ namespace MediaBrowser.Controller.MediaEncoding private static double GetVideoBitrateScaleFactor(string codec) { + // hevc & vp9 - 40% more efficient than h.264 if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "av1", StringComparison.OrdinalIgnoreCase)) + || string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase)) { return .6; } + // av1 - 50% more efficient than h.264 + if (string.Equals(codec, "av1", StringComparison.OrdinalIgnoreCase)) + { + return .5; + } + return 1; } @@ -2131,7 +2137,9 @@ namespace MediaBrowser.Controller.MediaEncoding { var inputScaleFactor = GetVideoBitrateScaleFactor(inputVideoCodec); var outputScaleFactor = GetVideoBitrateScaleFactor(outputVideoCodec); - var scaleFactor = outputScaleFactor / inputScaleFactor; + + // Don't scale the real bitrate lower than the requested bitrate + var scaleFactor = Math.Min(outputScaleFactor / inputScaleFactor, 1); if (bitrate <= 500000) { From ee4ffd64e132fce99cb6c297453623729e20bad4 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Wed, 15 Mar 2023 10:13:06 +0100 Subject: [PATCH 164/858] Prefer other codecs over DTS and TrueHD on transcode --- .../MediaEncoding/EncodingHelper.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index b6118e600a..562b5d0229 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -5609,14 +5609,22 @@ namespace MediaBrowser.Controller.MediaEncoding } var inputChannels = audioStream is null ? 6 : audioStream.Channels ?? 6; + var shiftAudioCodecs = new List<string>(); if (inputChannels >= 6) { - return; + // DTS and TrueHD are not supported by HLS + // Keep them in the supported codecs list, but shift them to the end of the list so that if transcoding happens, another codec is used + shiftAudioCodecs.Add("dca"); + shiftAudioCodecs.Add("truehd"); + } + else + { + // Transcoding to 2ch ac3 or eac3 almost always causes a playback failure + // Keep them in the supported codecs list, but shift them to the end of the list so that if transcoding happens, another codec is used + shiftAudioCodecs.Add("ac3"); + shiftAudioCodecs.Add("eac3"); } - // Transcoding to 2ch ac3 almost always causes a playback failure - // Keep it in the supported codecs list, but shift it to the end of the list so that if transcoding happens, another codec is used - var shiftAudioCodecs = new[] { "ac3", "eac3" }; if (audioCodecs.All(i => shiftAudioCodecs.Contains(i, StringComparison.OrdinalIgnoreCase))) { return; From 79d34c590ea4d0b1772fff77c9f5b52991d460b4 Mon Sep 17 00:00:00 2001 From: mammo0 <marc.ammon@hotmail.de> Date: Wed, 15 Mar 2023 11:52:22 +0100 Subject: [PATCH 165/858] removed unnecessary file stacking rule This rule did not check for a parttype. According to the documantation (https://jellyfin.org/docs/general/server/media/shows/#episodes-split-across-multiple-parts) there should be one. --- Emby.Naming/Common/NamingOptions.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index e9161a6b7b..39c3ceeb3d 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -141,8 +141,7 @@ namespace Emby.Naming.Common VideoFileStackingRules = new[] { new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[0-9]+)[\)\]]?(?:\.[^.]+)?$", true), - new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[a-d])[\)\]]?(?:\.[^.]+)?$", false), - new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]?)(?<number>[a-d])(?:\.[^.]+)?$", false) + new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[a-d])[\)\]]?(?:\.[^.]+)?$", false) }; CleanDateTimes = new[] From 1c57c52474799fb7bcba3c5f08098e348f214590 Mon Sep 17 00:00:00 2001 From: mammo0 <marc.ammon@hotmail.de> Date: Wed, 15 Mar 2023 11:54:44 +0100 Subject: [PATCH 166/858] fixed stacking test cases Movies should not be stacked if no parttype is given. --- tests/Jellyfin.Naming.Tests/Video/StackTests.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/Jellyfin.Naming.Tests/Video/StackTests.cs b/tests/Jellyfin.Naming.Tests/Video/StackTests.cs index 368c3592ef..97b52f7495 100644 --- a/tests/Jellyfin.Naming.Tests/Video/StackTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/StackTests.cs @@ -236,7 +236,7 @@ namespace Jellyfin.Naming.Tests.Video } [Fact] - public void TestFalsePositive() + public void TestMissingParttype() { var files = new[] { @@ -248,9 +248,8 @@ namespace Jellyfin.Naming.Tests.Video var result = StackResolver.ResolveFiles(files, _namingOptions).ToList(); - Assert.Single(result); - - TestStackInfo(result[0], "300", 3); + // There should be no stack, because all files should be treated as separate movies + Assert.Empty(result); } [Fact] @@ -297,11 +296,11 @@ namespace Jellyfin.Naming.Tests.Video var result = StackResolver.ResolveFiles(files, _namingOptions).ToList(); - Assert.Equal(3, result.Count); + // Only 'Bad Boys (2006)' and '300 (2006)' should be in the stack + Assert.Equal(2, result.Count); TestStackInfo(result[0], "300 (2006)", 4); - TestStackInfo(result[1], "300", 3); - TestStackInfo(result[2], "Bad Boys (2006)", 4); + TestStackInfo(result[1], "Bad Boys (2006)", 4); } [Fact] From 90e8aad05d93842c7400b0ff5326e69fe5e7b48e Mon Sep 17 00:00:00 2001 From: mammo0 <marc.ammon@hotmail.de> Date: Wed, 15 Mar 2023 11:56:11 +0100 Subject: [PATCH 167/858] fixed FourSisters test case The files should be treated as separate movies and should not be stacked, because the parttype is missing. --- tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs index cc9cfdd7dd..6f9ae7307e 100644 --- a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs @@ -332,7 +332,9 @@ namespace Jellyfin.Naming.Tests.Video files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(), _namingOptions).ToList(); - Assert.Single(result); + // The result should contain to individual movies + // Version grouping should not work here, because the files are not in a directory with the name 'Four Sisters and a Wedding' + Assert.Equal(2, result.Count); } [Fact] From 8d9cc9fdcca2cf73a09722af31edbcdf68545d5b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 15 Mar 2023 11:34:32 +0000 Subject: [PATCH 168/858] Update dependency EFCoreSecondLevelCacheInterceptor to v3.8.6 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index cfbe2abbb0..ba02b21fda 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -17,7 +17,7 @@ <PackageVersion Include="Diacritics" Version="3.3.14" /> <PackageVersion Include="DiscUtils.Udf" Version="0.16.13" /> <PackageVersion Include="DotNet.Glob" Version="3.1.3" /> - <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.8.5" /> + <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.8.6" /> <PackageVersion Include="FsCheck.Xunit" Version="2.16.5" /> <PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" /> <PackageVersion Include="libse" Version="3.6.11" /> From 0fd46a100ba94aca06c60b44e063f62523371ec6 Mon Sep 17 00:00:00 2001 From: mammo0 <mammo0@users.noreply.github.com> Date: Wed, 15 Mar 2023 13:01:39 +0100 Subject: [PATCH 169/858] fixed typo Co-authored-by: Shadowghost <Shadowghost@users.noreply.github.com> --- tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs index 6f9ae7307e..0316377d49 100644 --- a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs @@ -332,7 +332,7 @@ namespace Jellyfin.Naming.Tests.Video files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(), _namingOptions).ToList(); - // The result should contain to individual movies + // The result should contain two individual movies // Version grouping should not work here, because the files are not in a directory with the name 'Four Sisters and a Wedding' Assert.Equal(2, result.Count); } From a6d23906fdb045c9f04fa3ce189cfd169c305b56 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 15 Mar 2023 12:16:36 +0000 Subject: [PATCH 170/858] Update github/codeql-action digest to 168b99b --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 996a849b8c..cbc4fad949 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@16964e90ba004cdf0cd845b866b5df21038b7723 # v2 + uses: github/codeql-action/init@168b99b3c22180941ae7dbdd5f5c9678ede476ba # v2 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@16964e90ba004cdf0cd845b866b5df21038b7723 # v2 + uses: github/codeql-action/autobuild@168b99b3c22180941ae7dbdd5f5c9678ede476ba # v2 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@16964e90ba004cdf0cd845b866b5df21038b7723 # v2 + uses: github/codeql-action/analyze@168b99b3c22180941ae7dbdd5f5c9678ede476ba # v2 From 24cc1e9aead3a353b4749dc3e0c0d17a79a85c96 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 15 Mar 2023 19:20:45 -0600 Subject: [PATCH 171/858] Update actions/checkout digest to 24cb908 (#9501) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/commands.yml | 4 ++-- .github/workflows/openapi.yml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index cbc4fad949..d5dcb785c1 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3 + uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3 - name: Setup .NET uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3 with: diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index 75227c57b9..c0b365b02f 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -24,7 +24,7 @@ jobs: reactions: '+1' - name: Checkout the latest code - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3 + uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3 with: token: ${{ secrets.JF_BOT_TOKEN }} fetch-depth: 0 @@ -51,7 +51,7 @@ jobs: reactions: eyes - name: Checkout the latest code - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3 + uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3 with: token: ${{ secrets.JF_BOT_TOKEN }} fetch-depth: 0 diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index aa2e0417fe..a67ce90d22 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -14,7 +14,7 @@ jobs: permissions: read-all steps: - name: Checkout repository - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3 + uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3 with: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} @@ -39,7 +39,7 @@ jobs: permissions: read-all steps: - name: Checkout repository - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3 + uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3 with: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} From 3503ea5171d9e8feaf883636be23421111373463 Mon Sep 17 00:00:00 2001 From: Joe Rogers <1337joe@gmail.com> Date: Thu, 16 Mar 2023 00:57:01 -0400 Subject: [PATCH 172/858] Specify full action versions --- .github/workflows/codeql-analysis.yml | 10 +++++----- .github/workflows/commands.yml | 14 +++++++------- .github/workflows/openapi.yml | 22 +++++++++++----------- .github/workflows/repo-stale.yaml | 2 +- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index d5dcb785c1..cdb7970aca 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -20,18 +20,18 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3 + uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3.4.0 - name: Setup .NET - uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3 + uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # v3.0.3 with: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@168b99b3c22180941ae7dbdd5f5c9678ede476ba # v2 + uses: github/codeql-action/init@168b99b3c22180941ae7dbdd5f5c9678ede476ba # v2.2.7 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@168b99b3c22180941ae7dbdd5f5c9678ede476ba # v2 + uses: github/codeql-action/autobuild@168b99b3c22180941ae7dbdd5f5c9678ede476ba # v2.2.7 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@168b99b3c22180941ae7dbdd5f5c9678ede476ba # v2 + uses: github/codeql-action/analyze@168b99b3c22180941ae7dbdd5f5c9678ede476ba # v2.2.7 diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index c0b365b02f..fe1317a759 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -17,14 +17,14 @@ jobs: runs-on: ubuntu-latest steps: - name: Notify as seen - uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2 + uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2.1.1 with: token: ${{ secrets.JF_BOT_TOKEN }} comment-id: ${{ github.event.comment.id }} reactions: '+1' - name: Checkout the latest code - uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3 + uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3.4.0 with: token: ${{ secrets.JF_BOT_TOKEN }} fetch-depth: 0 @@ -43,7 +43,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Notify as seen - uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2 + uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2.1.1 if: ${{ github.event.comment != null }} with: token: ${{ secrets.JF_BOT_TOKEN }} @@ -51,14 +51,14 @@ jobs: reactions: eyes - name: Checkout the latest code - uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3 + uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3.4.0 with: token: ${{ secrets.JF_BOT_TOKEN }} fetch-depth: 0 - name: Notify as running id: comment_running - uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2 + uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2.1.1 if: ${{ github.event.comment != null }} with: token: ${{ secrets.JF_BOT_TOKEN }} @@ -93,7 +93,7 @@ jobs: exit ${retcode} - name: Notify with result success - uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2 + uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2.1.1 if: ${{ github.event.comment != null && success() }} with: token: ${{ secrets.JF_BOT_TOKEN }} @@ -108,7 +108,7 @@ jobs: reactions: hooray - name: Notify with result failure - uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2 + uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2.1.1 if: ${{ github.event.comment != null && failure() }} with: token: ${{ secrets.JF_BOT_TOKEN }} diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index a67ce90d22..8fa8971ec2 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -14,18 +14,18 @@ jobs: permissions: read-all steps: - name: Checkout repository - uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3 + uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3.4.0 with: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} - name: Setup .NET - uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3 + uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # v3.0.3 with: dotnet-version: '7.0.x' - name: Generate openapi.json run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests" - name: Upload openapi.json - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3 + uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 with: name: openapi-head retention-days: 14 @@ -39,7 +39,7 @@ jobs: permissions: read-all steps: - name: Checkout repository - uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3 + uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3.4.0 with: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} @@ -51,13 +51,13 @@ jobs: ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/${{ github.head_ref }}) git checkout --progress --force $ANCESTOR_REF - name: Setup .NET - uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3 + uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # v3.0.3 with: dotnet-version: '7.0.x' - name: Generate openapi.json run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests" - name: Upload openapi.json - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3 + uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 with: name: openapi-base retention-days: 14 @@ -76,12 +76,12 @@ jobs: - openapi-base steps: - name: Download openapi-head - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3 + uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 with: name: openapi-head path: openapi-head - name: Download openapi-base - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3 + uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 with: name: openapi-base path: openapi-base @@ -103,14 +103,14 @@ jobs: body="${body//$'\r'/'%0D'}" echo ::set-output name=body::$body - name: Find difference comment - uses: peter-evans/find-comment@034abe94d3191f9c89d870519735beae326f2bdb # v2 + uses: peter-evans/find-comment@034abe94d3191f9c89d870519735beae326f2bdb # v2.3.0 id: find-comment with: issue-number: ${{ github.event.pull_request.number }} direction: last body-includes: openapi-diff-workflow-comment - name: Reply or edit difference comment (changed) - uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2 + uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2.1.1 if: ${{ steps.read-diff.outputs.body != '' }} with: issue-number: ${{ github.event.pull_request.number }} @@ -125,7 +125,7 @@ jobs: </details> - name: Edit difference comment (unchanged) - uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2 + uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2.1.1 if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }} with: issue-number: ${{ github.event.pull_request.number }} diff --git a/.github/workflows/repo-stale.yaml b/.github/workflows/repo-stale.yaml index 7f6fcffed5..276bb21501 100644 --- a/.github/workflows/repo-stale.yaml +++ b/.github/workflows/repo-stale.yaml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest if: ${{ contains(github.repository, 'jellyfin/') }} steps: - - uses: actions/stale@6f05e4244c9a0b2ed3401882b05d701dd0a7289b # v7 + - uses: actions/stale@6f05e4244c9a0b2ed3401882b05d701dd0a7289b # v7.0.0 with: repo-token: ${{ secrets.JF_BOT_TOKEN }} days-before-stale: 120 From 2e4905ff05b764f5c2d09ee60d51823d7e3b912e Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Thu, 16 Mar 2023 17:33:43 +0100 Subject: [PATCH 173/858] Fix #9378 Remove sort words before replacing dots with spaces --- MediaBrowser.Controller/Entities/BaseItem.cs | 30 ++++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 8fe9cfa7f9..a04f02bf91 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -893,16 +893,6 @@ namespace MediaBrowser.Controller.Entities var sortable = Name.Trim().ToLowerInvariant(); - foreach (var removeChar in ConfigurationManager.Configuration.SortRemoveCharacters) - { - sortable = sortable.Replace(removeChar, string.Empty, StringComparison.Ordinal); - } - - foreach (var replaceChar in ConfigurationManager.Configuration.SortReplaceCharacters) - { - sortable = sortable.Replace(replaceChar, " ", StringComparison.Ordinal); - } - foreach (var search in ConfigurationManager.Configuration.SortRemoveWords) { // Remove from beginning if a space follows @@ -921,12 +911,22 @@ namespace MediaBrowser.Controller.Entities } } + foreach (var removeChar in ConfigurationManager.Configuration.SortRemoveCharacters) + { + sortable = sortable.Replace(removeChar, string.Empty, StringComparison.Ordinal); + } + + foreach (var replaceChar in ConfigurationManager.Configuration.SortReplaceCharacters) + { + sortable = sortable.Replace(replaceChar, " ", StringComparison.Ordinal); + } + return ModifySortChunks(sortable); } - internal static string ModifySortChunks(string name) + internal static string ModifySortChunks(ReadOnlySpan<char> name) { - void AppendChunk(StringBuilder builder, bool isDigitChunk, ReadOnlySpan<char> chunk) + static void AppendChunk(StringBuilder builder, bool isDigitChunk, ReadOnlySpan<char> chunk) { if (isDigitChunk && chunk.Length < 10) { @@ -936,7 +936,7 @@ namespace MediaBrowser.Controller.Entities builder.Append(chunk); } - if (name.Length == 0) + if (name.IsEmpty) { return string.Empty; } @@ -950,13 +950,13 @@ namespace MediaBrowser.Controller.Entities var isDigit = char.IsDigit(name[i]); if (isDigit != isDigitChunk) { - AppendChunk(builder, isDigitChunk, name.AsSpan(chunkStart, i - chunkStart)); + AppendChunk(builder, isDigitChunk, name.Slice(chunkStart, i - chunkStart)); chunkStart = i; isDigitChunk = isDigit; } } - AppendChunk(builder, isDigitChunk, name.AsSpan(chunkStart)); + AppendChunk(builder, isDigitChunk, name.Slice(chunkStart)); // logger.LogDebug("ModifySortChunks Start: {0} End: {1}", name, builder.ToString()); return builder.ToString().RemoveDiacritics(); From 30556d8fc598779d862de34a45068591b5e62eeb Mon Sep 17 00:00:00 2001 From: Gabriele Bizzon <gbizzon@hotmail.com> Date: Wed, 15 Mar 2023 16:32:44 +0000 Subject: [PATCH 174/858] Translated using Weblate (Portuguese) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt/ --- Emby.Server.Implementations/Localization/Core/pt.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json index 39229f45f9..92e0d34aec 100644 --- a/Emby.Server.Implementations/Localization/Core/pt.json +++ b/Emby.Server.Implementations/Localization/Core/pt.json @@ -121,5 +121,6 @@ "TaskOptimizeDatabase": "Otimizar base de dados", "TaskOptimizeDatabaseDescription": "Base de dados compacta e corta espaço livre. A execução desta tarefa depois de digitalizar a biblioteca ou de fazer outras alterações que impliquem modificações na base de dados pode melhorar o desempenho.", "External": "Externo", - "HearingImpaired": "Problemas auditivos" + "HearingImpaired": "Problemas auditivos", + "TaskKeyframeExtractor": "Extrator de quadro-chave" } From 82080bd1ef056153fd54e05fb9decbf5f5830572 Mon Sep 17 00:00:00 2001 From: Shadowghost <Shadowghost@users.noreply.github.com> Date: Sun, 12 Mar 2023 19:42:18 +0100 Subject: [PATCH 175/858] Apply review suggestions --- Emby.Server.Implementations/Playlists/PlaylistManager.cs | 6 +++--- Jellyfin.Api/Controllers/UserController.cs | 2 +- Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs | 2 +- MediaBrowser.Controller/Playlists/IPlaylistManager.cs | 4 ++-- MediaBrowser.Controller/Playlists/Playlist.cs | 6 +++--- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index c15c2c4008..853daf7247 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -530,7 +530,7 @@ namespace Emby.Server.Implementations.Playlists _libraryManager.GetUserRootFolder().Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, TypeName, StringComparison.Ordinal)); } - public async Task RemovePlaylists(Guid userId) + public async Task RemovePlaylistsAsync(Guid userId) { var playlists = GetPlaylists(userId); foreach (var playlist in playlists) @@ -564,7 +564,7 @@ namespace Emby.Server.Implementations.Playlists } } - public async Task UpdatePlaylist(Playlist playlist) + public async Task UpdatePlaylistAsync(Playlist playlist) { var currentPlaylist = (Playlist)_libraryManager.GetItemById(playlist.Id); currentPlaylist.OwnerUserId = playlist.OwnerUserId; @@ -572,7 +572,7 @@ namespace Emby.Server.Implementations.Playlists await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); - if (playlist.IsFile) + if (currentPlaylist.IsFile) { SavePlaylistFile(currentPlaylist); } diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 09bf895505..e495288670 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -158,7 +158,7 @@ public class UserController : BaseJellyfinApiController } await _sessionManager.RevokeUserTokens(user.Id, null).ConfigureAwait(false); - await _playlistManager.RemovePlaylists(userId).ConfigureAwait(false); + await _playlistManager.RemovePlaylistsAsync(userId).ConfigureAwait(false); await _userManager.DeleteUserAsync(userId).ConfigureAwait(false); return NoContent(); } diff --git a/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs b/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs index d5736fd3c3..55aadae79a 100644 --- a/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs +++ b/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs @@ -59,7 +59,7 @@ internal class FixPlaylistOwner : IMigrationRoutine playlist.OwnerUserId = guid; playlist.Shares = shares.Where(x => x != firstEditShare).ToArray(); - _playlistManager.UpdatePlaylist(playlist).GetAwaiter().GetResult(); + _playlistManager.UpdatePlaylistAsync(playlist).GetAwaiter().GetResult(); } } } diff --git a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs index 22f974420f..d889436629 100644 --- a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs +++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs @@ -63,13 +63,13 @@ namespace MediaBrowser.Controller.Playlists /// </summary> /// <param name="userId">The user id.</param> /// <returns>Task.</returns> - Task RemovePlaylists(Guid userId); + Task RemovePlaylistsAsync(Guid userId); /// <summary> /// Updates a playlist. /// </summary> /// <param name="playlist">The updated playlist.</param> /// <returns>Task.</returns> - Task UpdatePlaylist(Playlist playlist); + Task UpdatePlaylistAsync(Playlist playlist); } } diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs index 2ed05a32c0..344e996ea8 100644 --- a/MediaBrowser.Controller/Playlists/Playlist.cs +++ b/MediaBrowser.Controller/Playlists/Playlist.cs @@ -233,7 +233,8 @@ namespace MediaBrowser.Controller.Playlists return base.IsVisible(user); } - if (user.Id.Equals(OwnerUserId)) + var userId = user.Id; + if (userId.Equals(OwnerUserId)) { return true; } @@ -241,10 +242,9 @@ namespace MediaBrowser.Controller.Playlists var shares = Shares; if (shares.Length == 0) { - return base.IsVisible(user); + return false; } - var userId = user.Id; return shares.Any(share => Guid.TryParse(share.UserId, out var id) && id.Equals(userId)); } From 3f6a23d7d094ddd36b01d9b565ce7b3ecce9c0ee Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Fri, 17 Mar 2023 11:49:07 +0100 Subject: [PATCH 176/858] Fix condition in CanStreamCopyAudio --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 562b5d0229..bf73321ab9 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -2023,9 +2023,9 @@ namespace MediaBrowser.Controller.MediaEncoding } } - // Video bitrate must fall within requested value + // Audio bitrate must fall within requested value if (request.AudioBitRate.HasValue - && audioStream.BitDepth.HasValue + && audioStream.BitRate.HasValue && audioStream.BitRate.Value > request.AudioBitRate.Value) { return false; From eba24d188d3ff9747e2c39281e3f64d5fdd53899 Mon Sep 17 00:00:00 2001 From: Shadowghost <Shadowghost@users.noreply.github.com> Date: Sat, 18 Mar 2023 08:25:34 +0100 Subject: [PATCH 177/858] Update Emby.Server.Implementations/Playlists/PlaylistManager.cs Co-authored-by: Joe Rogers <1337joe@users.noreply.github.com> --- Emby.Server.Implementations/Playlists/PlaylistManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 853daf7247..4d1b454288 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -136,7 +136,7 @@ namespace Emby.Server.Implementations.Playlists Name = name, Path = path, OwnerUserId = options.UserId, - Shares = options.Shares + Shares = options.Shares ?? Array.Empty<Share>() }; playlist.SetMediaType(options.MediaType); From 31dfbfdbdf5621823adabbc9f7c3a238f2574068 Mon Sep 17 00:00:00 2001 From: Bas <weblate@hanka.mp> Date: Fri, 17 Mar 2023 10:38:50 +0000 Subject: [PATCH 178/858] Translated using Weblate (Dutch) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/nl/ --- Emby.Server.Implementations/Localization/Core/nl.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index 383096f7e2..01a2ab273f 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -114,7 +114,7 @@ "TasksApplicationCategory": "Toepassing", "TasksLibraryCategory": "Bibliotheek", "TasksMaintenanceCategory": "Onderhoud", - "TaskCleanActivityLogDescription": "Verwijdert activiteiten logs ouder dan de ingestelde leeftijd.", + "TaskCleanActivityLogDescription": "Verwijdert activiteitenlogs ouder dan de ingestelde leeftijd.", "TaskCleanActivityLog": "Activiteitenlogboek legen", "Undefined": "Niet gedefinieerd", "Forced": "Geforceerd", From 39677525f30e01118a879477b1f223c994b71059 Mon Sep 17 00:00:00 2001 From: SenorSmartyPants <senorsmartypants@gmail.com> Date: Sat, 18 Mar 2023 20:24:48 -0500 Subject: [PATCH 179/858] Don't overwrite NFO images (#9452) Co-authored-by: Cody Robibero <cody@robibe.ro> --- .../Providers/ImageRefreshOptions.cs | 3 ++- .../Manager/ItemImageProvider.cs | 2 +- MediaBrowser.Providers/Manager/MetadataService.cs | 14 ++++++++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs b/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs index fd73ed5f80..05b4d43a5a 100644 --- a/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs +++ b/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs @@ -1,6 +1,7 @@ #pragma warning disable CA1819, CS1591 using System; +using System.Collections.Generic; using System.Linq; using MediaBrowser.Model.Entities; @@ -23,7 +24,7 @@ namespace MediaBrowser.Controller.Providers public bool ReplaceAllImages { get; set; } - public ImageType[] ReplaceImages { get; set; } + public IReadOnlyList<ImageType> ReplaceImages { get; set; } public bool IsAutomated { get; set; } diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs index 5d59c4663c..ba2d2db2f7 100644 --- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs +++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs @@ -273,7 +273,7 @@ namespace MediaBrowser.Providers.Manager } if (!refreshOptions.ReplaceAllImages && - refreshOptions.ReplaceImages.Length == 0 && + refreshOptions.ReplaceImages.Count == 0 && ContainsImages(item, provider.GetSupportedImages(item).ToList(), savedOptions, backdropLimit)) { return; diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index 0605b0bd78..ebd653e349 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -26,6 +26,8 @@ namespace MediaBrowser.Providers.Manager where TItemType : BaseItem, IHasLookupInfo<TIdType>, new() where TIdType : ItemLookupInfo, new() { + private static readonly ImageType[] AllImageTypes = Enum.GetValues<ImageType>(); + protected MetadataService(IServerConfigurationManager serverConfigurationManager, ILogger<MetadataService<TItemType, TIdType>> logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager) { ServerConfigurationManager = serverConfigurationManager; @@ -672,6 +674,8 @@ namespace MediaBrowser.Providers.Manager } var hasLocalMetadata = false; + var replaceImages = AllImageTypes.ToList(); + var localImagesFound = false; foreach (var provider in providers.OfType<ILocalMetadataProvider<TItemType>>()) { @@ -698,6 +702,10 @@ namespace MediaBrowser.Providers.Manager await ProviderManager.SaveImage(item, remoteImage.Url, remoteImage.Type, null, cancellationToken).ConfigureAwait(false); refreshResult.UpdateType |= ItemUpdateType.ImageUpdate; + + // remove imagetype that has just been downloaded + replaceImages.Remove(remoteImage.Type); + localImagesFound = true; } catch (HttpRequestException ex) { @@ -705,6 +713,12 @@ namespace MediaBrowser.Providers.Manager } } + if (localImagesFound) + { + options.ReplaceAllImages = false; + options.ReplaceImages = replaceImages; + } + if (imageService.MergeImages(item, localItem.Images)) { refreshResult.UpdateType |= ItemUpdateType.ImageUpdate; From b9f7e3971e453e9faa136de38fe45edd52df253e Mon Sep 17 00:00:00 2001 From: SenorSmartyPants <senorsmartypants@gmail.com> Date: Sat, 18 Mar 2023 21:52:04 -0500 Subject: [PATCH 180/858] Add test for cleaning extra names --- .../Library/LibraryManager/FindExtrasTests.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs index 5995990711..562711337f 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs @@ -79,6 +79,35 @@ public class FindExtrasTests Assert.Equal(ExtraType.Sample, extras[2].ExtraType); } + [Fact] + public void FindExtras_SeparateMovieFolder_CleanExtraNames() + { + var owner = new Movie { Name = "Up", Path = "/movies/Up/Up.mkv" }; + var paths = new List<string> + { + "/movies/Up/Up.mkv", + "/movies/Up/Recording the audio[Bluray]-behindthescenes.mkv", + "/movies/Up/Interview with the dog-interview.mkv", + "/movies/Up/shorts/Balloons[1080p].mkv" + }; + + var files = paths.Select(p => new FileSystemMetadata + { + FullName = p, + IsDirectory = false + }).ToList(); + + var extras = _libraryManager.FindExtras(owner, files, new DirectoryService(_fileSystemMock.Object)).OrderBy(e => e.ExtraType).ToList(); + + Assert.Equal(3, extras.Count); + Assert.Equal(ExtraType.BehindTheScenes, extras[0].ExtraType); + Assert.Equal("Recording the audio", extras[0].Name); + Assert.Equal(ExtraType.Interview, extras[1].ExtraType); + Assert.Equal("Interview with the dog", extras[1].Name); + Assert.Equal(ExtraType.Short, extras[2].ExtraType); + Assert.Equal("Balloons", extras[2].Name); + } + [Fact] public void FindExtras_SeparateMovieFolderWithMixedExtras_FindsCorrectExtras() { From e57c33c4420da4336dacb59c0fc784a7000e71a0 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Sun, 19 Mar 2023 15:07:01 +0100 Subject: [PATCH 181/858] Add DCA and TrueHD to fMP4 audio codecs to support remuxing --- MediaBrowser.Model/Dlna/StreamBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 1e05aea275..21e581801f 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -27,7 +27,7 @@ namespace MediaBrowser.Model.Dlna private readonly ITranscoderSupport _transcoderSupport; private static readonly string[] _supportedHlsVideoCodecs = new string[] { "h264", "hevc" }; private static readonly string[] _supportedHlsAudioCodecsTs = new string[] { "aac", "ac3", "eac3", "mp3" }; - private static readonly string[] _supportedHlsAudioCodecsMp4 = new string[] { "aac", "ac3", "eac3", "mp3", "alac", "flac", "opus" }; + private static readonly string[] _supportedHlsAudioCodecsMp4 = new string[] { "aac", "ac3", "eac3", "mp3", "alac", "flac", "opus", "dca", "truehd" }; /// <summary> /// Initializes a new instance of the <see cref="StreamBuilder"/> class. From 34200a79eabf100eb2cbcbdfea20eeacb7e986c6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 19 Mar 2023 08:23:57 -0600 Subject: [PATCH 182/858] Update dependency Diacritics to v3.3.18 (#9516) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index ba02b21fda..f37eaa11e7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -14,7 +14,7 @@ <PackageVersion Include="BlurHashSharp" Version="1.2.0" /> <PackageVersion Include="CommandLineParser" Version="2.9.1" /> <PackageVersion Include="coverlet.collector" Version="3.2.0" /> - <PackageVersion Include="Diacritics" Version="3.3.14" /> + <PackageVersion Include="Diacritics" Version="3.3.18" /> <PackageVersion Include="DiscUtils.Udf" Version="0.16.13" /> <PackageVersion Include="DotNet.Glob" Version="3.1.3" /> <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.8.6" /> From 6a39882a70fed9f1333f7673292583c08f0904fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=80lex=20Bravo?= <alexbravobosch@gmail.com> Date: Sun, 19 Mar 2023 12:26:45 +0000 Subject: [PATCH 183/858] Translated using Weblate (Catalan) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ca/ --- .../Localization/Core/ca.json | 84 +++++++++---------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json index 1966f69683..26290df4d8 100644 --- a/Emby.Server.Implementations/Localization/Core/ca.json +++ b/Emby.Server.Implementations/Localization/Core/ca.json @@ -5,7 +5,7 @@ "Artists": "Artistes", "AuthenticationSucceededWithUserName": "{0} s'ha autenticat correctament", "Books": "Llibres", - "CameraImageUploadedFrom": "S'ha pujat una nova imatge des de la camera desde {0}", + "CameraImageUploadedFrom": "S'ha pujat una nova imatge de càmera des de {0}", "Channels": "Canals", "ChapterNameValue": "Capítol {0}", "Collections": "Col·leccions", @@ -16,65 +16,65 @@ "Folders": "Carpetes", "Genres": "Gèneres", "HeaderAlbumArtists": "Artistes de l'àlbum", - "HeaderContinueWatching": "Continua Veient", - "HeaderFavoriteAlbums": "Àlbums Preferits", - "HeaderFavoriteArtists": "Artistes Predilectes", - "HeaderFavoriteEpisodes": "Episodis Predilectes", - "HeaderFavoriteShows": "Sèries Predilectes", - "HeaderFavoriteSongs": "Cançons Predilectes", - "HeaderLiveTV": "TV en Directe", + "HeaderContinueWatching": "Continuar veient", + "HeaderFavoriteAlbums": "Àlbums preferits", + "HeaderFavoriteArtists": "Artistes preferits", + "HeaderFavoriteEpisodes": "Episodis preferits", + "HeaderFavoriteShows": "Sèries preferides", + "HeaderFavoriteSongs": "Cançons preferides", + "HeaderLiveTV": "TV en directe", "HeaderNextUp": "A continuació", - "HeaderRecordingGroups": "Grups d'Enregistrament", - "HomeVideos": "Vídeos Domèstics", + "HeaderRecordingGroups": "Grups d'enregistrament", + "HomeVideos": "Vídeos domèstics", "Inherit": "Hereta", - "ItemAddedWithName": "{0} ha estat afegit a la biblioteca", - "ItemRemovedWithName": "{0} ha estat eliminat de la biblioteca", + "ItemAddedWithName": "{0} ha sigut afegit a la biblioteca", + "ItemRemovedWithName": "{0} ha sigut eliminat de la biblioteca", "LabelIpAddressValue": "Adreça IP: {0}", "LabelRunningTimeValue": "Temps en funcionament: {0}", - "Latest": "Darreres", - "MessageApplicationUpdated": "El Servidor de Jellyfin ha estat actualitzat", - "MessageApplicationUpdatedTo": "El Servidor de Jellyfin ha estat actualitzat a {0}", + "Latest": "Darrers", + "MessageApplicationUpdated": "El servidor de Jellyfin ha estat actualitzat", + "MessageApplicationUpdatedTo": "El servidor de Jellyfin ha estat actualitzat a {0}", "MessageNamedServerConfigurationUpdatedWithValue": "La secció {0} de la configuració del servidor ha estat actualitzada", "MessageServerConfigurationUpdated": "S'ha actualitzat la configuració del servidor", "MixedContent": "Contingut barrejat", "Movies": "Pel·lícules", "Music": "Música", - "MusicVideos": "Vídeos Musicals", + "MusicVideos": "Videoclips", "NameInstallFailed": "{0} instal·lació fallida", "NameSeasonNumber": "Temporada {0}", - "NameSeasonUnknown": "Temporada Desconeguda", - "NewVersionIsAvailable": "Una nova versió del Servidor Jellyfin està disponible per descarregar.", - "NotificationOptionApplicationUpdateAvailable": "Actualització d'aplicació disponible", - "NotificationOptionApplicationUpdateInstalled": "Actualització d'aplicació instal·lada", + "NameSeasonUnknown": "Temporada desconeguda", + "NewVersionIsAvailable": "Una nova versió del servidor de Jellyfin està disponible per a descarregar.", + "NotificationOptionApplicationUpdateAvailable": "Actualització de l'aplicació disponible", + "NotificationOptionApplicationUpdateInstalled": "Actualització de l'aplicació instal·lada", "NotificationOptionAudioPlayback": "Reproducció d'àudio iniciada", "NotificationOptionAudioPlaybackStopped": "Reproducció d'àudio aturada", "NotificationOptionCameraImageUploaded": "Imatge de càmera pujada", "NotificationOptionInstallationFailed": "Instal·lació fallida", "NotificationOptionNewLibraryContent": "Nou contingut afegit", - "NotificationOptionPluginError": "Un connector ha fallat", - "NotificationOptionPluginInstalled": "Connector instal·lat", - "NotificationOptionPluginUninstalled": "Connector desinstal·lat", - "NotificationOptionPluginUpdateInstalled": "Actualització de connector instal·lada", + "NotificationOptionPluginError": "Un complement ha fallat", + "NotificationOptionPluginInstalled": "Complement instal·lat", + "NotificationOptionPluginUninstalled": "Complement desinstal·lat", + "NotificationOptionPluginUpdateInstalled": "Actualització de complement instal·lada", "NotificationOptionServerRestartRequired": "Reinici del servidor requerit", "NotificationOptionTaskFailed": "Tasca programada fallida", - "NotificationOptionUserLockedOut": "Usuari tancat", - "NotificationOptionVideoPlayback": "Reproducció de video iniciada", - "NotificationOptionVideoPlaybackStopped": "Reproducció de video aturada", + "NotificationOptionUserLockedOut": "Usuari expulsat", + "NotificationOptionVideoPlayback": "Reproducció de vídeo iniciada", + "NotificationOptionVideoPlaybackStopped": "Reproducció de vídeo aturada", "Photos": "Fotos", "Playlists": "Llistes de reproducció", - "Plugin": "Connector", + "Plugin": "Complement", "PluginInstalledWithName": "{0} ha estat instal·lat", "PluginUninstalledWithName": "{0} ha estat desinstal·lat", "PluginUpdatedWithName": "{0} ha estat actualitzat", "ProviderValue": "Proveïdor: {0}", "ScheduledTaskFailedWithName": "{0} ha fallat", - "ScheduledTaskStartedWithName": "{0} iniciat", + "ScheduledTaskStartedWithName": "{0} s'ha iniciat", "ServerNameNeedsToBeRestarted": "{0} necessita ser reiniciat", "Shows": "Sèries", "Songs": "Cançons", - "StartupEmbyServerIsLoading": "El Servidor de Jellyfin està carregant. Si et plau, prova de nou ben aviat.", + "StartupEmbyServerIsLoading": "El servidor de Jellyfin s'està carregant. Proveu-ho altre cop aviat.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", - "SubtitleDownloadFailureFromForItem": "Els subtítols no s'han pogut baixar de {0} per {1}", + "SubtitleDownloadFailureFromForItem": "Els subtítols per a {1} no s'han pogut baixar de {0}", "Sync": "Sincronitzar", "System": "Sistema", "TvShows": "Sèries de TV", @@ -82,11 +82,11 @@ "UserCreatedWithName": "S'ha creat l'usuari {0}", "UserDeletedWithName": "L'usuari {0} ha estat eliminat", "UserDownloadingItemWithValues": "{0} està descarregant {1}", - "UserLockedOutWithName": "L'usuari {0} ha sigut tancat", + "UserLockedOutWithName": "L'usuari {0} ha sigut expulsat", "UserOfflineFromDevice": "{0} s'ha desconnectat de {1}", "UserOnlineFromDevice": "{0} està connectat des de {1}", "UserPasswordChangedWithName": "La contrasenya ha estat canviada per a l'usuari {0}", - "UserPolicyUpdatedWithName": "La política d'usuari s'ha actualitzat per {0}", + "UserPolicyUpdatedWithName": "La política d'usuari s'ha actualitzat per a {0}", "UserStartedPlayingItemWithValues": "{0} ha començat a reproduir {1}", "UserStoppedPlayingItemWithValues": "{0} ha parat de reproduir {1}", "ValueHasBeenAddedToLibrary": "{0} ha sigut afegit a la teva biblioteca", @@ -94,14 +94,14 @@ "VersionNumber": "Versió {0}", "TaskDownloadMissingSubtitlesDescription": "Cerca a internet els subtítols que faltin a partir de la configuració de metadades.", "TaskDownloadMissingSubtitles": "Descarrega els subtítols que faltin", - "TaskRefreshChannelsDescription": "Actualitza la informació dels canals d'Internet.", - "TaskRefreshChannels": "Actualitza Canals", - "TaskCleanTranscodeDescription": "Elimina els arxius temporals de transcodificacions que tinguin més d'un dia.", + "TaskRefreshChannelsDescription": "Actualitza la informació dels canals d'internet.", + "TaskRefreshChannels": "Actualitza els canals", + "TaskCleanTranscodeDescription": "Elimina els arxius de transcodificacions que tinguin més d'un dia.", "TaskCleanTranscode": "Neteja les transcodificacions", - "TaskUpdatePluginsDescription": "Actualitza les extensions que estan configurades per actualitzar-se automàticament.", - "TaskUpdatePlugins": "Actualitza les extensions", + "TaskUpdatePluginsDescription": "Actualitza els connectors que estan configurats per a actualitzar-se automàticament.", + "TaskUpdatePlugins": "Actualitza els connectors", "TaskRefreshPeopleDescription": "Actualitza les metadades dels actors i directors de la teva mediateca.", - "TaskRefreshPeople": "Actualitza Persones", + "TaskRefreshPeople": "Actualitza les persones", "TaskCleanLogsDescription": "Esborra els logs que tinguin més de {0} dies.", "TaskCleanLogs": "Neteja els registres", "TaskRefreshLibraryDescription": "Escaneja la mediateca buscant fitxers nous i refresca les metadades.", @@ -110,12 +110,12 @@ "TaskRefreshChapterImages": "Extreure les imatges dels capítols", "TaskCleanCacheDescription": "Elimina els arxius temporals que ja no són necessaris per al servidor.", "TaskCleanCache": "Elimina arxius temporals", - "TasksChannelsCategory": "Canals d'Internet", + "TasksChannelsCategory": "Canals d'internet", "TasksApplicationCategory": "Aplicació", "TasksLibraryCategory": "Biblioteca", "TasksMaintenanceCategory": "Manteniment", "TaskCleanActivityLogDescription": "Eliminat entrades del registre d'activitats mes antigues que l'antiguitat configurada.", - "TaskCleanActivityLog": "Buidar Registre d'Activitat", + "TaskCleanActivityLog": "Buidar el registre d'activitat", "Undefined": "Indefinit", "Forced": "Forçat", "Default": "Per defecte", @@ -124,5 +124,5 @@ "TaskKeyframeExtractorDescription": "Extreu fotogrames clau dels fitxers de vídeo per crear llistes de reproducció HLS més precises. Aquesta tasca pot durar molt de temps.", "TaskKeyframeExtractor": "Extractor de fotogrames clau", "External": "Extern", - "HearingImpaired": "Discapacitat Auditiva" + "HearingImpaired": "Discapacitat auditiva" } From e0a7e9baa06fb8efb06aa91fc4d1cd205d0b7d40 Mon Sep 17 00:00:00 2001 From: knackebrot <knackebrot@tfwno.gf> Date: Tue, 21 Mar 2023 15:01:32 +0100 Subject: [PATCH 184/858] Fix audio VBR calculation Pass encoder, not codec --- Jellyfin.Api/Controllers/DynamicHlsController.cs | 4 ++-- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index b827daa3a4..42a7ac2b16 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1693,7 +1693,7 @@ public class DynamicHlsController : BaseJellyfinApiController if (audioBitrate.HasValue && !EncodingHelper.LosslessAudioCodecs.Contains(state.ActualOutputAudioCodec, StringComparison.OrdinalIgnoreCase)) { - var vbrParam = _encodingHelper.GetAudioVbrModeParam(state.OutputAudioCodec, audioBitrate.Value / (audioChannels ?? 2)); + var vbrParam = _encodingHelper.GetAudioVbrModeParam(audioCodec, audioBitrate.Value / (audioChannels ?? 2)); if (_encodingOptions.EnableAudioVbr && vbrParam is not null) { audioTranscodeParams += vbrParam; @@ -1760,7 +1760,7 @@ public class DynamicHlsController : BaseJellyfinApiController var bitrate = state.OutputAudioBitrate; if (bitrate.HasValue && !EncodingHelper.LosslessAudioCodecs.Contains(actualOutputAudioCodec, StringComparison.OrdinalIgnoreCase)) { - var vbrParam = _encodingHelper.GetAudioVbrModeParam(actualOutputAudioCodec, bitrate.Value / (channels ?? 2)); + var vbrParam = _encodingHelper.GetAudioVbrModeParam(audioCodec, bitrate.Value / (channels ?? 2)); if (_encodingOptions.EnableAudioVbr && vbrParam is not null) { args += vbrParam; diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 960b0b7cdf..f7ee3c1732 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -6015,7 +6015,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (bitrate.HasValue && !LosslessAudioCodecs.Contains(outputCodec, StringComparison.OrdinalIgnoreCase)) { - var vbrParam = GetAudioVbrModeParam(outputCodec, bitrate.Value / (channels ?? 2)); + var vbrParam = GetAudioVbrModeParam(GetAudioEncoder(state), bitrate.Value / (channels ?? 2)); if (encodingOptions.EnableAudioVbr && vbrParam is not null) { audioTranscodeParams.Add(vbrParam); From e3a04b5d66722f9604fa6710e0105b5d2baa37a3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 22 Mar 2023 12:55:43 +0000 Subject: [PATCH 185/858] Update actions/stale action to v8 --- .github/workflows/repo-stale.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/repo-stale.yaml b/.github/workflows/repo-stale.yaml index 276bb21501..f8428847b0 100644 --- a/.github/workflows/repo-stale.yaml +++ b/.github/workflows/repo-stale.yaml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest if: ${{ contains(github.repository, 'jellyfin/') }} steps: - - uses: actions/stale@6f05e4244c9a0b2ed3401882b05d701dd0a7289b # v7.0.0 + - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0 with: repo-token: ${{ secrets.JF_BOT_TOKEN }} days-before-stale: 120 From 09147a47cb1821bd8615764fda67ce710e69d311 Mon Sep 17 00:00:00 2001 From: Bill Thornton <billt2006@gmail.com> Date: Wed, 22 Mar 2023 11:09:06 -0400 Subject: [PATCH 186/858] Add merge conflict comment --- .github/workflows/automation.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/automation.yml b/.github/workflows/automation.yml index 4b5571c774..47abce02a3 100644 --- a/.github/workflows/automation.yml +++ b/.github/workflows/automation.yml @@ -19,6 +19,7 @@ jobs: if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}} with: dirtyLabel: 'merge conflict' + commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.' repoToken: ${{ secrets.JF_BOT_TOKEN }} project: From ea76d5497f6e0fd30e27f71b77e64eaed64a96a1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 22 Mar 2023 19:31:32 +0000 Subject: [PATCH 187/858] Update github/codeql-action action to v2.2.8 --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index cdb7970aca..402c0f4f83 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@168b99b3c22180941ae7dbdd5f5c9678ede476ba # v2.2.7 + uses: github/codeql-action/init@67a35a08586135a9573f4327e904ecbf517a882d # v2.2.8 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@168b99b3c22180941ae7dbdd5f5c9678ede476ba # v2.2.7 + uses: github/codeql-action/autobuild@67a35a08586135a9573f4327e904ecbf517a882d # v2.2.8 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@168b99b3c22180941ae7dbdd5f5c9678ede476ba # v2.2.7 + uses: github/codeql-action/analyze@67a35a08586135a9573f4327e904ecbf517a882d # v2.2.8 From e9821b822083055f75b4661438b29e01a9706e89 Mon Sep 17 00:00:00 2001 From: azam <azamshul@gmail.com> Date: Tue, 21 Mar 2023 20:28:30 +0000 Subject: [PATCH 188/858] Translated using Weblate (Malay) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ms/ --- .../Localization/Core/ms.json | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/ms.json b/Emby.Server.Implementations/Localization/Core/ms.json index 3d54a5a950..b2293e4b60 100644 --- a/Emby.Server.Implementations/Localization/Core/ms.json +++ b/Emby.Server.Implementations/Localization/Core/ms.json @@ -39,7 +39,7 @@ "MixedContent": "Kandungan campuran", "Movies": "Filem-filem", "Music": "Muzik", - "MusicVideos": "Video muzik", + "MusicVideos": "Video Muzik", "NameInstallFailed": "{0} pemasangan gagal", "NameSeasonNumber": "Musim {0}", "NameSeasonUnknown": "Musim Tidak Diketahui", @@ -55,7 +55,7 @@ "NotificationOptionPluginInstalled": "Plugin telah dipasang", "NotificationOptionPluginUninstalled": "Plugin telah dinyahpasang", "NotificationOptionPluginUpdateInstalled": "Kemaskini plugin telah dipasang", - "NotificationOptionServerRestartRequired": "", + "NotificationOptionServerRestartRequired": "Perlu mulakan semula server", "NotificationOptionTaskFailed": "Kegagalan tugas berjadual", "NotificationOptionUserLockedOut": "Pengguna telah dikunci", "NotificationOptionVideoPlayback": "Ulangmain video bermula", @@ -109,5 +109,20 @@ "TaskRefreshLibrary": "Imbas Perpustakaan Media", "TaskRefreshChapterImagesDescription": "Membuat gambaran kecil untuk video yang mempunyai bab.", "TaskRefreshChapterImages": "Ekstrak Gambar-gambar Bab", - "TaskCleanCacheDescription": "Menghapuskan fail cache yang tidak lagi diperlukan oleh sistem." + "TaskCleanCacheDescription": "Menghapuskan fail cache yang tidak lagi diperlukan oleh sistem.", + "HearingImpaired": "Lemah Pendengaran", + "TaskRefreshPeopleDescription": "Kemas kini metadata untuk pelakon dan pengarah di dalam perpustakaan media.", + "TaskUpdatePluginsDescription": "Muat turun dan kemas kini plugin yang dikonfigurasi secara automatik.", + "TaskDownloadMissingSubtitlesDescription": "Cari sari kata yang hilang di internet, berdasarkan konfigurasi metadata.", + "TaskOptimizeDatabaseDescription": "Mampatkan pangkalan data dan potong ruang kosong. Pelaksanaan tugas ini selepas pengimbasan perpustakaan boleh membantu membaiki prestasi.", + "TaskRefreshChannels": "Segarkan Saluran-saluran", + "TaskUpdatePlugins": "Kemas kini plugin", + "TaskDownloadMissingSubtitles": "Muat turn sari kata yang tiada", + "TaskCleanTranscodeDescription": "Padam fail transkod yang lebih lama dari satu hari.", + "TaskRefreshChannelsDescription": "Segarkan maklumat saluran internet.", + "TaskCleanTranscode": "Bersihkan direktori transkod", + "External": "Luaran", + "TaskOptimizeDatabase": "Optimumkan pangkalan data", + "TaskKeyframeExtractor": "Ekstrak bingkai kunci", + "TaskKeyframeExtractorDescription": "Ekstrak bingkai kunci dari fail video untuk membina HLS playlist yang lebih tepat. Tugas ini mungkin perlukan masa yang panjang." } From 93d43ffac174a8d9f59d4ae81805a3e0b58f2504 Mon Sep 17 00:00:00 2001 From: azam <azamshul@gmail.com> Date: Tue, 21 Mar 2023 18:10:32 +0000 Subject: [PATCH 189/858] Translated using Weblate (Japanese) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ja/ --- Emby.Server.Implementations/Localization/Core/ja.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/ja.json b/Emby.Server.Implementations/Localization/Core/ja.json index 7f616c35ad..7b059c68ea 100644 --- a/Emby.Server.Implementations/Localization/Core/ja.json +++ b/Emby.Server.Implementations/Localization/Core/ja.json @@ -37,8 +37,8 @@ "MessageNamedServerConfigurationUpdatedWithValue": "サーバー設定項目の {0} が更新されました", "MessageServerConfigurationUpdated": "サーバー設定が更新されました", "MixedContent": "ミックスコンテンツ", - "Movies": "ムービー", - "Music": "ミュージック", + "Movies": "映画", + "Music": "音楽", "MusicVideos": "ミュージックビデオ", "NameInstallFailed": "{0}のインストールに失敗しました", "NameSeasonNumber": "シーズン {0}", From 827afa9417e7fa68f6be3d9b31129e907e7d2118 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 24 Mar 2023 06:29:44 +0000 Subject: [PATCH 190/858] Update actions/checkout action to v3.5.0 --- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/commands.yml | 4 ++-- .github/workflows/openapi.yml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 402c0f4f83..5e54a40c54 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3.4.0 + uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0 - name: Setup .NET uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # v3.0.3 with: diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index fe1317a759..236a11a137 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -24,7 +24,7 @@ jobs: reactions: '+1' - name: Checkout the latest code - uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3.4.0 + uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0 with: token: ${{ secrets.JF_BOT_TOKEN }} fetch-depth: 0 @@ -51,7 +51,7 @@ jobs: reactions: eyes - name: Checkout the latest code - uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3.4.0 + uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0 with: token: ${{ secrets.JF_BOT_TOKEN }} fetch-depth: 0 diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index 8fa8971ec2..c68679cede 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -14,7 +14,7 @@ jobs: permissions: read-all steps: - name: Checkout repository - uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3.4.0 + uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0 with: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} @@ -39,7 +39,7 @@ jobs: permissions: read-all steps: - name: Checkout repository - uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3.4.0 + uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0 with: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} From 7ffe44d70512ab51414a0207afe892d353f4ebf9 Mon Sep 17 00:00:00 2001 From: Calvin Ng <calvin.alexander.ng@gmail.com> Date: Thu, 23 Mar 2023 15:59:02 +0000 Subject: [PATCH 191/858] Translated using Weblate (Filipino) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fil/ --- Emby.Server.Implementations/Localization/Core/fil.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/fil.json b/Emby.Server.Implementations/Localization/Core/fil.json index 99839ae6e8..01b3e95fc3 100644 --- a/Emby.Server.Implementations/Localization/Core/fil.json +++ b/Emby.Server.Implementations/Localization/Core/fil.json @@ -119,5 +119,9 @@ "Undefined": "Hindi tiyak", "Forced": "Sapilitan", "TaskOptimizeDatabaseDescription": "Iko-compact ang database at ita-truncate ang free space. Ang pagpapatakbo ng gawaing ito pagkatapos ng pag-scan sa library o paggawa ng iba pang mga pagbabago na nagpapahiwatig ng mga pagbabago sa database ay maaaring magpa-improve ng performance.", - "TaskOptimizeDatabase": "I-optimize ang database" + "TaskOptimizeDatabase": "I-optimize ang database", + "HearingImpaired": "Bingi", + "TaskKeyframeExtractor": "Tagabunot ng Keyframe", + "TaskKeyframeExtractorDescription": "Nagbubunot ng keyframe mula sa mga bidyo upang makabuo ng mas tumpak na HLS playlist. Maaaring matagal ito gawin.", + "External": "External" } From 9211a73e4011c0c610fdbcf24e0723a3552f22fa Mon Sep 17 00:00:00 2001 From: Shadowghost <Shadowghost@users.noreply.github.com> Date: Sat, 25 Mar 2023 18:41:09 +0100 Subject: [PATCH 192/858] Apply suggestions from code review Co-authored-by: Cody Robibero <cody@robibe.ro> --- Emby.Server.Implementations/Playlists/PlaylistManager.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 4d1b454288..6176879b66 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -530,6 +530,7 @@ namespace Emby.Server.Implementations.Playlists _libraryManager.GetUserRootFolder().Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, TypeName, StringComparison.Ordinal)); } + /// <inheritdoc /> public async Task RemovePlaylistsAsync(Guid userId) { var playlists = GetPlaylists(userId); @@ -564,6 +565,7 @@ namespace Emby.Server.Implementations.Playlists } } + /// <inheritdoc /> public async Task UpdatePlaylistAsync(Playlist playlist) { var currentPlaylist = (Playlist)_libraryManager.GetItemById(playlist.Id); From 89be3aa37f89fcd91c2b68da07b5827510394141 Mon Sep 17 00:00:00 2001 From: Cody Robibero <cody@robibe.ro> Date: Sat, 25 Mar 2023 11:52:02 -0600 Subject: [PATCH 193/858] Convert Person.Type to use PersonKind enum (#9487) --- Emby.Dlna/Didl/DidlBuilder.cs | 20 ++-- .../Data/SqliteItemRepository.cs | 7 +- Emby.Server.Implementations/Dto/DtoService.cs | 12 +-- .../LiveTv/EmbyTV/EmbyTV.cs | 8 +- Jellyfin.Data/Enums/PersonKind.cs | 97 +++++++++++++++++++ .../Entities/PeopleHelper.cs | 23 ++--- .../Entities/PersonInfo.cs | 9 +- .../Parsers/BaseItemXmlParser.cs | 17 ++-- .../Savers/BaseXmlSaver.cs | 4 +- .../Probing/ProbeResultNormalizer.cs | 27 +++--- MediaBrowser.Model/Dto/BaseItemPerson.cs | 3 +- .../MediaInfo/AudioFileProber.cs | 7 +- .../Music/AlbumMetadataService.cs | 5 +- .../Plugins/Omdb/OmdbProvider.cs | 7 +- .../Plugins/Tmdb/Movies/TmdbMovieProvider.cs | 14 +-- .../Plugins/Tmdb/TV/TmdbEpisodeProvider.cs | 7 +- .../Plugins/Tmdb/TV/TmdbSeasonProvider.cs | 5 +- .../Plugins/Tmdb/TV/TmdbSeriesProvider.cs | 7 +- .../Plugins/Tmdb/TmdbUtils.cs | 21 +++- .../Parsers/BaseNfoParser.cs | 25 ++--- .../Savers/BaseNfoSaver.cs | 15 ++- .../Probing/ProbeResultNormalizerTests.cs | 11 ++- .../Parsers/EpisodeNfoProviderTests.cs | 7 +- .../Parsers/MovieNfoParserTests.cs | 9 +- .../Parsers/SeasonNfoProviderTests.cs | 3 +- .../Parsers/SeriesNfoParserTests.cs | 3 +- 26 files changed, 235 insertions(+), 138 deletions(-) create mode 100644 Jellyfin.Data/Enums/PersonKind.cs diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs index bea7a5a0da..f668dc829a 100644 --- a/Emby.Dlna/Didl/DidlBuilder.cs +++ b/Emby.Dlna/Didl/DidlBuilder.cs @@ -10,6 +10,7 @@ using System.Text; using System.Xml; using Emby.Dlna.ContentDirectory; using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; @@ -870,11 +871,11 @@ namespace Emby.Dlna.Didl var types = new[] { - PersonType.Director, - PersonType.Writer, - PersonType.Producer, - PersonType.Composer, - "creator" + PersonKind.Director, + PersonKind.Writer, + PersonKind.Producer, + PersonKind.Composer, + PersonKind.Creator }; // Seeing some LG models locking up due content with large lists of people @@ -888,10 +889,13 @@ namespace Emby.Dlna.Didl foreach (var actor in people) { - var type = types.FirstOrDefault(i => string.Equals(i, actor.Type, StringComparison.OrdinalIgnoreCase) || string.Equals(i, actor.Role, StringComparison.OrdinalIgnoreCase)) - ?? PersonType.Actor; + var type = types.FirstOrDefault(i => i == actor.Type || string.Equals(actor.Role, i.ToString(), StringComparison.OrdinalIgnoreCase)); + if (type == PersonKind.Unknown) + { + type = PersonKind.Actor; + } - AddValue(writer, "upnp", type.ToLowerInvariant(), actor.Name, NsUpnp); + AddValue(writer, "upnp", type.ToString().ToLowerInvariant(), actor.Name, NsUpnp); } } diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 3bf4d07c59..fcff5f98c6 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -5540,7 +5540,7 @@ AND Type = @InternalPersonType)"); statement.TryBind("@Name" + index, person.Name); statement.TryBind("@Role" + index, person.Role); - statement.TryBind("@PersonType" + index, person.Type); + statement.TryBind("@PersonType" + index, person.Type.ToString()); statement.TryBind("@SortOrder" + index, person.SortOrder); statement.TryBind("@ListOrder" + index, listIndex); @@ -5569,9 +5569,10 @@ AND Type = @InternalPersonType)"); item.Role = role; } - if (reader.TryGetString(3, out var type)) + if (reader.TryGetString(3, out var type) + && Enum.TryParse(type, true, out PersonKind personKind)) { - item.Type = type; + item.Type = personKind; } if (reader.TryGetInt32(4, out var sortOrder)) diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 45270de893..8b66829032 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -523,32 +523,32 @@ namespace Emby.Server.Implementations.Dto var people = _libraryManager.GetPeople(item).OrderBy(i => i.SortOrder ?? int.MaxValue) .ThenBy(i => { - if (i.IsType(PersonType.Actor)) + if (i.IsType(PersonKind.Actor)) { return 0; } - if (i.IsType(PersonType.GuestStar)) + if (i.IsType(PersonKind.GuestStar)) { return 1; } - if (i.IsType(PersonType.Director)) + if (i.IsType(PersonKind.Director)) { return 2; } - if (i.IsType(PersonType.Writer)) + if (i.IsType(PersonKind.Writer)) { return 3; } - if (i.IsType(PersonType.Producer)) + if (i.IsType(PersonKind.Producer)) { return 4; } - if (i.IsType(PersonType.Composer)) + if (i.IsType(PersonKind.Composer)) { return 4; } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index 8edd8f66ae..e7f4d2f4eb 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -2032,7 +2032,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV var people = item.Id.Equals(default) ? new List<PersonInfo>() : _libraryManager.GetPeople(item); var directors = people - .Where(i => IsPersonType(i, PersonType.Director)) + .Where(i => i.IsType(PersonKind.Director)) .Select(i => i.Name) .ToList(); @@ -2042,7 +2042,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } var writers = people - .Where(i => IsPersonType(i, PersonType.Writer)) + .Where(i => i.IsType(PersonKind.Writer)) .Select(i => i.Name) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); @@ -2122,10 +2122,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } } - private static bool IsPersonType(PersonInfo person, string type) - => string.Equals(person.Type, type, StringComparison.OrdinalIgnoreCase) - || string.Equals(person.Role, type, StringComparison.OrdinalIgnoreCase); - private LiveTvProgram GetProgramInfoFromCache(string programId) { var query = new InternalItemsQuery diff --git a/Jellyfin.Data/Enums/PersonKind.cs b/Jellyfin.Data/Enums/PersonKind.cs new file mode 100644 index 0000000000..10a8056669 --- /dev/null +++ b/Jellyfin.Data/Enums/PersonKind.cs @@ -0,0 +1,97 @@ +namespace Jellyfin.Data.Enums; + +/// <summary> +/// The person kind. +/// </summary> +public enum PersonKind +{ + /// <summary> + /// An unknown person kind. + /// </summary> + Unknown, + + /// <summary> + /// A person whose profession is acting on the stage, in films, or on television. + /// </summary> + Actor, + + /// <summary> + /// A person who supervises the actors and other staff in a film, play, or similar production. + /// </summary> + Director, + + /// <summary> + /// A person who writes music, especially as a professional occupation. + /// </summary> + Composer, + + /// <summary> + /// A writer of a book, article, or document. Can also be used as a generic term for music writer if there is a lack of specificity. + /// </summary> + Writer, + + /// <summary> + /// A well-known actor or other performer who appears in a work in which they do not have a regular role. + /// </summary> + GuestStar, + + /// <summary> + /// A person responsible for the financial and managerial aspects of the making of a film or broadcast or for staging a play, opera, etc. + /// </summary> + Producer, + + /// <summary> + /// A person who directs the performance of an orchestra or choir. + /// </summary> + Conductor, + + /// <summary> + /// A person who writes the words to a song or musical. + /// </summary> + Lyricist, + + /// <summary> + /// A person who adapts a musical composition for performance. + /// </summary> + Arranger, + + /// <summary> + /// An audio engineer who performed a general engineering role. + /// </summary> + Engineer, + + /// <summary> + /// An engineer responsible for using a mixing console to mix a recorded track into a single piece of music suitable for release. + /// </summary> + Mixer, + + /// <summary> + /// A person who remixed a recording by taking one or more other tracks, substantially altering them and mixing them together with other material. + /// </summary> + Remixer, + + /// <summary> + /// A person who created the material. + /// </summary> + Creator, + + /// <summary> + /// A person who was the artist. + /// </summary> + Artist, + + /// <summary> + /// A person who was the album artist. + /// </summary> + AlbumArtist, + + /// <summary> + /// A person who was the author. + /// </summary> + Author, + + /// <summary> + /// A person who was the illustrator. + /// </summary> + Illustrator, +} diff --git a/MediaBrowser.Controller/Entities/PeopleHelper.cs b/MediaBrowser.Controller/Entities/PeopleHelper.cs index 7f8dc069cf..5292bd7727 100644 --- a/MediaBrowser.Controller/Entities/PeopleHelper.cs +++ b/MediaBrowser.Controller/Entities/PeopleHelper.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Jellyfin.Data.Enums; using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Entities @@ -17,38 +18,38 @@ namespace MediaBrowser.Controller.Entities // Normalize if (string.Equals(person.Role, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase)) { - person.Type = PersonType.GuestStar; + person.Type = PersonKind.GuestStar; } else if (string.Equals(person.Role, PersonType.Director, StringComparison.OrdinalIgnoreCase)) { - person.Type = PersonType.Director; + person.Type = PersonKind.Director; } else if (string.Equals(person.Role, PersonType.Producer, StringComparison.OrdinalIgnoreCase)) { - person.Type = PersonType.Producer; + person.Type = PersonKind.Producer; } else if (string.Equals(person.Role, PersonType.Writer, StringComparison.OrdinalIgnoreCase)) { - person.Type = PersonType.Writer; + person.Type = PersonKind.Writer; } // If the type is GuestStar and there's already an Actor entry, then update it to avoid dupes - if (string.Equals(person.Type, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase)) + if (person.Type == PersonKind.GuestStar) { - var existing = people.FirstOrDefault(p => p.Name.Equals(person.Name, StringComparison.OrdinalIgnoreCase) && p.Type.Equals(PersonType.Actor, StringComparison.OrdinalIgnoreCase)); + var existing = people.FirstOrDefault(p => p.Name.Equals(person.Name, StringComparison.OrdinalIgnoreCase) && p.Type == PersonKind.Actor); if (existing is not null) { - existing.Type = PersonType.GuestStar; + existing.Type = PersonKind.GuestStar; MergeExisting(existing, person); return; } } - if (string.Equals(person.Type, PersonType.Actor, StringComparison.OrdinalIgnoreCase)) + if (person.Type == PersonKind.Actor) { // If the actor already exists without a role and we have one, fill it in - var existing = people.FirstOrDefault(p => p.Name.Equals(person.Name, StringComparison.OrdinalIgnoreCase) && (p.Type.Equals(PersonType.Actor, StringComparison.OrdinalIgnoreCase) || p.Type.Equals(PersonType.GuestStar, StringComparison.OrdinalIgnoreCase))); + var existing = people.FirstOrDefault(p => p.Name.Equals(person.Name, StringComparison.OrdinalIgnoreCase) && (p.Type == PersonKind.Actor || p.Type == PersonKind.GuestStar)); if (existing is null) { // Wasn't there - add it @@ -68,8 +69,8 @@ namespace MediaBrowser.Controller.Entities else { var existing = people.FirstOrDefault(p => - string.Equals(p.Name, person.Name, StringComparison.OrdinalIgnoreCase) && - string.Equals(p.Type, person.Type, StringComparison.OrdinalIgnoreCase)); + string.Equals(p.Name, person.Name, StringComparison.OrdinalIgnoreCase) + && p.Type == person.Type); // Check for dupes based on the combination of Name and Type if (existing is null) diff --git a/MediaBrowser.Controller/Entities/PersonInfo.cs b/MediaBrowser.Controller/Entities/PersonInfo.cs index 2b689ae7e2..3df0b0b785 100644 --- a/MediaBrowser.Controller/Entities/PersonInfo.cs +++ b/MediaBrowser.Controller/Entities/PersonInfo.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using Jellyfin.Data.Enums; using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Entities @@ -36,7 +37,7 @@ namespace MediaBrowser.Controller.Entities /// Gets or sets the type. /// </summary> /// <value>The type.</value> - public string Type { get; set; } + public PersonKind Type { get; set; } /// <summary> /// Gets or sets the ascending sort order. @@ -57,10 +58,6 @@ namespace MediaBrowser.Controller.Entities return Name; } - public bool IsType(string type) - { - return string.Equals(Type, type, StringComparison.OrdinalIgnoreCase) - || string.Equals(Role, type, StringComparison.OrdinalIgnoreCase); - } + public bool IsType(PersonKind type) => Type == type || string.Equals(type.ToString(), Role, StringComparison.OrdinalIgnoreCase); } } diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs index c8912807ea..09abd3c364 100644 --- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Text; using System.Threading; using System.Xml; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; @@ -370,7 +371,7 @@ namespace MediaBrowser.LocalMetadata.Parsers case "Director": { - foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonType.Director })) + foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Director })) { if (string.IsNullOrWhiteSpace(p.Name)) { @@ -385,7 +386,7 @@ namespace MediaBrowser.LocalMetadata.Parsers case "Writer": { - foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonType.Writer })) + foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Writer })) { if (string.IsNullOrWhiteSpace(p.Name)) { @@ -412,7 +413,7 @@ namespace MediaBrowser.LocalMetadata.Parsers else { // Old-style piped string - foreach (var p in SplitNames(actors).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonType.Actor })) + foreach (var p in SplitNames(actors).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Actor })) { if (string.IsNullOrWhiteSpace(p.Name)) { @@ -428,7 +429,7 @@ namespace MediaBrowser.LocalMetadata.Parsers case "GuestStars": { - foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonType.GuestStar })) + foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.GuestStar })) { if (string.IsNullOrWhiteSpace(p.Name)) { @@ -1035,7 +1036,7 @@ namespace MediaBrowser.LocalMetadata.Parsers private IEnumerable<PersonInfo> GetPersonsFromXmlNode(XmlReader reader) { var name = string.Empty; - var type = PersonType.Actor; // If type is not specified assume actor + var type = PersonKind.Actor; // If type is not specified assume actor var role = string.Empty; int? sortOrder = null; @@ -1056,11 +1057,7 @@ namespace MediaBrowser.LocalMetadata.Parsers case "Type": { var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - type = val; - } + _ = Enum.TryParse(val, true, out type); break; } diff --git a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs index d92b504740..0c016746b4 100644 --- a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs +++ b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs @@ -374,8 +374,8 @@ namespace MediaBrowser.LocalMetadata.Savers { await writer.WriteStartElementAsync(null, "Person", null).ConfigureAwait(false); await writer.WriteElementStringAsync(null, "Name", null, person.Name).ConfigureAwait(false); - await writer.WriteElementStringAsync(null, "Type", null, person.Type).ConfigureAwait(false); - await writer.WriteElementStringAsync(null, "Role", null, person.Role).ConfigureAwait(false); + await writer.WriteElementStringAsync(null, "Type", null, person.Type.ToString()).ConfigureAwait(false); + await writer.WriteElementStringAsync(null, "Role", null, person.Role.ToString()).ConfigureAwait(false); if (person.SortOrder.HasValue) { diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index cb482301fe..dce3f0e39d 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Xml; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Dto; @@ -507,7 +508,7 @@ namespace MediaBrowser.MediaEncoding.Probing peoples.Add(new BaseItemPerson { Name = pair.Value, - Type = PersonType.Writer + Type = PersonKind.Writer }); } } @@ -518,7 +519,7 @@ namespace MediaBrowser.MediaEncoding.Probing peoples.Add(new BaseItemPerson { Name = pair.Value, - Type = PersonType.Producer + Type = PersonKind.Producer }); } } @@ -529,7 +530,7 @@ namespace MediaBrowser.MediaEncoding.Probing peoples.Add(new BaseItemPerson { Name = pair.Value, - Type = PersonType.Director + Type = PersonKind.Director }); } } @@ -1163,7 +1164,7 @@ namespace MediaBrowser.MediaEncoding.Probing { foreach (var person in Split(composer, false)) { - people.Add(new BaseItemPerson { Name = person, Type = PersonType.Composer }); + people.Add(new BaseItemPerson { Name = person, Type = PersonKind.Composer }); } } @@ -1171,7 +1172,7 @@ namespace MediaBrowser.MediaEncoding.Probing { foreach (var person in Split(conductor, false)) { - people.Add(new BaseItemPerson { Name = person, Type = PersonType.Conductor }); + people.Add(new BaseItemPerson { Name = person, Type = PersonKind.Conductor }); } } @@ -1179,7 +1180,7 @@ namespace MediaBrowser.MediaEncoding.Probing { foreach (var person in Split(lyricist, false)) { - people.Add(new BaseItemPerson { Name = person, Type = PersonType.Lyricist }); + people.Add(new BaseItemPerson { Name = person, Type = PersonKind.Lyricist }); } } @@ -1195,7 +1196,7 @@ namespace MediaBrowser.MediaEncoding.Probing people.Add(new BaseItemPerson { Name = match.Groups["name"].Value, - Type = PersonType.Actor, + Type = PersonKind.Actor, Role = CultureInfo.InvariantCulture.TextInfo.ToTitleCase(match.Groups["instrument"].Value) }); } @@ -1207,7 +1208,7 @@ namespace MediaBrowser.MediaEncoding.Probing { foreach (var person in Split(writer, false)) { - people.Add(new BaseItemPerson { Name = person, Type = PersonType.Writer }); + people.Add(new BaseItemPerson { Name = person, Type = PersonKind.Writer }); } } @@ -1215,7 +1216,7 @@ namespace MediaBrowser.MediaEncoding.Probing { foreach (var person in Split(arranger, false)) { - people.Add(new BaseItemPerson { Name = person, Type = PersonType.Arranger }); + people.Add(new BaseItemPerson { Name = person, Type = PersonKind.Arranger }); } } @@ -1223,7 +1224,7 @@ namespace MediaBrowser.MediaEncoding.Probing { foreach (var person in Split(engineer, false)) { - people.Add(new BaseItemPerson { Name = person, Type = PersonType.Engineer }); + people.Add(new BaseItemPerson { Name = person, Type = PersonKind.Engineer }); } } @@ -1231,7 +1232,7 @@ namespace MediaBrowser.MediaEncoding.Probing { foreach (var person in Split(mixer, false)) { - people.Add(new BaseItemPerson { Name = person, Type = PersonType.Mixer }); + people.Add(new BaseItemPerson { Name = person, Type = PersonKind.Mixer }); } } @@ -1239,7 +1240,7 @@ namespace MediaBrowser.MediaEncoding.Probing { foreach (var person in Split(remixer, false)) { - people.Add(new BaseItemPerson { Name = person, Type = PersonType.Remixer }); + people.Add(new BaseItemPerson { Name = person, Type = PersonKind.Remixer }); } } @@ -1491,7 +1492,7 @@ namespace MediaBrowser.MediaEncoding.Probing { video.People = people.Split(new[] { ';', '/' }, StringSplitOptions.RemoveEmptyEntries) .Where(i => !string.IsNullOrWhiteSpace(i)) - .Select(i => new BaseItemPerson { Name = i.Trim(), Type = PersonType.Actor }) + .Select(i => new BaseItemPerson { Name = i.Trim(), Type = PersonKind.Actor }) .ToArray(); } diff --git a/MediaBrowser.Model/Dto/BaseItemPerson.cs b/MediaBrowser.Model/Dto/BaseItemPerson.cs index 9c65a2308d..d3bcf492d8 100644 --- a/MediaBrowser.Model/Dto/BaseItemPerson.cs +++ b/MediaBrowser.Model/Dto/BaseItemPerson.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Text.Json.Serialization; +using Jellyfin.Data.Enums; using MediaBrowser.Model.Entities; namespace MediaBrowser.Model.Dto @@ -33,7 +34,7 @@ namespace MediaBrowser.Model.Dto /// Gets or sets the type. /// </summary> /// <value>The type.</value> - public string Type { get; set; } + public PersonKind Type { get; set; } /// <summary> /// Gets or sets the primary image tag. diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index 19b594c1c8..b8578c46f8 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; @@ -163,7 +164,7 @@ namespace MediaBrowser.Providers.MediaInfo PeopleHelper.AddPerson(people, new PersonInfo { Name = albumArtist, - Type = "AlbumArtist" + Type = PersonKind.AlbumArtist }); } @@ -173,7 +174,7 @@ namespace MediaBrowser.Providers.MediaInfo PeopleHelper.AddPerson(people, new PersonInfo { Name = performer, - Type = "Artist" + Type = PersonKind.Artist }); } @@ -182,7 +183,7 @@ namespace MediaBrowser.Providers.MediaInfo PeopleHelper.AddPerson(people, new PersonInfo { Name = composer, - Type = "Composer" + Type = PersonKind.Composer }); } diff --git a/MediaBrowser.Providers/Music/AlbumMetadataService.cs b/MediaBrowser.Providers/Music/AlbumMetadataService.cs index 3476e70009..0ddb2ad67b 100644 --- a/MediaBrowser.Providers/Music/AlbumMetadataService.cs +++ b/MediaBrowser.Providers/Music/AlbumMetadataService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -187,7 +188,7 @@ namespace MediaBrowser.Providers.Music PeopleHelper.AddPerson(people, new PersonInfo { Name = albumArtist, - Type = "AlbumArtist" + Type = PersonKind.AlbumArtist }); } @@ -196,7 +197,7 @@ namespace MediaBrowser.Providers.Music PeopleHelper.AddPerson(people, new PersonInfo { Name = artist, - Type = "Artist" + Type = PersonKind.Artist }); } diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs index dfaba6423b..3fd4ae1fc0 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs @@ -13,6 +13,7 @@ using System.Net.Http.Json; using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using Jellyfin.Extensions.Json; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; @@ -424,7 +425,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb var person = new PersonInfo { Name = result.Director, - Type = PersonType.Director + Type = PersonKind.Director }; itemResult.AddPerson(person); @@ -435,7 +436,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb var person = new PersonInfo { Name = result.Writer, - Type = PersonType.Writer + Type = PersonKind.Writer }; itemResult.AddPerson(person); @@ -454,7 +455,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb var person = new PersonInfo { Name = actor, - Type = PersonType.Actor + Type = PersonKind.Actor }; itemResult.AddPerson(person); diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs index fc72023666..2f62e117eb 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities; @@ -258,7 +259,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies { Name = actor.Name.Trim(), Role = actor.Character, - Type = PersonType.Actor, + Type = PersonKind.Actor, SortOrder = actor.Order }; @@ -278,20 +279,13 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies if (movieResult.Credits?.Crew is not null) { - var keepTypes = new[] - { - PersonType.Director, - PersonType.Writer, - PersonType.Producer - }; - foreach (var person in movieResult.Credits.Crew) { // Normalize this var type = TmdbUtils.MapCrewToPersonType(person); - if (!keepTypes.Contains(type, StringComparison.OrdinalIgnoreCase) && - !keepTypes.Contains(person.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase)) + if (!TmdbUtils.WantedCrewKinds.Contains(type) + && !TmdbUtils.WantedCrewTypes.Contains(person.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase)) { continue; } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs index 66decde842..f18575aa98 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities; @@ -168,7 +169,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV { Name = actor.Name.Trim(), Role = actor.Character, - Type = PersonType.Actor, + Type = PersonKind.Actor, SortOrder = actor.Order }); } @@ -182,7 +183,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV { Name = guest.Name.Trim(), Role = guest.Character, - Type = PersonType.GuestStar, + Type = PersonKind.GuestStar, SortOrder = guest.Order }); } @@ -196,7 +197,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV // Normalize this var type = TmdbUtils.MapCrewToPersonType(person); - if (!TmdbUtils.WantedCrewTypes.Contains(type, StringComparison.OrdinalIgnoreCase) + if (!TmdbUtils.WantedCrewKinds.Contains(type) && !TmdbUtils.WantedCrewTypes.Contains(person.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase)) { continue; diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs index 3cb72b89b5..10efb68b94 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities; @@ -88,7 +89,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV { Name = cast[i].Name.Trim(), Role = cast[i].Character, - Type = PersonType.Actor, + Type = PersonKind.Actor, SortOrder = cast[i].Order }); } @@ -101,7 +102,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV // Normalize this var type = TmdbUtils.MapCrewToPersonType(person); - if (!TmdbUtils.WantedCrewTypes.Contains(type, StringComparison.OrdinalIgnoreCase) + if (!TmdbUtils.WantedCrewKinds.Contains(type) && !TmdbUtils.WantedCrewTypes.Contains(person.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase)) { continue; diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs index 09d1a739d2..8dc2d69385 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities; @@ -352,7 +353,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV { Name = actor.Name.Trim(), Role = actor.Character, - Type = PersonType.Actor, + Type = PersonKind.Actor, SortOrder = actor.Order, ImageUrl = _tmdbClientManager.GetPosterUrl(actor.ProfilePath) }; @@ -380,8 +381,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV // Normalize this var type = TmdbUtils.MapCrewToPersonType(person); - if (!keepTypes.Contains(type, StringComparison.OrdinalIgnoreCase) - && !keepTypes.Contains(person.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase)) + if (!TmdbUtils.WantedCrewKinds.Contains(type) + && !TmdbUtils.WantedCrewTypes.Contains(person.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase)) { continue; } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs index b326d22c87..516eee758c 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Text.RegularExpressions; +using Jellyfin.Data.Enums; using MediaBrowser.Model.Entities; using TMDbLib.Objects.General; @@ -39,6 +40,16 @@ namespace MediaBrowser.Providers.Plugins.Tmdb PersonType.Producer }; + /// <summary> + /// The crew kinds to keep. + /// </summary> + public static readonly PersonKind[] WantedCrewKinds = + { + PersonKind.Director, + PersonKind.Writer, + PersonKind.Producer + }; + /// <summary> /// Cleans the name according to TMDb requirements. /// </summary> @@ -55,26 +66,26 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// </summary> /// <param name="crew">Crew member to map against the Jellyfin person types.</param> /// <returns>The Jellyfin person type.</returns> - public static string MapCrewToPersonType(Crew crew) + public static PersonKind MapCrewToPersonType(Crew crew) { if (crew.Department.Equals("production", StringComparison.OrdinalIgnoreCase) && crew.Job.Contains("director", StringComparison.OrdinalIgnoreCase)) { - return PersonType.Director; + return PersonKind.Director; } if (crew.Department.Equals("production", StringComparison.OrdinalIgnoreCase) && crew.Job.Contains("producer", StringComparison.OrdinalIgnoreCase)) { - return PersonType.Producer; + return PersonKind.Producer; } if (crew.Department.Equals("writing", StringComparison.OrdinalIgnoreCase)) { - return PersonType.Writer; + return PersonKind.Writer; } - return string.Empty; + return PersonKind.Unknown; } /// <summary> diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs index 8bd30447a2..111d0c5cbc 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Text; using System.Threading; using System.Xml; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Providers; @@ -530,7 +531,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers case "director": { var val = reader.ReadElementContentAsString(); - foreach (var p in SplitNames(val).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonType.Director })) + foreach (var p in SplitNames(val).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Director })) { if (string.IsNullOrWhiteSpace(p.Name)) { @@ -552,7 +553,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers var parts = val.Split('/').Select(i => i.Trim()) .Where(i => !string.IsNullOrEmpty(i)); - foreach (var p in parts.Select(v => new PersonInfo { Name = v.Trim(), Type = PersonType.Writer })) + foreach (var p in parts.Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Writer })) { if (string.IsNullOrWhiteSpace(p.Name)) { @@ -569,7 +570,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers case "writer": { var val = reader.ReadElementContentAsString(); - foreach (var p in SplitNames(val).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonType.Writer })) + foreach (var p in SplitNames(val).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Writer })) { if (string.IsNullOrWhiteSpace(p.Name)) { @@ -1206,7 +1207,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers private PersonInfo GetPersonFromXmlNode(XmlReader reader) { var name = string.Empty; - var type = PersonType.Actor; // If type is not specified assume actor + var type = PersonKind.Actor; // If type is not specified assume actor var role = string.Empty; int? sortOrder = null; string? imageUrl = null; @@ -1240,21 +1241,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers case "type": { var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) + if (!Enum.TryParse(val, true, out type)) { - type = val switch - { - PersonType.Composer => PersonType.Composer, - PersonType.Conductor => PersonType.Conductor, - PersonType.Director => PersonType.Director, - PersonType.Lyricist => PersonType.Lyricist, - PersonType.Producer => PersonType.Producer, - PersonType.Writer => PersonType.Writer, - PersonType.GuestStar => PersonType.GuestStar, - // unknown type --> actor - _ => PersonType.Actor - }; + type = PersonKind.Actor; } break; diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs index 130d0bfe40..4f8f869ace 100644 --- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs @@ -10,6 +10,7 @@ using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using System.Xml; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; @@ -485,7 +486,7 @@ namespace MediaBrowser.XbmcMetadata.Savers var people = libraryManager.GetPeople(item); var directors = people - .Where(i => IsPersonType(i, PersonType.Director)) + .Where(i => i.IsType(PersonKind.Director)) .Select(i => i.Name) .ToList(); @@ -495,7 +496,7 @@ namespace MediaBrowser.XbmcMetadata.Savers } var writers = people - .Where(i => IsPersonType(i, PersonType.Writer)) + .Where(i => i.IsType(PersonKind.Writer)) .Select(i => i.Name) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); @@ -913,7 +914,7 @@ namespace MediaBrowser.XbmcMetadata.Savers { foreach (var person in people) { - if (IsPersonType(person, PersonType.Director) || IsPersonType(person, PersonType.Writer)) + if (person.IsType(PersonKind.Director) || person.IsType(PersonKind.Writer)) { continue; } @@ -930,9 +931,9 @@ namespace MediaBrowser.XbmcMetadata.Savers writer.WriteElementString("role", person.Role); } - if (!string.IsNullOrWhiteSpace(person.Type)) + if (person.Type != PersonKind.Unknown) { - writer.WriteElementString("type", person.Type); + writer.WriteElementString("type", person.Type.ToString()); } if (person.SortOrder.HasValue) @@ -969,10 +970,6 @@ namespace MediaBrowser.XbmcMetadata.Savers return libraryManager.GetPathAfterNetworkSubstitution(image.Path); } - private bool IsPersonType(PersonInfo person, string type) - => string.Equals(person.Type, type, StringComparison.OrdinalIgnoreCase) - || string.Equals(person.Role, type, StringComparison.OrdinalIgnoreCase); - private void AddCustomTags(string path, IReadOnlyCollection<string> xmlTagsUsed, XmlWriter writer, ILogger<BaseNfoSaver> logger) { var settings = new XmlReaderSettings() diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs index 6cb98b2b86..198dc63efe 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs @@ -2,6 +2,7 @@ using System; using System.Globalization; using System.IO; using System.Text.Json; +using Jellyfin.Data.Enums; using Jellyfin.Extensions.Json; using Jellyfin.Extensions.Json.Converters; using MediaBrowser.MediaEncoding.Probing; @@ -314,15 +315,15 @@ namespace Jellyfin.MediaEncoding.Tests.Probing Assert.Equal(DateTime.Parse("2020-10-26T00:00Z", DateTimeFormatInfo.CurrentInfo, DateTimeStyles.AdjustToUniversal), res.PremiereDate); Assert.Equal(22, res.People.Length); Assert.Equal("Krysta Youngs", res.People[0].Name); - Assert.Equal(PersonType.Composer, res.People[0].Type); + Assert.Equal(PersonKind.Composer, res.People[0].Type); Assert.Equal("Julia Ross", res.People[1].Name); - Assert.Equal(PersonType.Composer, res.People[1].Type); + Assert.Equal(PersonKind.Composer, res.People[1].Type); Assert.Equal("Yiwoomin", res.People[2].Name); - Assert.Equal(PersonType.Composer, res.People[2].Type); + Assert.Equal(PersonKind.Composer, res.People[2].Type); Assert.Equal("Ji-hyo Park", res.People[3].Name); - Assert.Equal(PersonType.Lyricist, res.People[3].Type); + Assert.Equal(PersonKind.Lyricist, res.People[3].Type); Assert.Equal("Yiwoomin", res.People[4].Name); - Assert.Equal(PersonType.Actor, res.People[4].Type); + Assert.Equal(PersonKind.Actor, res.People[4].Type); Assert.Equal("Electric Piano", res.People[4].Role); Assert.Equal(4, res.Genres.Length); Assert.Contains("Electronic", res.Genres); diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs index 4f4ae5afb9..f63bc0e1bc 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Threading; +using Jellyfin.Data.Enums; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; @@ -79,18 +80,18 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers Assert.Equal("1276153", item.ProviderIds[MetadataProvider.Tmdb.ToString()]); // Credits - var writers = result.People.Where(x => x.Type == PersonType.Writer).ToArray(); + var writers = result.People.Where(x => x.Type == PersonKind.Writer).ToArray(); Assert.Equal(2, writers.Length); Assert.Contains("Bryan Fuller", writers.Select(x => x.Name)); Assert.Contains("Michael Green", writers.Select(x => x.Name)); // Direcotrs - var directors = result.People.Where(x => x.Type == PersonType.Director).ToArray(); + var directors = result.People.Where(x => x.Type == PersonKind.Director).ToArray(); Assert.Single(directors); Assert.Contains("David Slade", directors.Select(x => x.Name)); // Actors - var actors = result.People.Where(x => x.Type == PersonType.Actor).ToArray(); + var actors = result.People.Where(x => x.Type == PersonKind.Actor).ToArray(); Assert.Equal(11, actors.Length); // Only test one actor var shadow = actors.FirstOrDefault(x => x.Role.Equals("Shadow Moon", StringComparison.Ordinal)); diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs index 988abce812..f56f58c6fe 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs @@ -2,6 +2,7 @@ using System; using System.Linq; using System.Threading; using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; @@ -117,18 +118,18 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers Assert.Equal(20, result.People.Count); - var writers = result.People.Where(x => x.Type == PersonType.Writer).ToArray(); + var writers = result.People.Where(x => x.Type == PersonKind.Writer).ToArray(); Assert.Equal(3, writers.Length); var writerNames = writers.Select(x => x.Name); Assert.Contains("Jerry Siegel", writerNames); Assert.Contains("Joe Shuster", writerNames); Assert.Contains("Test", writerNames); - var directors = result.People.Where(x => x.Type == PersonType.Director).ToArray(); + var directors = result.People.Where(x => x.Type == PersonKind.Director).ToArray(); Assert.Single(directors); Assert.Equal("Zack Snyder", directors[0].Name); - var actors = result.People.Where(x => x.Type == PersonType.Actor).ToArray(); + var actors = result.People.Where(x => x.Type == PersonKind.Actor).ToArray(); Assert.Equal(15, actors.Length); // Only test one actor @@ -138,7 +139,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers Assert.Equal(5, aquaman!.SortOrder); Assert.Equal("https://m.media-amazon.com/images/M/MV5BMTI5MTU5NjM1MV5BMl5BanBnXkFtZTcwODc4MDk0Mw@@._V1_SX1024_SY1024_.jpg", aquaman!.ImageUrl); - var lyricist = result.People.FirstOrDefault(x => x.Type == PersonType.Lyricist); + var lyricist = result.People.FirstOrDefault(x => x.Type == PersonKind.Lyricist); Assert.NotNull(lyricist); Assert.Equal("Test Lyricist", lyricist!.Name); diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs index 31110dbd7d..e69ca996cc 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Threading; +using Jellyfin.Data.Enums; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; @@ -60,7 +61,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers Assert.Equal(10, result.People.Count); - Assert.True(result.People.All(x => x.Type == PersonType.Actor)); + Assert.True(result.People.All(x => x.Type == PersonKind.Actor)); // Only test one actor var nini = result.People.FirstOrDefault(x => x.Role.Equals("Nini", StringComparison.Ordinal)); diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeriesNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeriesNfoParserTests.cs index bdedae205b..542324e78b 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeriesNfoParserTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeriesNfoParserTests.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Threading; +using Jellyfin.Data.Enums; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; @@ -67,7 +68,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers Assert.Equal(6, result.People.Count); - Assert.True(result.People.All(x => x.Type == PersonType.Actor)); + Assert.True(result.People.All(x => x.Type == PersonKind.Actor)); // Only test one actor var sweeney = result.People.FirstOrDefault(x => x.Role.Equals("Mad Sweeney", StringComparison.Ordinal)); From 16bae527e56432fdc549882ae6f68505518f233c Mon Sep 17 00:00:00 2001 From: Bas <weblate@hanka.mp> Date: Sat, 25 Mar 2023 16:32:35 +0000 Subject: [PATCH 194/858] Translated using Weblate (Dutch) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/nl/ --- Emby.Server.Implementations/Localization/Core/nl.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index 01a2ab273f..4eb00d2896 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -8,7 +8,7 @@ "CameraImageUploadedFrom": "Nieuwe camera-afbeelding toegevoegd vanaf {0}", "Channels": "Kanalen", "ChapterNameValue": "Hoofdstuk {0}", - "Collections": "Verzamelingen", + "Collections": "Collecties", "DeviceOfflineWithName": "Verbinding met {0} is verbroken", "DeviceOnlineWithName": "{0} is verbonden", "FailedLoginAttemptWithUserName": "Mislukte inlogpoging van {0}", From 8316bd590e4553953c7ca78e6eef26672fbcb416 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Mon, 27 Mar 2023 15:22:21 +0200 Subject: [PATCH 195/858] Fix #7610 --- Jellyfin.Api/Controllers/LibraryController.cs | 46 +++++++++++++------ .../Controllers/LibraryControllerTests.cs | 23 ++++++++++ 2 files changed, 56 insertions(+), 13 deletions(-) diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index bf59febed8..e094d2d774 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -15,6 +15,7 @@ using Jellyfin.Api.Models.LibraryDtos; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Extensions; +using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Progress; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; @@ -332,12 +333,26 @@ public class LibraryController : BaseJellyfinApiController [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult DeleteItem(Guid itemId) { - var item = _libraryManager.GetItemById(itemId); - var user = _userManager.GetUserById(User.GetUserId()); + var isApiKey = User.GetIsApiKey(); + var userId = User.GetUserId(); + var user = !isApiKey && !userId.Equals(default) + ? _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException() + : null; + if (!isApiKey && user is null) + { + return Unauthorized("Unauthorized access"); + } - if (!item.CanDelete(user)) + var item = _libraryManager.GetItemById(itemId); + if (item is null) + { + return NotFound(); + } + + if (user is not null && !item.CanDelete(user)) { return Unauthorized("Unauthorized access"); } @@ -361,26 +376,31 @@ public class LibraryController : BaseJellyfinApiController [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids) { - if (ids.Length == 0) + var isApiKey = User.GetIsApiKey(); + var userId = User.GetUserId(); + var user = !isApiKey && !userId.Equals(default) + ? _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException() + : null; + + if (!isApiKey && user is null) { - return NoContent(); + return Unauthorized("Unauthorized access"); } foreach (var i in ids) { var item = _libraryManager.GetItemById(i); - var user = _userManager.GetUserById(User.GetUserId()); - - if (!item.CanDelete(user)) + if (item is null) { - if (ids.Length > 1) - { - return Unauthorized("Unauthorized access"); - } + return NotFound(); + } - continue; + if (user is not null && !item.CanDelete(user)) + { + return Unauthorized("Unauthorized access"); } _libraryManager.DeleteItem( diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs index 013d19a9fd..8998683a79 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs @@ -37,4 +37,27 @@ public sealed class LibraryControllerTests : IClassFixture<JellyfinApplicationFa var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid())).ConfigureAwait(false); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } + + [Theory] + [InlineData("Items/{0}")] + [InlineData("Items?ids={0}")] + public async Task Delete_NonExistentItemId_Unauthorised(string format) + { + var client = _factory.CreateClient(); + + var response = await client.DeleteAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid())).ConfigureAwait(false); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + } + + [Theory] + [InlineData("Items/{0}")] + [InlineData("Items?ids={0}")] + public async Task Delete_NonExistentItemId_NotFound(string format) + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + var response = await client.DeleteAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid())).ConfigureAwait(false); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } } From c89e9e4812ec85d126d20a6707bc31585d03c272 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 Mar 2023 14:08:12 +0000 Subject: [PATCH 196/858] Update github/codeql-action action to v2.2.9 --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 5e54a40c54..73aafe2732 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@67a35a08586135a9573f4327e904ecbf517a882d # v2.2.8 + uses: github/codeql-action/init@04df1262e6247151b5ac09cd2c303ac36ad3f62b # v2.2.9 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@67a35a08586135a9573f4327e904ecbf517a882d # v2.2.8 + uses: github/codeql-action/autobuild@04df1262e6247151b5ac09cd2c303ac36ad3f62b # v2.2.9 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@67a35a08586135a9573f4327e904ecbf517a882d # v2.2.8 + uses: github/codeql-action/analyze@04df1262e6247151b5ac09cd2c303ac36ad3f62b # v2.2.9 From 2c32d093487b2d0140669a2aa061919130992b02 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Mon, 27 Mar 2023 23:54:02 +0200 Subject: [PATCH 197/858] Don't add .spc audio files (#9034) --- Emby.Naming/Common/NamingOptions.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index 17d77837f0..a069da1022 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -270,7 +270,6 @@ namespace Emby.Naming.Common ".sfx", ".shn", ".sid", - ".spc", ".stm", ".strm", ".ult", From 726bc347aa022ed09747690009c04a014046edf3 Mon Sep 17 00:00:00 2001 From: Bill Thornton <thornbill@users.noreply.github.com> Date: Thu, 30 Mar 2023 08:40:29 -0400 Subject: [PATCH 198/858] Add action to close PRs with merge conflicts (#9561) --- .github/workflows/repo-stale.yaml | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/.github/workflows/repo-stale.yaml b/.github/workflows/repo-stale.yaml index f8428847b0..acbdcd1b75 100644 --- a/.github/workflows/repo-stale.yaml +++ b/.github/workflows/repo-stale.yaml @@ -1,4 +1,4 @@ -name: Issue Stale Check +name: Stale Check on: schedule: @@ -7,8 +7,11 @@ on: permissions: issues: write + pull-requests: write + jobs: - stale: + issues: + name: Check issues runs-on: ubuntu-latest if: ${{ contains(github.repository, 'jellyfin/') }} steps: @@ -28,3 +31,21 @@ jobs: If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or master branch, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label. This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://docs.jellyfin.org/general/getting-help.html). + + prs-conflicts: + name: Check PRs with merge conflicts + runs-on: ubuntu-latest + if: ${{ contains(github.repository, 'jellyfin/') }} + steps: + - uses: actions/stale@6f05e4244c9a0b2ed3401882b05d701dd0a7289b # v7.0.0 + with: + repo-token: ${{ secrets.JF_BOT_TOKEN }} + operations-per-run: 75 + # The merge conflict action will remove the label when updated + remove-stale-when-updated: false + days-before-stale: -1 + days-before-close: 90 + days-before-issue-close: -1 + stale-pr-label: merge conflict + close-pr-message: |- + This PR has been closed due to having unresolved merge conflicts. From d45cabfa74263b4f11945fc88daeffa75ed77570 Mon Sep 17 00:00:00 2001 From: Marc Brooks <IDisposable@gmail.com> Date: Thu, 30 Mar 2023 07:41:27 -0500 Subject: [PATCH 199/858] Fix migration for MusicBrainz (#9559) --- .../MigrateMusicBrainzTimeout.cs | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs index 14b51bd4c4..bee135efda 100644 --- a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs +++ b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs @@ -44,9 +44,7 @@ public class MigrateMusicBrainzTimeout : IMigrationRoutine return; } - var serverConfigSerializer = new XmlSerializer(typeof(OldMusicBrainzConfiguration), new XmlRootAttribute("PluginConfiguration")); - using var xmlReader = XmlReader.Create(path); - var oldPluginConfiguration = serverConfigSerializer.Deserialize(xmlReader) as OldMusicBrainzConfiguration; + var oldPluginConfiguration = ReadOld(path); if (oldPluginConfiguration is not null) { @@ -55,10 +53,25 @@ public class MigrateMusicBrainzTimeout : IMigrationRoutine newPluginConfiguration.ReplaceArtistName = oldPluginConfiguration.ReplaceArtistName; var newRateLimit = oldPluginConfiguration.RateLimit / 1000.0; newPluginConfiguration.RateLimit = newRateLimit < 1.0 ? 1.0 : newRateLimit; + WriteNew(path, newPluginConfiguration); + } + } - var pluginConfigurationSerializer = new XmlSerializer(typeof(PluginConfiguration), new XmlRootAttribute("PluginConfiguration")); - var xmlWriterSettings = new XmlWriterSettings { Indent = true }; - using var xmlWriter = XmlWriter.Create(path, xmlWriterSettings); + private OldMusicBrainzConfiguration? ReadOld(string path) + { + using (var xmlReader = XmlReader.Create(path)) + { + var serverConfigSerializer = new XmlSerializer(typeof(OldMusicBrainzConfiguration), new XmlRootAttribute("PluginConfiguration")); + return serverConfigSerializer.Deserialize(xmlReader) as OldMusicBrainzConfiguration; + } + } + + private void WriteNew(string path, PluginConfiguration newPluginConfiguration) + { + var pluginConfigurationSerializer = new XmlSerializer(typeof(PluginConfiguration), new XmlRootAttribute("PluginConfiguration")); + var xmlWriterSettings = new XmlWriterSettings { Indent = true }; + using (var xmlWriter = XmlWriter.Create(path, xmlWriterSettings)) + { pluginConfigurationSerializer.Serialize(xmlWriter, newPluginConfiguration); } } From 891b9f7a997ce5e5892c1b0f166a921ff07abf68 Mon Sep 17 00:00:00 2001 From: AmbulantRex <21176662+AmbulantRex@users.noreply.github.com> Date: Thu, 30 Mar 2023 08:59:21 -0600 Subject: [PATCH 200/858] Add DLL whitelist support for plugins --- .../Library/PathExtensions.cs | 99 ++++++++++--- .../Plugins/PluginManager.cs | 83 ++++++++++- MediaBrowser.Common/Plugins/PluginManifest.cs | 9 ++ MediaBrowser.Model/Updates/VersionInfo.cs | 8 ++ .../Library/PathExtensionsTests.cs | 43 ++++++ .../Plugins/PluginManagerTests.cs | 135 ++++++++++++++++++ .../Test Data/Updates/manifest-stable.json | 10 +- .../Updates/InstallationManagerTests.cs | 14 ++ 8 files changed, 375 insertions(+), 26 deletions(-) diff --git a/Emby.Server.Implementations/Library/PathExtensions.cs b/Emby.Server.Implementations/Library/PathExtensions.cs index 64e7d54466..7e0d0b78d5 100644 --- a/Emby.Server.Implementations/Library/PathExtensions.cs +++ b/Emby.Server.Implementations/Library/PathExtensions.cs @@ -1,6 +1,9 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; using MediaBrowser.Common.Providers; +using Nikse.SubtitleEdit.Core.Common; namespace Emby.Server.Implementations.Library { @@ -86,24 +89,8 @@ namespace Emby.Server.Implementations.Library return false; } - char oldDirectorySeparatorChar; - char newDirectorySeparatorChar; - // True normalization is still not possible https://github.com/dotnet/runtime/issues/2162 - // The reasoning behind this is that a forward slash likely means it's a Linux path and - // so the whole path should be normalized to use / and vice versa for Windows (although Windows doesn't care much). - if (newSubPath.Contains('/', StringComparison.Ordinal)) - { - oldDirectorySeparatorChar = '\\'; - newDirectorySeparatorChar = '/'; - } - else - { - oldDirectorySeparatorChar = '/'; - newDirectorySeparatorChar = '\\'; - } - - path = path.Replace(oldDirectorySeparatorChar, newDirectorySeparatorChar); - subPath = subPath.Replace(oldDirectorySeparatorChar, newDirectorySeparatorChar); + subPath = subPath.NormalizePath(out var newDirectorySeparatorChar)!; + path = path.NormalizePath(newDirectorySeparatorChar)!; // We have to ensure that the sub path ends with a directory separator otherwise we'll get weird results // when the sub path matches a similar but in-complete subpath @@ -120,12 +107,86 @@ namespace Emby.Server.Implementations.Library return false; } - var newSubPathTrimmed = newSubPath.AsSpan().TrimEnd(newDirectorySeparatorChar); + var newSubPathTrimmed = newSubPath.AsSpan().TrimEnd((char)newDirectorySeparatorChar!); // Ensure that the path with the old subpath removed starts with a leading dir separator int idx = oldSubPathEndsWithSeparator ? subPath.Length - 1 : subPath.Length; newPath = string.Concat(newSubPathTrimmed, path.AsSpan(idx)); return true; } + + /// <summary> + /// Retrieves the full resolved path and normalizes path separators to the <see cref="Path.DirectorySeparatorChar"/>. + /// </summary> + /// <param name="path">The path to canonicalize.</param> + /// <returns>The fully expanded, normalized path.</returns> + public static string Canonicalize(this string path) + { + return Path.GetFullPath(path).NormalizePath()!; + } + + /// <summary> + /// Normalizes the path's directory separator character to the currently defined <see cref="Path.DirectorySeparatorChar"/>. + /// </summary> + /// <param name="path">The path to normalize.</param> + /// <returns>The normalized path string or <see langword="null"/> if the input path is null or empty.</returns> + public static string? NormalizePath(this string? path) + { + return path.NormalizePath(Path.DirectorySeparatorChar); + } + + /// <summary> + /// Normalizes the path's directory separator character. + /// </summary> + /// <param name="path">The path to normalize.</param> + /// <param name="separator">The separator character the path now uses or <see langword="null"/>.</param> + /// <returns>The normalized path string or <see langword="null"/> if the input path is null or empty.</returns> + public static string? NormalizePath(this string? path, out char separator) + { + if (string.IsNullOrEmpty(path)) + { + separator = default; + return path; + } + + var newSeparator = '\\'; + + // True normalization is still not possible https://github.com/dotnet/runtime/issues/2162 + // The reasoning behind this is that a forward slash likely means it's a Linux path and + // so the whole path should be normalized to use / and vice versa for Windows (although Windows doesn't care much). + if (path.Contains('/', StringComparison.Ordinal)) + { + newSeparator = '/'; + } + + separator = newSeparator; + + return path?.NormalizePath(newSeparator); + } + + /// <summary> + /// Normalizes the path's directory separator character to the specified character. + /// </summary> + /// <param name="path">The path to normalize.</param> + /// <param name="newSeparator">The replacement directory separator character. Must be a valid directory separator.</param> + /// <returns>The normalized path.</returns> + /// <exception cref="ArgumentException">Thrown if the new separator character is not a directory separator.</exception> + public static string? NormalizePath(this string? path, char newSeparator) + { + const char Bs = '\\'; + const char Fs = '/'; + + if (!(newSeparator == Bs || newSeparator == Fs)) + { + throw new ArgumentException("The character must be a directory separator."); + } + + if (string.IsNullOrEmpty(path)) + { + return path; + } + + return newSeparator == Bs ? path?.Replace(Fs, newSeparator) : path?.Replace(Bs, newSeparator); + } } } diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs index 7c23254a12..a5c55c8a09 100644 --- a/Emby.Server.Implementations/Plugins/PluginManager.cs +++ b/Emby.Server.Implementations/Plugins/PluginManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Data; using System.Globalization; using System.IO; using System.Linq; @@ -9,6 +10,8 @@ using System.Runtime.Loader; using System.Text; using System.Text.Json; using System.Threading.Tasks; +using Emby.Server.Implementations.Library; +using Jellyfin.Extensions; using Jellyfin.Extensions.Json; using Jellyfin.Extensions.Json.Converters; using MediaBrowser.Common; @@ -19,8 +22,11 @@ using MediaBrowser.Model.Configuration; using MediaBrowser.Model.IO; using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Updates; +using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Nikse.SubtitleEdit.Core.Common; +using SQLitePCL.pretty; namespace Emby.Server.Implementations.Plugins { @@ -44,7 +50,7 @@ namespace Emby.Server.Implementations.Plugins /// <summary> /// Initializes a new instance of the <see cref="PluginManager"/> class. /// </summary> - /// <param name="logger">The <see cref="ILogger"/>.</param> + /// <param name="logger">The <see cref="ILogger{PluginManager}"/>.</param> /// <param name="appHost">The <see cref="IApplicationHost"/>.</param> /// <param name="config">The <see cref="ServerConfiguration"/>.</param> /// <param name="pluginsPath">The plugin path.</param> @@ -424,7 +430,8 @@ namespace Emby.Server.Implementations.Plugins Version = versionInfo.Version, Status = status == PluginStatus.Disabled ? PluginStatus.Disabled : PluginStatus.Active, // Keep disabled state. AutoUpdate = true, - ImagePath = imagePath + ImagePath = imagePath, + Assemblies = versionInfo.Assemblies }; return SaveManifest(manifest, path); @@ -688,7 +695,15 @@ namespace Emby.Server.Implementations.Plugins var entry = versions[x]; if (!string.Equals(lastName, entry.Name, StringComparison.OrdinalIgnoreCase)) { - entry.DllFiles = Directory.GetFiles(entry.Path, "*.dll", SearchOption.AllDirectories); + if (!TryGetPluginDlls(entry, out var allowedDlls)) + { + _logger.LogError("One or more assembly paths was invalid. Marking plugin {Plugin} as \"Malfunctioned\".", entry.Name); + ChangePluginState(entry, PluginStatus.Malfunctioned); + continue; + } + + entry.DllFiles = allowedDlls; + if (entry.IsEnabledAndSupported) { lastName = entry.Name; @@ -734,6 +749,68 @@ namespace Emby.Server.Implementations.Plugins return versions.Where(p => p.DllFiles.Count != 0); } + /// <summary> + /// Attempts to retrieve valid DLLs from the plugin path. This method will consider the assembly whitelist + /// from the manifest. + /// </summary> + /// <remarks> + /// Loading DLLs from externally supplied paths introduces a path traversal risk. This method + /// uses a safelisting tactic of considering DLLs from the plugin directory and only using + /// the plugin's canonicalized assembly whitelist for comparison. See + /// <see href="https://owasp.org/www-community/attacks/Path_Traversal"/> for more details. + /// </remarks> + /// <param name="plugin">The plugin.</param> + /// <param name="whitelistedDlls">The whitelisted DLLs. If the method returns <see langword="false"/>, this will be empty.</param> + /// <returns> + /// <see langword="true"/> if all assemblies listed in the manifest were available in the plugin directory. + /// <see langword="false"/> if any assemblies were invalid or missing from the plugin directory. + /// </returns> + /// <exception cref="ArgumentNullException">If the <see cref="LocalPlugin"/> is null.</exception> + private bool TryGetPluginDlls(LocalPlugin plugin, out IReadOnlyList<string> whitelistedDlls) + { + _ = plugin ?? throw new ArgumentNullException(nameof(plugin)); + + IReadOnlyList<string> pluginDlls = Directory.GetFiles(plugin.Path, "*.dll", SearchOption.AllDirectories); + + whitelistedDlls = Array.Empty<string>(); + if (pluginDlls.Count > 0 && plugin.Manifest.Assemblies.Count > 0) + { + _logger.LogInformation("Registering whitelisted assemblies for plugin \"{Plugin}\"...", plugin.Name); + + var canonicalizedPaths = new List<string>(); + foreach (var path in plugin.Manifest.Assemblies) + { + var canonicalized = Path.Combine(plugin.Path, path).Canonicalize(); + + // Ensure we stay in the plugin directory. + if (!canonicalized.StartsWith(plugin.Path.NormalizePath()!, StringComparison.Ordinal)) + { + _logger.LogError("Assembly path {Path} is not inside the plugin directory.", path); + return false; + } + + canonicalizedPaths.Add(canonicalized); + } + + var intersected = pluginDlls.Intersect(canonicalizedPaths).ToList(); + + if (intersected.Count != canonicalizedPaths.Count) + { + _logger.LogError("Plugin {Plugin} contained assembly paths that were not found in the directory.", plugin.Name); + return false; + } + + whitelistedDlls = intersected; + } + else + { + // No whitelist, default to loading all DLLs in plugin directory. + whitelistedDlls = pluginDlls; + } + + return true; + } + /// <summary> /// Changes the status of the other versions of the plugin to "Superceded". /// </summary> diff --git a/MediaBrowser.Common/Plugins/PluginManifest.cs b/MediaBrowser.Common/Plugins/PluginManifest.cs index 2910dbe144..2bad3454d1 100644 --- a/MediaBrowser.Common/Plugins/PluginManifest.cs +++ b/MediaBrowser.Common/Plugins/PluginManifest.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Text.Json.Serialization; using MediaBrowser.Model.Plugins; @@ -23,6 +24,7 @@ namespace MediaBrowser.Common.Plugins Overview = string.Empty; TargetAbi = string.Empty; Version = string.Empty; + Assemblies = new List<string>(); } /// <summary> @@ -104,5 +106,12 @@ namespace MediaBrowser.Common.Plugins /// </summary> [JsonPropertyName("imagePath")] public string? ImagePath { get; set; } + + /// <summary> + /// Gets or sets the collection of assemblies that should be loaded. + /// Paths are considered relative to the plugin folder. + /// </summary> + [JsonPropertyName("assemblies")] + public IList<string> Assemblies { get; set; } } } diff --git a/MediaBrowser.Model/Updates/VersionInfo.cs b/MediaBrowser.Model/Updates/VersionInfo.cs index 320199f984..1e24bde84e 100644 --- a/MediaBrowser.Model/Updates/VersionInfo.cs +++ b/MediaBrowser.Model/Updates/VersionInfo.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Generic; using System.Text.Json.Serialization; using SysVersion = System.Version; @@ -73,5 +75,11 @@ namespace MediaBrowser.Model.Updates /// </summary> [JsonPropertyName("repositoryUrl")] public string RepositoryUrl { get; set; } = string.Empty; + + /// <summary> + /// Gets or sets the assemblies whitelist for this version. + /// </summary> + [JsonPropertyName("assemblies")] + public IList<string> Assemblies { get; set; } = Array.Empty<string>(); } } diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs index be2dfe0a86..c33a957e69 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using Emby.Server.Implementations.Library; using Xunit; @@ -73,5 +74,47 @@ namespace Jellyfin.Server.Implementations.Tests.Library Assert.False(PathExtensions.TryReplaceSubPath(path, subPath, newSubPath, out var result)); Assert.Null(result); } + + [Theory] + [InlineData(null, '/', null)] + [InlineData(null, '\\', null)] + [InlineData("/home/jeff/myfile.mkv", '\\', "\\home\\jeff\\myfile.mkv")] + [InlineData("C:\\Users\\Jeff\\myfile.mkv", '/', "C:/Users/Jeff/myfile.mkv")] + [InlineData("\\home/jeff\\myfile.mkv", '\\', "\\home\\jeff\\myfile.mkv")] + [InlineData("\\home/jeff\\myfile.mkv", '/', "/home/jeff/myfile.mkv")] + [InlineData("", '/', "")] + public void NormalizePath_SpecifyingSeparator_Normalizes(string path, char separator, string expectedPath) + { + Assert.Equal(expectedPath, path.NormalizePath(separator)); + } + + [Theory] + [InlineData("/home/jeff/myfile.mkv")] + [InlineData("C:\\Users\\Jeff\\myfile.mkv")] + [InlineData("\\home/jeff\\myfile.mkv")] + public void NormalizePath_NoArgs_UsesDirectorySeparatorChar(string path) + { + var separator = Path.DirectorySeparatorChar; + + Assert.Equal(path.Replace('\\', separator).Replace('/', separator), path.NormalizePath()); + } + + [Theory] + [InlineData("/home/jeff/myfile.mkv", '/')] + [InlineData("C:\\Users\\Jeff\\myfile.mkv", '\\')] + [InlineData("\\home/jeff\\myfile.mkv", '/')] + public void NormalizePath_OutVar_Correct(string path, char expectedSeparator) + { + var result = path.NormalizePath(out var separator); + + Assert.Equal(expectedSeparator, separator); + Assert.Equal(path.Replace('\\', separator).Replace('/', separator), result); + } + + [Fact] + public void NormalizePath_SpecifyInvalidSeparator_ThrowsException() + { + Assert.Throws<ArgumentException>(() => string.Empty.NormalizePath('a')); + } } } diff --git a/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs index bc6a447410..d9fdc96f55 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs @@ -1,7 +1,12 @@ using System; using System.IO; +using System.Text.Json; +using Emby.Server.Implementations.Library; using Emby.Server.Implementations.Plugins; +using Jellyfin.Extensions.Json; +using Jellyfin.Extensions.Json.Converters; using MediaBrowser.Common.Plugins; +using MediaBrowser.Model.Plugins; using Microsoft.Extensions.Logging.Abstractions; using Xunit; @@ -40,6 +45,136 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins Assert.Equal(manifest.Status, res.Manifest.Status); Assert.Equal(manifest.AutoUpdate, res.Manifest.AutoUpdate); Assert.Equal(manifest.ImagePath, res.Manifest.ImagePath); + Assert.Equal(manifest.Assemblies, res.Manifest.Assemblies); + } + + /// <summary> + /// Tests safe traversal within the plugin directory. + /// </summary> + /// <param name="dllFile">The safe path to evaluate.</param> + [Theory] + [InlineData("./some.dll")] + [InlineData("some.dll")] + [InlineData("sub/path/some.dll")] + public void Constructor_DiscoversSafePluginAssembly_Status_Active(string dllFile) + { + var manifest = new PluginManifest + { + Id = Guid.NewGuid(), + Name = "Safe Assembly", + Assemblies = new string[] { dllFile } + }; + + var filename = Path.GetFileName(dllFile)!; + var (tempPath, pluginPath) = GetTestPaths("safe"); + + Directory.CreateDirectory(Path.Combine(pluginPath, dllFile.Replace(filename, string.Empty, StringComparison.OrdinalIgnoreCase))); + File.Create(Path.Combine(pluginPath, dllFile)); + + var options = GetTestSerializerOptions(); + var data = JsonSerializer.Serialize(manifest, options); + var metafilePath = Path.Combine(tempPath, "safe", "meta.json"); + + File.WriteAllText(metafilePath, data); + + var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, tempPath, new Version(1, 0)); + + var res = JsonSerializer.Deserialize<PluginManifest>(File.ReadAllText(metafilePath), options); + + var expectedFullPath = Path.Combine(pluginPath, dllFile).Canonicalize(); + + Assert.NotNull(res); + Assert.NotEmpty(pluginManager.Plugins); + Assert.Equal(PluginStatus.Active, res!.Status); + Assert.Equal(expectedFullPath, pluginManager.Plugins[0].DllFiles[0]); + Assert.StartsWith(Path.Combine(tempPath, "safe"), expectedFullPath, StringComparison.InvariantCulture); + } + + /// <summary> + /// Tests unsafe attempts to traverse to higher directories. + /// </summary> + /// <remarks> + /// Attempts to load directories outside of the plugin should be + /// constrained. Path traversal, shell expansion, and double encoding + /// can be used to load unintended files. + /// See <see href="https://owasp.org/www-community/attacks/Path_Traversal"/> for more. + /// </remarks> + /// <param name="unsafePath">The unsafe path to evaluate.</param> + [Theory] + [InlineData("/some.dll")] // Root path. + [InlineData("../some.dll")] // Simple traversal. + [InlineData("C:\\some.dll")] // Windows root path. + [InlineData("test.txt")] // Not a DLL + [InlineData(".././.././../some.dll")] // Traversal with current and parent + [InlineData("..\\.\\..\\.\\..\\some.dll")] // Windows traversal with current and parent + [InlineData("\\\\network\\resource.dll")] // UNC Path + [InlineData("https://jellyfin.org/some.dll")] // URL + [InlineData("....//....//some.dll")] // Path replacement risk if a single "../" replacement occurs. + [InlineData("~/some.dll")] // Tilde poses a shell expansion risk, but is a valid path character. + public void Constructor_DiscoversUnsafePluginAssembly_Status_Malfunctioned(string unsafePath) + { + var manifest = new PluginManifest + { + Id = Guid.NewGuid(), + Name = "Unsafe Assembly", + Assemblies = new string[] { unsafePath } + }; + + var (tempPath, pluginPath) = GetTestPaths("unsafe"); + + Directory.CreateDirectory(pluginPath); + + var files = new string[] + { + "../other.dll", + "some.dll" + }; + + foreach (var file in files) + { + File.Create(Path.Combine(pluginPath, file)); + } + + var options = GetTestSerializerOptions(); + var data = JsonSerializer.Serialize(manifest, options); + var metafilePath = Path.Combine(tempPath, "unsafe", "meta.json"); + + File.WriteAllText(metafilePath, data); + + var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, tempPath, new Version(1, 0)); + + var res = JsonSerializer.Deserialize<PluginManifest>(File.ReadAllText(metafilePath), options); + + Assert.NotNull(res); + Assert.Empty(pluginManager.Plugins); + Assert.Equal(PluginStatus.Malfunctioned, res!.Status); + } + + private JsonSerializerOptions GetTestSerializerOptions() + { + var options = new JsonSerializerOptions(JsonDefaults.Options) + { + WriteIndented = true + }; + + for (var i = 0; i < options.Converters.Count; i++) + { + // Remove the Guid converter for parity with plugin manager. + if (options.Converters[i] is JsonGuidConverter converter) + { + options.Converters.Remove(converter); + } + } + + return options; + } + + private (string TempPath, string PluginPath) GetTestPaths(string pluginFolderName) + { + var tempPath = Path.Combine(_testPathRoot, "plugins-" + Path.GetRandomFileName()); + var pluginPath = Path.Combine(tempPath, pluginFolderName); + + return (tempPath, pluginPath); } } } diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/manifest-stable.json b/tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/manifest-stable.json index fa8fbd8d2c..d69a52d6d0 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/manifest-stable.json +++ b/tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/manifest-stable.json @@ -13,7 +13,8 @@ "targetAbi": "10.6.0.0", "sourceUrl": "https://repo.jellyfin.org/releases/plugin/anime/anime_10.0.0.0.zip", "checksum": "93e969adeba1050423fc8817ed3c36f8", - "timestamp": "2020-08-17T01:41:13Z" + "timestamp": "2020-08-17T01:41:13Z", + "assemblies": [ "Jellyfin.Plugin.Anime.dll" ] }, { "version": "9.0.0.0", @@ -21,9 +22,10 @@ "targetAbi": "10.6.0.0", "sourceUrl": "https://repo.jellyfin.org/releases/plugin/anime/anime_9.0.0.0.zip", "checksum": "9b1cebff835813e15f414f44b40c41c8", - "timestamp": "2020-07-20T01:30:16Z" + "timestamp": "2020-07-20T01:30:16Z", + "assemblies": [ "Jellyfin.Plugin.Anime.dll" ] } - ] + ] }, { "guid": "70b7b43b-471b-4159-b4be-56750c795499", @@ -681,4 +683,4 @@ } ] } -] \ No newline at end of file +] diff --git a/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs index 7abd2e685f..70d03f8c4a 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs @@ -106,5 +106,19 @@ namespace Jellyfin.Server.Implementations.Tests.Updates var ex = await Record.ExceptionAsync(() => _installationManager.InstallPackage(packageInfo, CancellationToken.None)).ConfigureAwait(false); Assert.Null(ex); } + + [Fact] + public async Task InstallPackage_WithAssemblies_Success() + { + PackageInfo[] packages = await _installationManager.GetPackages( + "Jellyfin Stable", + "https://repo.jellyfin.org/releases/plugin/manifest-stable.json", + false); + + packages = _installationManager.FilterPackages(packages, "Anime").ToArray(); + Assert.Single(packages); + Assert.NotEmpty(packages[0].Versions[0].Assemblies); + Assert.Equal("Jellyfin.Plugin.Anime.dll", packages[0].Versions[0].Assemblies[0]); + } } } From 677b1f8e34799d26ffa9ef792aabcc79ad9073ca Mon Sep 17 00:00:00 2001 From: AmbulantRex <21176662+AmbulantRex@users.noreply.github.com> Date: Thu, 30 Mar 2023 12:56:57 -0600 Subject: [PATCH 201/858] Remove unnecessary using statements in PluginManager --- Emby.Server.Implementations/Library/PathExtensions.cs | 2 -- Emby.Server.Implementations/Plugins/PluginManager.cs | 3 --- 2 files changed, 5 deletions(-) diff --git a/Emby.Server.Implementations/Library/PathExtensions.cs b/Emby.Server.Implementations/Library/PathExtensions.cs index 7e0d0b78d5..a3d748a138 100644 --- a/Emby.Server.Implementations/Library/PathExtensions.cs +++ b/Emby.Server.Implementations/Library/PathExtensions.cs @@ -1,9 +1,7 @@ using System; using System.Diagnostics.CodeAnalysis; using System.IO; -using System.Linq; using MediaBrowser.Common.Providers; -using Nikse.SubtitleEdit.Core.Common; namespace Emby.Server.Implementations.Library { diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs index a5c55c8a09..d253a0ab99 100644 --- a/Emby.Server.Implementations/Plugins/PluginManager.cs +++ b/Emby.Server.Implementations/Plugins/PluginManager.cs @@ -22,11 +22,8 @@ using MediaBrowser.Model.Configuration; using MediaBrowser.Model.IO; using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Updates; -using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Nikse.SubtitleEdit.Core.Common; -using SQLitePCL.pretty; namespace Emby.Server.Implementations.Plugins { From 945f2761d29f8709ac928ab8a2d94506579d2220 Mon Sep 17 00:00:00 2001 From: heim3x <heim3x@proton.me> Date: Mon, 27 Mar 2023 20:23:27 +0000 Subject: [PATCH 202/858] Translated using Weblate (Danish) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/da/ --- .../Localization/Core/da.json | 104 +++++++++--------- 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json index 0d0d0c8136..1b6eecdcfe 100644 --- a/Emby.Server.Implementations/Localization/Core/da.json +++ b/Emby.Server.Implementations/Localization/Core/da.json @@ -1,9 +1,9 @@ { - "Albums": "Albummer", + "Albums": "Album", "AppDeviceValues": "App: {0}, Enhed: {1}", "Application": "Applikation", "Artists": "Kunstnere", - "AuthenticationSucceededWithUserName": "{0} succesfuldt autentificeret", + "AuthenticationSucceededWithUserName": "{0} er logget ind", "Books": "Bøger", "CameraImageUploadedFrom": "Et nyt kamerabillede er blevet uploadet fra {0}", "Channels": "Kanaler", @@ -11,17 +11,17 @@ "Collections": "Samlinger", "DeviceOfflineWithName": "{0} har afbrudt forbindelsen", "DeviceOnlineWithName": "{0} er forbundet", - "FailedLoginAttemptWithUserName": "Fejlet loginforsøg fra {0}", + "FailedLoginAttemptWithUserName": "Mislykket loginforsøg fra {0}", "Favorites": "Favoritter", "Folders": "Mapper", "Genres": "Genrer", - "HeaderAlbumArtists": "Albumkunstner", + "HeaderAlbumArtists": "Albums kunstnere", "HeaderContinueWatching": "Fortsæt afspilning", - "HeaderFavoriteAlbums": "Favoritalbummer", - "HeaderFavoriteArtists": "Favoritkunstnere", - "HeaderFavoriteEpisodes": "Favoritepisoder", - "HeaderFavoriteShows": "Favoritserier", - "HeaderFavoriteSongs": "Favoritsange", + "HeaderFavoriteAlbums": "Favorit albummer", + "HeaderFavoriteArtists": "Favorit kunstnere", + "HeaderFavoriteEpisodes": "Favorit afsnit", + "HeaderFavoriteShows": "Favorit serier", + "HeaderFavoriteSongs": "Favorit sange", "HeaderLiveTV": "Live-TV", "HeaderNextUp": "Næste", "HeaderRecordingGroups": "Optagelsesgrupper", @@ -39,90 +39,90 @@ "MixedContent": "Blandet indhold", "Movies": "Film", "Music": "Musik", - "MusicVideos": "Musik videoer", + "MusicVideos": "Musikvideoer", "NameInstallFailed": "{0} installationen mislykkedes", "NameSeasonNumber": "Sæson {0}", "NameSeasonUnknown": "Ukendt sæson", - "NewVersionIsAvailable": "En ny version af Jellyfin Server er tilgængelig til download.", - "NotificationOptionApplicationUpdateAvailable": "Opdatering til applikation tilgængelig", - "NotificationOptionApplicationUpdateInstalled": "Opdatering til applikation installeret", + "NewVersionIsAvailable": "En ny version af Jellyfin Server er tilgængelig.", + "NotificationOptionApplicationUpdateAvailable": "Opdatering til applikationen er tilgængelig", + "NotificationOptionApplicationUpdateInstalled": "Opdatering til applikationen blev installeret", "NotificationOptionAudioPlayback": "Lydafspilning påbegyndt", "NotificationOptionAudioPlaybackStopped": "Lydafspilning stoppet", "NotificationOptionCameraImageUploaded": "Kamerabillede uploadet", - "NotificationOptionInstallationFailed": "Installationen fejlede", + "NotificationOptionInstallationFailed": "Installationen mislykkedes", "NotificationOptionNewLibraryContent": "Nyt indhold tilføjet", - "NotificationOptionPluginError": "Pluginfejl", - "NotificationOptionPluginInstalled": "Plugin installeret", - "NotificationOptionPluginUninstalled": "Plugin afinstalleret", - "NotificationOptionPluginUpdateInstalled": "Opdatering til plugin installeret", - "NotificationOptionServerRestartRequired": "Genstart af server påkrævet", - "NotificationOptionTaskFailed": "Planlagt opgave fejlet", - "NotificationOptionUserLockedOut": "Bruger låst ude", + "NotificationOptionPluginError": "Plugin fejl", + "NotificationOptionPluginInstalled": "Plugin blev installeret", + "NotificationOptionPluginUninstalled": "Plugin blev afinstalleret", + "NotificationOptionPluginUpdateInstalled": "Opdatering til plugin blev installeret", + "NotificationOptionServerRestartRequired": "Genstart af serveren er påkrævet", + "NotificationOptionTaskFailed": "Planlagt opgave er fejlet", + "NotificationOptionUserLockedOut": "Bruger er låst ude", "NotificationOptionVideoPlayback": "Videoafspilning påbegyndt", - "NotificationOptionVideoPlaybackStopped": "Videoafspilning stoppet", - "Photos": "Fotoer", + "NotificationOptionVideoPlaybackStopped": "Videoafspilning blev stoppet", + "Photos": "Fotos", "Playlists": "Afspilningslister", "Plugin": "Plugin", "PluginInstalledWithName": "{0} blev installeret", "PluginUninstalledWithName": "{0} blev afinstalleret", "PluginUpdatedWithName": "{0} blev opdateret", "ProviderValue": "Udbyder: {0}", - "ScheduledTaskFailedWithName": "{0} fejlet", - "ScheduledTaskStartedWithName": "{0} påbegyndt", + "ScheduledTaskFailedWithName": "{0} mislykkedes", + "ScheduledTaskStartedWithName": "{0} påbegyndte", "ServerNameNeedsToBeRestarted": "{0} skal genstartes", "Shows": "Serier", "Songs": "Sange", - "StartupEmbyServerIsLoading": "Jellyfin Server er i gang med at starte op. Prøv venligst igen om lidt.", + "StartupEmbyServerIsLoading": "Jellyfin Server er i gang med at starte. Forsøg igen om et øjeblik.", "SubtitleDownloadFailureForItem": "Fejlet i download af undertekster for {0}", - "SubtitleDownloadFailureFromForItem": "Undertekster kunne ikke downloades fra {0} til {1}", - "Sync": "Synk", + "SubtitleDownloadFailureFromForItem": "Undertekster kunne ikke hentes fra {0} til {1}", + "Sync": "Synkroniser", "System": "System", - "TvShows": "Tv-serier", + "TvShows": "TV-serier", "User": "Bruger", "UserCreatedWithName": "Bruger {0} er blevet oprettet", - "UserDeletedWithName": "Brugeren {0} er blevet slettet", - "UserDownloadingItemWithValues": "{0} downloader {1}", + "UserDeletedWithName": "Brugeren {0} er nu slettet", + "UserDownloadingItemWithValues": "{0} henter {1}", "UserLockedOutWithName": "Brugeren {0} er blevet låst ude", "UserOfflineFromDevice": "{0} har afbrudt fra {1}", "UserOnlineFromDevice": "{0} er online fra {1}", - "UserPasswordChangedWithName": "Adgangskode er ændret for bruger {0}", - "UserPolicyUpdatedWithName": "Brugerpolitik er blevet opdateret for {0}", + "UserPasswordChangedWithName": "Adgangskode er ændret for brugeren {0}", + "UserPolicyUpdatedWithName": "Brugerpolitikken er blevet opdateret for {0}", "UserStartedPlayingItemWithValues": "{0} har påbegyndt afspilning af {1}", "UserStoppedPlayingItemWithValues": "{0} har afsluttet afspilning af {1} på {2}", "ValueHasBeenAddedToLibrary": "{0} er blevet tilføjet til dit mediebibliotek", "ValueSpecialEpisodeName": "Special - {0}", "VersionNumber": "Version {0}", - "TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata konfiguration.", - "TaskDownloadMissingSubtitles": "Download manglende undertekster", - "TaskUpdatePluginsDescription": "Downloader og installere opdateringer for plugins som er konfigureret til at opdatere automatisk.", + "TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata konfigurationen.", + "TaskDownloadMissingSubtitles": "Hent manglende undertekster", + "TaskUpdatePluginsDescription": "Henter og installerer opdateringer for plugins, som er indstillet til at blive opdateret automatisk.", "TaskUpdatePlugins": "Opdater Plugins", - "TaskCleanLogsDescription": "Sletter log filer som er mere end {0} dage gammle.", - "TaskCleanLogs": "Ryd Log Mappe", - "TaskRefreshLibraryDescription": "Scanner dit medie bibliotek for nye filer og opdaterer metadata.", + "TaskCleanLogsDescription": "Sletter log filer som er mere end {0} dage gamle.", + "TaskCleanLogs": "Ryd Log mappe", + "TaskRefreshLibraryDescription": "Scanner dit medie bibliotek for nye filer og opdateret metadata.", "TaskRefreshLibrary": "Scan Medie Bibliotek", - "TaskCleanCacheDescription": "Sletter cache filer som systemet ikke har brug for længere.", - "TaskCleanCache": "Ryd Cache Mappe", + "TaskCleanCacheDescription": "Sletter cache filer som systemet ikke længere bruger.", + "TaskCleanCache": "Ryd Cache mappe", "TasksChannelsCategory": "Internet Kanaler", "TasksApplicationCategory": "Applikation", "TasksLibraryCategory": "Bibliotek", "TasksMaintenanceCategory": "Vedligeholdelse", - "TaskRefreshChapterImages": "Udtræk Kapitel billeder", + "TaskRefreshChapterImages": "Udtræk kapitel billeder", "TaskRefreshChapterImagesDescription": "Lav miniaturebilleder for videoer der har kapitler.", - "TaskRefreshChannelsDescription": "Genopfrisker internet kanal information.", - "TaskRefreshChannels": "Genopfrisk Kanaler", - "TaskCleanTranscodeDescription": "Fjern transcode filer som er mere end en dag gammel.", - "TaskCleanTranscode": "Rengør Transcode Mappen", - "TaskRefreshPeople": "Genopfrisk Personer", - "TaskRefreshPeopleDescription": "Opdatere metadata for skuespillere og instruktører i dit bibliotek.", - "TaskCleanActivityLogDescription": "Sletter linjer i aktivitetsloggen ældre end den konfigureret alder.", + "TaskRefreshChannelsDescription": "Opdater internet kanal information.", + "TaskRefreshChannels": "Opdater Kanaler", + "TaskCleanTranscodeDescription": "Fjern transcode filer som er mere end 1 dag gammel.", + "TaskCleanTranscode": "Tøm Transcode mappen", + "TaskRefreshPeople": "Opdater Personer", + "TaskRefreshPeopleDescription": "Opdaterer metadata for skuespillere og instruktører i dit mediebibliotek.", + "TaskCleanActivityLogDescription": "Sletter linjer i aktivitetsloggen ældre end den konfigurerede alder.", "TaskCleanActivityLog": "Ryd Aktivitetslog", "Undefined": "Udefineret", "Forced": "Tvunget", "Default": "Standard", - "TaskOptimizeDatabaseDescription": "Kompakter database og forkorter fri plads. Ved at køre denne proces efter at scanne biblioteket eller efter at ændre noget som kunne have indflydelse på databasen, kan forbedre ydeevne.", + "TaskOptimizeDatabaseDescription": "Komprimerer databasen og frigør plads. Denne handling køres efter at have scannet mediebiblioteket, eller efter at have lavet ændringer til databasen, for at højne ydeevnen.", "TaskOptimizeDatabase": "Optimér database", - "TaskKeyframeExtractorDescription": "Udtrækker billeder fra videofiler for at lave mere præcise HLS playlister. Denne opgave kan godt tage lang tid.", - "TaskKeyframeExtractor": "Billedramme udtrækker", + "TaskKeyframeExtractorDescription": "Udtrækker billeder fra videofiler for at lave mere præcise HLS playlister. Denne opgave kan tage lang tid.", + "TaskKeyframeExtractor": "Nøglebillede udtræk", "External": "Ekstern", "HearingImpaired": "Hørehæmmet" } From d1646e9b8d7f29a4734593b6686691c3256be4b0 Mon Sep 17 00:00:00 2001 From: Sunip Mukherjee <sunipkmukherjee@gmail.com> Date: Wed, 29 Mar 2023 22:54:32 +0000 Subject: [PATCH 203/858] Translated using Weblate (Bengali) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/bn/ --- .../Localization/Core/bn.json | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/bn.json b/Emby.Server.Implementations/Localization/Core/bn.json index c3fbe24082..355fb3b21e 100644 --- a/Emby.Server.Implementations/Localization/Core/bn.json +++ b/Emby.Server.Implementations/Localization/Core/bn.json @@ -1,27 +1,27 @@ { "DeviceOnlineWithName": "{0}-এর সাথে সংযুক্ত হয়েছে", "DeviceOfflineWithName": "{0}-এর সাথে সংযোগ বিচ্ছিন্ন হয়েছে", - "Collections": "সংগ্রহ", + "Collections": "সংগ্রহশালা", "ChapterNameValue": "অধ্যায় {0}", - "Channels": "চ্যানেল", + "Channels": "চ্যানেলসমূহ", "CameraImageUploadedFrom": "{0} থেকে একটি নতুন ক্যামেরার চিত্র আপলোড করা হয়েছে", - "Books": "বই", + "Books": "পুস্তকসমূহ", "AuthenticationSucceededWithUserName": "{0} অনুমোদন সফল", - "Artists": "শিল্পীরা", + "Artists": "শিল্পীগণ", "Application": "অ্যাপ্লিকেশন", - "Albums": "অ্যালবামগুলো", + "Albums": "অ্যালবামসমূহ", "HeaderFavoriteEpisodes": "প্রিব পর্বগুলো", "HeaderFavoriteArtists": "প্রিয় শিল্পীরা", "HeaderFavoriteAlbums": "প্রিয় এলবামগুলো", "HeaderContinueWatching": "দেখতে থাকুন", - "HeaderAlbumArtists": "এলবাম শিল্পীবৃন্দ", - "Genres": "শৈলী", - "Folders": "ফোল্ডারগুলো", + "HeaderAlbumArtists": "অ্যালবাম শিল্পীবৃন্দ", + "Genres": "শৈলীধারাসমূহ", + "Folders": "ফোল্ডারসমূহ", "Favorites": "পছন্দসমূহ", "FailedLoginAttemptWithUserName": "{0} লগিন করতে ব্যর্থ হয়েছে", "AppDeviceValues": "অ্যাপ: {0}, ডিভাইস: {0}", "VersionNumber": "সংস্করণ {0}", - "ValueSpecialEpisodeName": "বিশেষ - {0}", + "ValueSpecialEpisodeName": "বিশেষ পর্ব - {0}", "ValueHasBeenAddedToLibrary": "আপনার লাইব্রেরিতে {0} যোগ করা হয়েছে", "UserStoppedPlayingItemWithValues": "{2}তে {1} বাজানো শেষ করেছেন {0}", "UserStartedPlayingItemWithValues": "{2}তে {1} বাজাচ্ছেন {0}", @@ -36,10 +36,10 @@ "User": "ব্যবহারকারী", "TvShows": "টিভি শোগুলো", "System": "সিস্টেম", - "Sync": "সিংক", + "Sync": "সমলয় স্থাপন", "SubtitleDownloadFailureFromForItem": "{2} থেকে {1} এর জন্য সাবটাইটেল ডাউনলোড ব্যর্থ", "StartupEmbyServerIsLoading": "জেলিফিন সার্ভার লোড হচ্ছে। দয়া করে একটু পরে আবার চেষ্টা করুন।", - "Songs": "গানগুলো", + "Songs": "সঙ্গীতসমূহ", "Shows": "টিভি পর্ব", "ServerNameNeedsToBeRestarted": "{0} রিস্টার্ট করা প্রয়োজন", "ScheduledTaskStartedWithName": "{0} শুরু হয়েছে", @@ -49,8 +49,8 @@ "PluginUninstalledWithName": "{0} বাদ দেয়া হয়েছে", "PluginInstalledWithName": "{0} ইন্সটল করা হয়েছে", "Plugin": "প্লাগিন", - "Playlists": "প্লেলিস্ট", - "Photos": "ছবিগুলো", + "Playlists": "প্লে লিস্ট সমূহ", + "Photos": "চিত্রসমূহ", "NotificationOptionVideoPlaybackStopped": "ভিডিও চলা বন্ধ", "NotificationOptionVideoPlayback": "ভিডিও চলা শুরু হয়েছে", "NotificationOptionUserLockedOut": "ব্যবহারকারী ঢুকতে পারছে না", @@ -71,9 +71,9 @@ "NameSeasonUnknown": "সিজন অজানা", "NameSeasonNumber": "সিজন {0}", "NameInstallFailed": "{0} ইন্সটল ব্যর্থ", - "MusicVideos": "গানের ভিডিও", + "MusicVideos": "সঙ্গীত ভিডিয়ো সমূহ", "Music": "গান", - "Movies": "চলচ্চিত্র", + "Movies": "চলচ্চিত্রসমূহ", "MixedContent": "মিশ্র কন্টেন্ট", "MessageServerConfigurationUpdated": "সার্ভারের কনফিগারেশন আপডেট করা হয়েছে", "HeaderRecordingGroups": "রেকর্ডিং দল", @@ -117,5 +117,11 @@ "Forced": "জোরকরে", "TaskCleanActivityLogDescription": "নির্ধারিত সময়ের আগের কাজের হিসাব মুছে দিন খালি করুন.", "TaskCleanActivityLog": "কাজের ফাইল খালি করুন", - "Default": "প্রাথমিক" + "Default": "প্রাথমিক", + "HearingImpaired": "দুর্বল শ্রবণক্ষমতাধরদের জন্য", + "TaskOptimizeDatabaseDescription": "তথ্যভাণ্ডার সুবিন্যস্ত করে ও অব্যবহৃত জায়গা ছেড়ে দেয়। লাইব্রেরী স্ক্যান অথবা যেকোনো তথ্যভাণ্ডার পরিবর্তনের পর এই প্রক্রিয়া চালালে তথ্যভাণ্ডারের তথ্য প্রদান দ্রুততর হতে পারে।", + "External": "বাহ্যিক", + "TaskOptimizeDatabase": "তথ্যভাণ্ডার সুবিন্যাস", + "TaskKeyframeExtractor": "কি-ফ্রেম নিষ্কাশক", + "TaskKeyframeExtractorDescription": "ভিডিয়ো থেকে কি-ফ্রেম নিষ্কাশনের মাধ্যমে অধিকতর সঠিক HLS প্লে লিস্ট তৈরী করে। এই প্রক্রিয়া দীর্ঘ সময় ধরে চলতে পারে।" } From 56f5b37fbf8b081a477f936dea5194850223494a Mon Sep 17 00:00:00 2001 From: pankajabhyam <pkkumar391@gmail.com> Date: Mon, 27 Mar 2023 08:36:58 +0000 Subject: [PATCH 204/858] Translated using Weblate (Hindi) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hi/ --- .../Localization/Core/hi.json | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/hi.json b/Emby.Server.Implementations/Localization/Core/hi.json index a0e2f04a16..b62e64f4da 100644 --- a/Emby.Server.Implementations/Localization/Core/hi.json +++ b/Emby.Server.Implementations/Localization/Core/hi.json @@ -73,5 +73,23 @@ "Songs": "गाने", "UserStartedPlayingItemWithValues": "{0} {2} पर {1} खेल रहे हैं", "UserStoppedPlayingItemWithValues": "{0} ने {2} पर {1} खेलना खत्म किया", - "StartupEmbyServerIsLoading": "जेलीफ़िन सर्वर लोड हो रहा है। कृपया शीघ्र ही पुन: प्रयास करें।" + "StartupEmbyServerIsLoading": "जेलीफ़िन सर्वर लोड हो रहा है। कृपया शीघ्र ही पुन: प्रयास करें।", + "ServerNameNeedsToBeRestarted": "{0} रीस्टार्ट करने की आवश्यकता है", + "UserCreatedWithName": "उपयोगकर्ता {0} बनाया गया", + "UserDownloadingItemWithValues": "{0} डाउनलोड हो रहा है", + "UserOfflineFromDevice": "{0} {1} से डिस्कनेक्ट हो गया है", + "Undefined": "अनिर्धारित", + "UserOnlineFromDevice": "{0} {1} से ऑनलाइन है", + "Shows": "शो", + "UserPasswordChangedWithName": "उपयोगकर्ता {0} के लिए पासवर्ड बदल दिया गया है", + "UserDeletedWithName": "उपयोगकर्ता {0} हटा दिया गया", + "UserPolicyUpdatedWithName": "{0} के लिए उपयोगकर्ता नीति अपडेट कर दी गई है", + "User": "उपयोगकर्ता", + "SubtitleDownloadFailureFromForItem": "{1} के लिए {0} से उपशीर्षक डाउनलोड करने में विफल", + "ProviderValue": "प्रदाता: {0}", + "ScheduledTaskFailedWithName": "{0}असफल", + "UserLockedOutWithName": "उपयोगकर्ता {0} को लॉक आउट कर दिया गया है", + "System": "प्रणाली", + "TvShows": "टीवी शो", + "HearingImpaired": "मूक बधिर" } From eb59456e78b11379ba657503f08b1f7c9208f2ae Mon Sep 17 00:00:00 2001 From: Sahil Ahluwalia <satrader2000@gmail.com> Date: Wed, 29 Mar 2023 05:47:31 +0000 Subject: [PATCH 205/858] Translated using Weblate (Hindi) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hi/ --- Emby.Server.Implementations/Localization/Core/hi.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/hi.json b/Emby.Server.Implementations/Localization/Core/hi.json index b62e64f4da..cc0a926d03 100644 --- a/Emby.Server.Implementations/Localization/Core/hi.json +++ b/Emby.Server.Implementations/Localization/Core/hi.json @@ -91,5 +91,7 @@ "UserLockedOutWithName": "उपयोगकर्ता {0} को लॉक आउट कर दिया गया है", "System": "प्रणाली", "TvShows": "टीवी शो", - "HearingImpaired": "मूक बधिर" + "HearingImpaired": "मूक बधिर", + "ValueSpecialEpisodeName": "विशेष", + "TasksMaintenanceCategory": "रखरखाव" } From a944352aa8b961ab723fed2d66947c19129a11cc Mon Sep 17 00:00:00 2001 From: AmbulantRex <21176662+AmbulantRex@users.noreply.github.com> Date: Sat, 1 Apr 2023 04:59:07 -0600 Subject: [PATCH 206/858] Correct style inconsistencies --- Emby.Server.Implementations/Library/PathExtensions.cs | 9 ++++++--- Emby.Server.Implementations/Plugins/PluginManager.cs | 2 +- MediaBrowser.Common/Plugins/PluginManifest.cs | 4 ++-- MediaBrowser.Model/Updates/VersionInfo.cs | 2 +- .../Test Data/Updates/manifest-stable.json | 2 +- 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Emby.Server.Implementations/Library/PathExtensions.cs b/Emby.Server.Implementations/Library/PathExtensions.cs index a3d748a138..62a9e6419e 100644 --- a/Emby.Server.Implementations/Library/PathExtensions.cs +++ b/Emby.Server.Implementations/Library/PathExtensions.cs @@ -87,8 +87,8 @@ namespace Emby.Server.Implementations.Library return false; } - subPath = subPath.NormalizePath(out var newDirectorySeparatorChar)!; - path = path.NormalizePath(newDirectorySeparatorChar)!; + subPath = subPath.NormalizePath(out var newDirectorySeparatorChar); + path = path.NormalizePath(newDirectorySeparatorChar); // We have to ensure that the sub path ends with a directory separator otherwise we'll get weird results // when the sub path matches a similar but in-complete subpath @@ -128,6 +128,7 @@ namespace Emby.Server.Implementations.Library /// </summary> /// <param name="path">The path to normalize.</param> /// <returns>The normalized path string or <see langword="null"/> if the input path is null or empty.</returns> + [return: NotNullIfNotNull(nameof(path))] public static string? NormalizePath(this string? path) { return path.NormalizePath(Path.DirectorySeparatorChar); @@ -139,6 +140,7 @@ namespace Emby.Server.Implementations.Library /// <param name="path">The path to normalize.</param> /// <param name="separator">The separator character the path now uses or <see langword="null"/>.</param> /// <returns>The normalized path string or <see langword="null"/> if the input path is null or empty.</returns> + [return: NotNullIfNotNull(nameof(path))] public static string? NormalizePath(this string? path, out char separator) { if (string.IsNullOrEmpty(path)) @@ -169,6 +171,7 @@ namespace Emby.Server.Implementations.Library /// <param name="newSeparator">The replacement directory separator character. Must be a valid directory separator.</param> /// <returns>The normalized path.</returns> /// <exception cref="ArgumentException">Thrown if the new separator character is not a directory separator.</exception> + [return: NotNullIfNotNull(nameof(path))] public static string? NormalizePath(this string? path, char newSeparator) { const char Bs = '\\'; @@ -184,7 +187,7 @@ namespace Emby.Server.Implementations.Library return path; } - return newSeparator == Bs ? path?.Replace(Fs, newSeparator) : path?.Replace(Bs, newSeparator); + return newSeparator == Bs ? path.Replace(Fs, newSeparator) : path.Replace(Bs, newSeparator); } } } diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs index d253a0ab99..0a7c144ed7 100644 --- a/Emby.Server.Implementations/Plugins/PluginManager.cs +++ b/Emby.Server.Implementations/Plugins/PluginManager.cs @@ -765,7 +765,7 @@ namespace Emby.Server.Implementations.Plugins /// <exception cref="ArgumentNullException">If the <see cref="LocalPlugin"/> is null.</exception> private bool TryGetPluginDlls(LocalPlugin plugin, out IReadOnlyList<string> whitelistedDlls) { - _ = plugin ?? throw new ArgumentNullException(nameof(plugin)); + ArgumentNullException.ThrowIfNull(nameof(plugin)); IReadOnlyList<string> pluginDlls = Directory.GetFiles(plugin.Path, "*.dll", SearchOption.AllDirectories); diff --git a/MediaBrowser.Common/Plugins/PluginManifest.cs b/MediaBrowser.Common/Plugins/PluginManifest.cs index 2bad3454d1..e0847ccea4 100644 --- a/MediaBrowser.Common/Plugins/PluginManifest.cs +++ b/MediaBrowser.Common/Plugins/PluginManifest.cs @@ -24,7 +24,7 @@ namespace MediaBrowser.Common.Plugins Overview = string.Empty; TargetAbi = string.Empty; Version = string.Empty; - Assemblies = new List<string>(); + Assemblies = Array.Empty<string>(); } /// <summary> @@ -112,6 +112,6 @@ namespace MediaBrowser.Common.Plugins /// Paths are considered relative to the plugin folder. /// </summary> [JsonPropertyName("assemblies")] - public IList<string> Assemblies { get; set; } + public IReadOnlyList<string> Assemblies { get; set; } } } diff --git a/MediaBrowser.Model/Updates/VersionInfo.cs b/MediaBrowser.Model/Updates/VersionInfo.cs index 1e24bde84e..8f76806450 100644 --- a/MediaBrowser.Model/Updates/VersionInfo.cs +++ b/MediaBrowser.Model/Updates/VersionInfo.cs @@ -80,6 +80,6 @@ namespace MediaBrowser.Model.Updates /// Gets or sets the assemblies whitelist for this version. /// </summary> [JsonPropertyName("assemblies")] - public IList<string> Assemblies { get; set; } = Array.Empty<string>(); + public IReadOnlyList<string> Assemblies { get; set; } = Array.Empty<string>(); } } diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/manifest-stable.json b/tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/manifest-stable.json index d69a52d6d0..3aec299587 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/manifest-stable.json +++ b/tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/manifest-stable.json @@ -25,7 +25,7 @@ "timestamp": "2020-07-20T01:30:16Z", "assemblies": [ "Jellyfin.Plugin.Anime.dll" ] } - ] + ] }, { "guid": "70b7b43b-471b-4159-b4be-56750c795499", From 3a731051adf6d636517e5a9babbbe9f9da7d520b Mon Sep 17 00:00:00 2001 From: AmbulantRex <21176662+AmbulantRex@users.noreply.github.com> Date: Sat, 1 Apr 2023 05:03:55 -0600 Subject: [PATCH 207/858] Correct styling inconsistencies --- Emby.Server.Implementations/Library/PathExtensions.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Library/PathExtensions.cs b/Emby.Server.Implementations/Library/PathExtensions.cs index 62a9e6419e..c4b6b37561 100644 --- a/Emby.Server.Implementations/Library/PathExtensions.cs +++ b/Emby.Server.Implementations/Library/PathExtensions.cs @@ -105,7 +105,7 @@ namespace Emby.Server.Implementations.Library return false; } - var newSubPathTrimmed = newSubPath.AsSpan().TrimEnd((char)newDirectorySeparatorChar!); + var newSubPathTrimmed = newSubPath.AsSpan().TrimEnd(newDirectorySeparatorChar); // Ensure that the path with the old subpath removed starts with a leading dir separator int idx = oldSubPathEndsWithSeparator ? subPath.Length - 1 : subPath.Length; newPath = string.Concat(newSubPathTrimmed, path.AsSpan(idx)); @@ -120,7 +120,7 @@ namespace Emby.Server.Implementations.Library /// <returns>The fully expanded, normalized path.</returns> public static string Canonicalize(this string path) { - return Path.GetFullPath(path).NormalizePath()!; + return Path.GetFullPath(path).NormalizePath(); } /// <summary> @@ -161,7 +161,7 @@ namespace Emby.Server.Implementations.Library separator = newSeparator; - return path?.NormalizePath(newSeparator); + return path.NormalizePath(newSeparator); } /// <summary> From e74630a6137b77e76b7fc311859e6c81bcd776b5 Mon Sep 17 00:00:00 2001 From: Stepan Goremykin <goremukin@gmail.com> Date: Sat, 1 Apr 2023 23:00:51 +0200 Subject: [PATCH 208/858] Use MinBy and MaxBy --- Emby.Server.Implementations/Plugins/PluginManager.cs | 2 +- Emby.Server.Implementations/Session/WebSocketController.cs | 4 +--- Jellyfin.Api/Controllers/DynamicHlsController.cs | 4 +--- Jellyfin.Api/Controllers/PluginsController.cs | 2 +- Jellyfin.Networking/Manager/NetworkManager.cs | 3 +-- 5 files changed, 5 insertions(+), 10 deletions(-) diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs index 7c23254a12..f14f87ccda 100644 --- a/Emby.Server.Implementations/Plugins/PluginManager.cs +++ b/Emby.Server.Implementations/Plugins/PluginManager.cs @@ -304,7 +304,7 @@ namespace Emby.Server.Implementations.Plugins // If no version is given, return the current instance. var plugins = _plugins.Where(p => p.Id.Equals(id)).ToList(); - plugin = plugins.FirstOrDefault(p => p.Instance is not null) ?? plugins.OrderByDescending(p => p.Version).FirstOrDefault(); + plugin = plugins.FirstOrDefault(p => p.Instance is not null) ?? plugins.MaxBy(p => p.Version); } else { diff --git a/Emby.Server.Implementations/Session/WebSocketController.cs b/Emby.Server.Implementations/Session/WebSocketController.cs index 051fa5b3cf..cdc736950e 100644 --- a/Emby.Server.Implementations/Session/WebSocketController.cs +++ b/Emby.Server.Implementations/Session/WebSocketController.cs @@ -69,9 +69,7 @@ namespace Emby.Server.Implementations.Session T data, CancellationToken cancellationToken) { - var socket = GetActiveSockets() - .OrderByDescending(i => i.LastActivityDate) - .FirstOrDefault(); + var socket = GetActiveSockets().MaxBy(i => i.LastActivityDate); if (socket is null) { diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 42a7ac2b16..09cf7303e7 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -20,7 +20,6 @@ using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Net; using MediaBrowser.MediaEncoding.Encoder; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dlna; @@ -2030,8 +2029,7 @@ public class DynamicHlsController : BaseJellyfinApiController { return fileSystem.GetFiles(folder, new[] { segmentExtension }, true, false) .Where(i => Path.GetFileNameWithoutExtension(i.Name).StartsWith(filePrefix, StringComparison.OrdinalIgnoreCase)) - .OrderByDescending(fileSystem.GetLastWriteTimeUtc) - .FirstOrDefault(); + .MaxBy(fileSystem.GetLastWriteTimeUtc); } catch (IOException) { diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs index 4726cf0663..72ad14a281 100644 --- a/Jellyfin.Api/Controllers/PluginsController.cs +++ b/Jellyfin.Api/Controllers/PluginsController.cs @@ -146,7 +146,7 @@ public class PluginsController : BaseJellyfinApiController var plugins = _pluginManager.Plugins.Where(p => p.Id.Equals(pluginId)).ToList(); // Select the un-instanced one first. - var plugin = plugins.FirstOrDefault(p => p.Instance is null) ?? plugins.OrderBy(p => p.Manifest.Status).FirstOrDefault(); + var plugin = plugins.FirstOrDefault(p => p.Instance is null) ?? plugins.MinBy(p => p.Manifest.Status); if (plugin is not null) { diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index f406e27a6d..ac9258ad13 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -1256,8 +1256,7 @@ namespace Jellyfin.Networking.Manager // Look for the best internal address. bindAddress = addresses .Where(p => IsInLocalNetwork(p) && (p.Contains(source) || p.Equals(IPAddress.None))) - .OrderBy(p => p.Tag) - .FirstOrDefault()?.Address; + .MinBy(p => p.Tag)?.Address; } if (bindAddress is not null) From bb5bf0277ae33fd4668e00bd2f1a85e89aef466a Mon Sep 17 00:00:00 2001 From: elmuffo <elmuffo@pm.me> Date: Sun, 2 Apr 2023 09:01:05 +1200 Subject: [PATCH 209/858] Implement check to hide all libraries when user has no access (#9536) --- Emby.Server.Implementations/Library/LibraryManager.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index e5c520ca2b..36a300a572 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -1503,6 +1503,12 @@ namespace Emby.Server.Implementations.Library }); query.TopParentIds = userViews.SelectMany(i => GetTopParentIdsForQuery(i, user)).ToArray(); + + // Prevent searching in all libraries due to empty filter + if (query.TopParentIds.Length == 0) + { + query.TopParentIds = new[] { Guid.NewGuid() }; + } } } From 2d24da406d45ec074c52de08cd6737e087102db0 Mon Sep 17 00:00:00 2001 From: Stepan Goremykin <goremukin@gmail.com> Date: Sat, 1 Apr 2023 23:01:20 +0200 Subject: [PATCH 210/858] Add "Emby" to exceptions --- Jellyfin.sln.DotSettings | 1 + 1 file changed, 1 insertion(+) diff --git a/Jellyfin.sln.DotSettings b/Jellyfin.sln.DotSettings index b567416480..2ef037485c 100644 --- a/Jellyfin.sln.DotSettings +++ b/Jellyfin.sln.DotSettings @@ -1,3 +1,4 @@ <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> + <s:Boolean x:Key="/Default/UserDictionary/Words/=Emby/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Jellyfin/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Playstate/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary> \ No newline at end of file From 3a25b03ea99ea97326ea72484a8c6ece2ecf956b Mon Sep 17 00:00:00 2001 From: nyanmisaka <nst799610810@gmail.com> Date: Sun, 2 Apr 2023 17:30:22 +0800 Subject: [PATCH 211/858] Fix vaapi-vulkan subtitle tearing issue Signed-off-by: nyanmisaka <nst799610810@gmail.com> --- .../MediaEncoding/EncodingHelper.cs | 63 ++++++++++--------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index f7ee3c1732..0d5534324f 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -842,39 +842,38 @@ namespace MediaBrowser.Controller.MediaEncoding } var filterDevArgs = GetFilterHwDeviceArgs(VaapiAlias); + var doOclTonemap = isHwTonemapAvailable && IsOpenclFullSupported(); - if (isHwTonemapAvailable && IsOpenclFullSupported()) + if (_mediaEncoder.IsVaapiDeviceInteliHD || _mediaEncoder.IsVaapiDeviceInteli965) { - if (_mediaEncoder.IsVaapiDeviceInteliHD || _mediaEncoder.IsVaapiDeviceInteli965) + if (doOclTonemap && !isVaapiDecoder) { - if (!isVaapiDecoder) - { - args.Append(GetOpenclDeviceArgs(0, null, VaapiAlias, OpenclAlias)); - filterDevArgs = GetFilterHwDeviceArgs(OpenclAlias); - } - } - else if (_mediaEncoder.IsVaapiDeviceAmd) - { - if (!IsVulkanFullSupported() - || !_mediaEncoder.IsVaapiDeviceSupportVulkanFmtModifier - || Environment.OSVersion.Version < _minKernelVersionAmdVkFmtModifier) - { - args.Append(GetOpenclDeviceArgs(0, "Advanced Micro Devices", null, OpenclAlias)); - filterDevArgs = GetFilterHwDeviceArgs(OpenclAlias); - } - else - { - // libplacebo wants an explicitly set vulkan filter device. - args.Append(GetVulkanDeviceArgs(0, null, VaapiAlias, VulkanAlias)); - filterDevArgs = GetFilterHwDeviceArgs(VulkanAlias); - } - } - else - { - args.Append(GetOpenclDeviceArgs(0, null, null, OpenclAlias)); + args.Append(GetOpenclDeviceArgs(0, null, VaapiAlias, OpenclAlias)); filterDevArgs = GetFilterHwDeviceArgs(OpenclAlias); } } + else if (_mediaEncoder.IsVaapiDeviceAmd) + { + if (IsVulkanFullSupported() + && _mediaEncoder.IsVaapiDeviceSupportVulkanFmtModifier + && Environment.OSVersion.Version >= _minKernelVersionAmdVkFmtModifier) + { + // libplacebo wants an explicitly set vulkan filter device. + args.Append(GetVulkanDeviceArgs(0, null, VaapiAlias, VulkanAlias)); + filterDevArgs = GetFilterHwDeviceArgs(VulkanAlias); + } + else if (doOclTonemap) + { + // ROCm/ROCr OpenCL runtime + args.Append(GetOpenclDeviceArgs(0, "Advanced Micro Devices", null, OpenclAlias)); + filterDevArgs = GetFilterHwDeviceArgs(OpenclAlias); + } + } + else if (doOclTonemap) + { + args.Append(GetOpenclDeviceArgs(0, null, null, OpenclAlias)); + filterDevArgs = GetFilterHwDeviceArgs(OpenclAlias); + } args.Append(filterDevArgs); } @@ -4269,7 +4268,8 @@ namespace MediaBrowser.Controller.MediaEncoding // sw => hw if (doVkTonemap) { - mainFilters.Add("hwupload_vaapi"); + mainFilters.Add("hwupload=derive_device=vaapi"); + mainFilters.Add("format=vaapi"); mainFilters.Add("hwmap=derive_device=vulkan"); mainFilters.Add("format=vulkan"); } @@ -4380,12 +4380,15 @@ namespace MediaBrowser.Controller.MediaEncoding // prefer vaapi hwupload to vulkan hwupload, // Mesa RADV does not support a dedicated transfer queue. - subFilters.Add("hwupload_vaapi"); + subFilters.Add("hwupload=derive_device=vaapi"); + subFilters.Add("format=vaapi"); subFilters.Add("hwmap=derive_device=vulkan"); subFilters.Add("format=vulkan"); overlayFilters.Add("overlay_vulkan=eof_action=endall:shortest=1:repeatlast=0"); - overlayFilters.Add("scale_vulkan=format=nv12"); + + // TODO: figure out why libplacebo can sync without vaSyncSurface VPP support in radeonsi. + overlayFilters.Add("libplacebo=format=nv12:apply_filmgrain=0:apply_dolbyvision=0:upscaler=none:downscaler=none:dithering=none"); // OUTPUT vaapi(nv12/bgra) surface(vram) // reverse-mapping via vaapi-vulkan interop. From 078d7da3dab3e72feca1d18bc21669d04c45b70a Mon Sep 17 00:00:00 2001 From: SuperDumbTM <eltonlochunkit@gmail.com> Date: Tue, 4 Apr 2023 03:32:35 +0000 Subject: [PATCH 212/858] Translated using Weblate (Chinese (Traditional, Hong Kong)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant_HK/ --- .../Localization/Core/zh-HK.json | 92 +++++++++---------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json index cdc25ec7c7..b08119d0a6 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -5,12 +5,12 @@ "Artists": "藝人", "AuthenticationSucceededWithUserName": "{0} 授權成功", "Books": "圖書", - "CameraImageUploadedFrom": "{0} 成功上傳一張新相片", + "CameraImageUploadedFrom": "{0} 成功上傳一張新照片", "Channels": "頻道", - "ChapterNameValue": "章節 {0}", + "ChapterNameValue": "第 {0} 章", "Collections": "合輯", - "DeviceOfflineWithName": "{0} 已經斷開連接", - "DeviceOnlineWithName": "{0} 已經連接", + "DeviceOfflineWithName": "{0} 已斷開連接", + "DeviceOnlineWithName": "{0} 已連接", "FailedLoginAttemptWithUserName": "{0} 登入失敗", "Favorites": "我的最愛", "Folders": "資料夾", @@ -23,43 +23,43 @@ "HeaderFavoriteShows": "最愛的節目", "HeaderFavoriteSongs": "最愛的歌曲", "HeaderLiveTV": "電視直播", - "HeaderNextUp": "接下來", + "HeaderNextUp": "接著播放", "HeaderRecordingGroups": "錄製組", "HomeVideos": "家庭影片", "Inherit": "繼承", - "ItemAddedWithName": "{0} 已添加至媒體庫", + "ItemAddedWithName": "{0} 已被添加至媒體庫", "ItemRemovedWithName": "{0} 已從媒體庫移除", "LabelIpAddressValue": "IP 地址: {0}", "LabelRunningTimeValue": "運行時間: {0}", "Latest": "最新", - "MessageApplicationUpdated": "Jellyfin 伺服器已更新", - "MessageApplicationUpdatedTo": "Jellyfin 伺服器已更新至 {0}", - "MessageNamedServerConfigurationUpdatedWithValue": "伺服器設定 {0} 已更新", - "MessageServerConfigurationUpdated": "伺服器設定已經更新", + "MessageApplicationUpdated": "Jellyfin 已被更新", + "MessageApplicationUpdatedTo": "Jellyfin 已被更新至 {0}", + "MessageNamedServerConfigurationUpdatedWithValue": "伺服器設定 {0} 已被更新", + "MessageServerConfigurationUpdated": "伺服器設定已經被更新", "MixedContent": "混合內容", "Movies": "電影", "Music": "音樂", - "MusicVideos": "音樂影片", + "MusicVideos": "MV", "NameInstallFailed": "{0} 安裝失敗", "NameSeasonNumber": "第 {0} 季", - "NameSeasonUnknown": "未知季數", - "NewVersionIsAvailable": "新版本的 Jellyfin 伺服器可供下載。", + "NameSeasonUnknown": "未知的季度", + "NewVersionIsAvailable": "有較新版本的 Jellyfin 可供下載。", "NotificationOptionApplicationUpdateAvailable": "有可用的更新", - "NotificationOptionApplicationUpdateInstalled": "應用程式已更新", + "NotificationOptionApplicationUpdateInstalled": "應用程式已被更新", "NotificationOptionAudioPlayback": "開始播放音訊", - "NotificationOptionAudioPlaybackStopped": "已停止播放音訊", - "NotificationOptionCameraImageUploaded": "相片已上傳", + "NotificationOptionAudioPlaybackStopped": "停止播放音訊", + "NotificationOptionCameraImageUploaded": "相片已被上傳", "NotificationOptionInstallationFailed": "安裝失敗", "NotificationOptionNewLibraryContent": "已添加新内容", - "NotificationOptionPluginError": "擴充元件錯誤", - "NotificationOptionPluginInstalled": "擴充元件已安裝", - "NotificationOptionPluginUninstalled": "擴充元件已移除", - "NotificationOptionPluginUpdateInstalled": "擴充元件更新已安裝", - "NotificationOptionServerRestartRequired": "伺服器需要重啓", - "NotificationOptionTaskFailed": "計劃任務失敗", - "NotificationOptionUserLockedOut": "用家已鎖定", - "NotificationOptionVideoPlayback": "開始播放視頻", - "NotificationOptionVideoPlaybackStopped": "已停止播放視頻", + "NotificationOptionPluginError": "插件出現錯誤", + "NotificationOptionPluginInstalled": "插件已被安裝", + "NotificationOptionPluginUninstalled": "插件已被移除", + "NotificationOptionPluginUpdateInstalled": "插件已被更新", + "NotificationOptionServerRestartRequired": "伺服器需要重啟", + "NotificationOptionTaskFailed": "排程任務執行失敗", + "NotificationOptionUserLockedOut": "用戶已被鎖定", + "NotificationOptionVideoPlayback": "開始播放影片", + "NotificationOptionVideoPlaybackStopped": "已停止播放影片", "Photos": "相片", "Playlists": "播放清單", "Plugin": "插件", @@ -69,51 +69,51 @@ "ProviderValue": "提供者: {0}", "ScheduledTaskFailedWithName": "{0} 任務失敗", "ScheduledTaskStartedWithName": "{0} 任務開始", - "ServerNameNeedsToBeRestarted": "{0} 需要重啓", + "ServerNameNeedsToBeRestarted": "{0} 需要重啟", "Shows": "節目", "Songs": "歌曲", - "StartupEmbyServerIsLoading": "Jellyfin 伺服器載入中,請稍後再試。", + "StartupEmbyServerIsLoading": "Jellyfin 載入中,請稍後再試。", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "無法從 {0} 下載 {1} 的字幕", "Sync": "同步", "System": "系統", "TvShows": "電視節目", - "User": "使用者", - "UserCreatedWithName": "使用者 {0} 已創建", - "UserDeletedWithName": "使用者 {0} 已移除", + "User": "用戶", + "UserCreatedWithName": "用戶 {0} 已被建立", + "UserDeletedWithName": "用戶 {0} 已被移除", "UserDownloadingItemWithValues": "{0} 正在下載 {1}", "UserLockedOutWithName": "使用者 {0} 已被鎖定", - "UserOfflineFromDevice": "{0} 已從 {1} 斷開", - "UserOnlineFromDevice": "{0} 已連綫,來自 {1}", - "UserPasswordChangedWithName": "使用者 {0} 的密碼已變更", + "UserOfflineFromDevice": "{0} 從 {1} 斷開連接", + "UserOnlineFromDevice": "{0} 從 {1} 連線", + "UserPasswordChangedWithName": "{0} 的密碼已被變改", "UserPolicyUpdatedWithName": "使用者協議已更新為 {0}", "UserStartedPlayingItemWithValues": "{0} 正在 {2} 上播放 {1}", - "UserStoppedPlayingItemWithValues": "{0} 已在 {2} 上停止播放 {1}", - "ValueHasBeenAddedToLibrary": "{0} 已添加到你的媒體庫", + "UserStoppedPlayingItemWithValues": "{0} 已停止在 {2} 上播放 {1}", + "ValueHasBeenAddedToLibrary": "已添加 {0} 到你的媒體庫", "ValueSpecialEpisodeName": "特典 - {0}", - "VersionNumber": "版本{0}", - "TaskDownloadMissingSubtitles": "下載遺失的字幕", + "VersionNumber": "版本 {0}", + "TaskDownloadMissingSubtitles": "下載缺少的字幕", "TaskUpdatePlugins": "更新插件", "TasksApplicationCategory": "應用程式", - "TaskRefreshLibraryDescription": "掃描媒體庫以查找新文件並刷新metadata。", + "TaskRefreshLibraryDescription": "掃描媒體庫以加入新增檔案及重新載入 metadata。", "TasksMaintenanceCategory": "維護", - "TaskDownloadMissingSubtitlesDescription": "根據metadata配置在互聯網上搜索缺少的字幕。", - "TaskRefreshChannelsDescription": "刷新互聯網頻道信息。", - "TaskRefreshChannels": "刷新頻道", + "TaskDownloadMissingSubtitlesDescription": "根據元數據中的設定,在互聯網上搜索缺少的字幕。", + "TaskRefreshChannelsDescription": "重新載入網絡頻道的資訊。", + "TaskRefreshChannels": "重新載入頻道", "TaskCleanTranscodeDescription": "刪除超過一天的轉碼文件。", "TaskCleanTranscode": "清理轉碼目錄", - "TaskUpdatePluginsDescription": "下載並安裝配置為自動更新的插件的更新。", + "TaskUpdatePluginsDescription": "下載並更新能夠被自動更新的插件。", "TaskRefreshPeopleDescription": "更新媒體庫中演員和導演的元數據。", "TaskCleanLogsDescription": "刪除超過{0}天的日誌文件。", "TaskCleanLogs": "清理日誌目錄", "TaskRefreshLibrary": "掃描媒體庫", - "TaskRefreshChapterImagesDescription": "為帶有章節的視頻創建縮略圖。", + "TaskRefreshChapterImagesDescription": "為帶有章節的影片建立縮圖。", "TaskRefreshChapterImages": "提取章節圖像", "TaskCleanCacheDescription": "刪除系統不再需要的緩存文件。", "TaskCleanCache": "清理緩存目錄", - "TasksChannelsCategory": "互聯網頻道", + "TasksChannelsCategory": "網絡頻道", "TasksLibraryCategory": "庫", - "TaskRefreshPeople": "刷新人物", + "TaskRefreshPeople": "重新載入人物", "TaskCleanActivityLog": "清理活動記錄", "Undefined": "未定義", "Forced": "強制", @@ -121,7 +121,7 @@ "TaskOptimizeDatabaseDescription": "壓縮數據庫並截斷可用空間。在掃描媒體庫或執行其他數據庫的修改後運行此任務可能會提高性能。", "TaskOptimizeDatabase": "最佳化數據庫", "TaskCleanActivityLogDescription": "刪除早於設定時間的日誌記錄。", - "TaskKeyframeExtractorDescription": "提取關鍵格以創建更準確的HLS播放列表。次指示可能用時很長。", + "TaskKeyframeExtractorDescription": "提取關鍵幀以建立更準確的 HLS 播放列表。此工作或需要使用較長時間來完成。", "TaskKeyframeExtractor": "關鍵幀提取器", "External": "外部", "HearingImpaired": "聽力障礙" From 6b28e02cfcca7fd21852c0c9fe2d2a7fecee312a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 5 Apr 2023 09:55:31 +0000 Subject: [PATCH 213/858] Update peter-evans/create-or-update-comment action to v3 --- .github/workflows/commands.yml | 10 +++++----- .github/workflows/openapi.yml | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index 236a11a137..589383e830 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Notify as seen - uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2.1.1 + uses: peter-evans/create-or-update-comment@3383acd359705b10cb1eeef05c0e88c056ea4666 # v3.0.0 with: token: ${{ secrets.JF_BOT_TOKEN }} comment-id: ${{ github.event.comment.id }} @@ -43,7 +43,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Notify as seen - uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2.1.1 + uses: peter-evans/create-or-update-comment@3383acd359705b10cb1eeef05c0e88c056ea4666 # v3.0.0 if: ${{ github.event.comment != null }} with: token: ${{ secrets.JF_BOT_TOKEN }} @@ -58,7 +58,7 @@ jobs: - name: Notify as running id: comment_running - uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2.1.1 + uses: peter-evans/create-or-update-comment@3383acd359705b10cb1eeef05c0e88c056ea4666 # v3.0.0 if: ${{ github.event.comment != null }} with: token: ${{ secrets.JF_BOT_TOKEN }} @@ -93,7 +93,7 @@ jobs: exit ${retcode} - name: Notify with result success - uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2.1.1 + uses: peter-evans/create-or-update-comment@3383acd359705b10cb1eeef05c0e88c056ea4666 # v3.0.0 if: ${{ github.event.comment != null && success() }} with: token: ${{ secrets.JF_BOT_TOKEN }} @@ -108,7 +108,7 @@ jobs: reactions: hooray - name: Notify with result failure - uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2.1.1 + uses: peter-evans/create-or-update-comment@3383acd359705b10cb1eeef05c0e88c056ea4666 # v3.0.0 if: ${{ github.event.comment != null && failure() }} with: token: ${{ secrets.JF_BOT_TOKEN }} diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index c68679cede..2ce47555b2 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -110,7 +110,7 @@ jobs: direction: last body-includes: openapi-diff-workflow-comment - name: Reply or edit difference comment (changed) - uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2.1.1 + uses: peter-evans/create-or-update-comment@3383acd359705b10cb1eeef05c0e88c056ea4666 # v3.0.0 if: ${{ steps.read-diff.outputs.body != '' }} with: issue-number: ${{ github.event.pull_request.number }} @@ -125,7 +125,7 @@ jobs: </details> - name: Edit difference comment (unchanged) - uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2.1.1 + uses: peter-evans/create-or-update-comment@3383acd359705b10cb1eeef05c0e88c056ea4666 # v3.0.0 if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }} with: issue-number: ${{ github.event.pull_request.number }} From f794003a3a485a5c6e03499e5f67d69318a9a7d6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 5 Apr 2023 17:09:22 +0000 Subject: [PATCH 214/858] Update github/codeql-action action to v2.2.10 --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 73aafe2732..f7fef8e80b 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@04df1262e6247151b5ac09cd2c303ac36ad3f62b # v2.2.9 + uses: github/codeql-action/init@8c8d71dde4abced210732d8486586914b97752e8 # v2.2.10 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@04df1262e6247151b5ac09cd2c303ac36ad3f62b # v2.2.9 + uses: github/codeql-action/autobuild@8c8d71dde4abced210732d8486586914b97752e8 # v2.2.10 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@04df1262e6247151b5ac09cd2c303ac36ad3f62b # v2.2.9 + uses: github/codeql-action/analyze@8c8d71dde4abced210732d8486586914b97752e8 # v2.2.10 From 779a22a76adf61484571e5cae5cb6fb978f94ed0 Mon Sep 17 00:00:00 2001 From: Stepan Goremykin <goremukin@gmail.com> Date: Thu, 6 Apr 2023 18:33:34 +0200 Subject: [PATCH 215/858] Remove redundant Cast --- MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index 3980353d15..53b8a6e678 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -491,7 +491,6 @@ namespace MediaBrowser.MediaEncoding.Encoder var found = Regex .Matches(output, @"^\s\S{6}\s(?<codec>[\w|-]+)\s+.+$", RegexOptions.Multiline) - .Cast<Match>() .Select(x => x.Groups["codec"].Value) .Where(x => required.Contains(x)); @@ -520,7 +519,6 @@ namespace MediaBrowser.MediaEncoding.Encoder var found = Regex .Matches(output, @"^\s\S{3}\s(?<filter>[\w|-]+)\s+.+$", RegexOptions.Multiline) - .Cast<Match>() .Select(x => x.Groups["filter"].Value) .Where(x => _requiredFilters.Contains(x)); From 2c03f7e85d9bca7b243f0021f454b6bfd780d2f3 Mon Sep 17 00:00:00 2001 From: Stepan Goremykin <goremukin@gmail.com> Date: Thu, 6 Apr 2023 18:38:23 +0200 Subject: [PATCH 216/858] Use TryGetValue --- Emby.Dlna/PlayTo/TransportCommands.cs | 12 ++++++------ .../MediaEncoding/EncodingHelper.cs | 10 +++------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/Emby.Dlna/PlayTo/TransportCommands.cs b/Emby.Dlna/PlayTo/TransportCommands.cs index c463727329..6b2096d9dc 100644 --- a/Emby.Dlna/PlayTo/TransportCommands.cs +++ b/Emby.Dlna/PlayTo/TransportCommands.cs @@ -116,7 +116,7 @@ namespace Emby.Dlna.PlayTo return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString); } - public string BuildPost(ServiceAction action, string xmlNamesapce, object value, string commandParameter = "") + public string BuildPost(ServiceAction action, string xmlNamespace, object value, string commandParameter = "") { var stateString = string.Empty; @@ -137,10 +137,10 @@ namespace Emby.Dlna.PlayTo } } - return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString); + return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString); } - public string BuildPost(ServiceAction action, string xmlNamesapce, object value, Dictionary<string, string> dictionary) + public string BuildPost(ServiceAction action, string xmlNamespace, object value, Dictionary<string, string> dictionary) { var stateString = string.Empty; @@ -150,9 +150,9 @@ namespace Emby.Dlna.PlayTo { stateString += BuildArgumentXml(arg, "0"); } - else if (dictionary.ContainsKey(arg.Name)) + else if (dictionary.TryGetValue(arg.Name, out var argValue)) { - stateString += BuildArgumentXml(arg, dictionary[arg.Name]); + stateString += BuildArgumentXml(arg, argValue); } else { @@ -160,7 +160,7 @@ namespace Emby.Dlna.PlayTo } } - return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString); + return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString); } private string BuildArgumentXml(Argument argument, string? value, string commandParameter = "") diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 0d5534324f..0124c66c74 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -138,14 +138,10 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(hwType) && encodingOptions.EnableHardwareEncoding - && codecMap.ContainsKey(hwType)) + && codecMap.TryGetValue(hwType, out var preferredEncoder) + && _mediaEncoder.SupportsEncoder(preferredEncoder)) { - var preferredEncoder = codecMap[hwType]; - - if (_mediaEncoder.SupportsEncoder(preferredEncoder)) - { - return preferredEncoder; - } + return preferredEncoder; } } From 26958162d01ea9557d80f384face93012f70b821 Mon Sep 17 00:00:00 2001 From: Stepan Goremykin <goremukin@gmail.com> Date: Thu, 6 Apr 2023 18:41:25 +0200 Subject: [PATCH 217/858] Remove unused using directives --- Emby.Dlna/Main/DlnaEntryPoint.cs | 1 - Emby.Server.Implementations/Dto/DtoService.cs | 1 - Jellyfin.Api/Controllers/FilterController.cs | 1 - Jellyfin.Api/Controllers/QuickConnectController.cs | 2 -- Jellyfin.Api/Controllers/SearchController.cs | 1 - Jellyfin.Api/Controllers/UniversalAudioController.cs | 1 - .../Security/AuthorizationContext.cs | 1 - Jellyfin.Server/CoreAppHost.cs | 1 - Jellyfin.Server/Startup.cs | 1 - MediaBrowser.Controller/Lyrics/LyricMetadata.cs | 2 -- MediaBrowser.Controller/Session/ISessionManager.cs | 1 - MediaBrowser.Controller/Subtitles/ISubtitleManager.cs | 1 - MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs | 1 - MediaBrowser.Model/Configuration/ServerConfiguration.cs | 1 - MediaBrowser.Providers/Lyric/TxtLyricProvider.cs | 1 - .../Json/Converters/JsonBoolStringConverter.cs | 1 - src/Jellyfin.Extensions/StringExtensions.cs | 2 -- .../Auth/FirstTimeSetupPolicy/FirstTimeSetupHandlerTests.cs | 1 - tests/Jellyfin.Api.Tests/Controllers/ImageControllerTests.cs | 1 - 19 files changed, 22 deletions(-) diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index aab475153b..39cfc2d1d4 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -7,7 +7,6 @@ using System.Globalization; using System.Linq; using System.Net.Http; using System.Net.Sockets; -using System.Runtime.InteropServices; using System.Threading.Tasks; using Emby.Dlna.PlayTo; using Emby.Dlna.Ssdp; diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 8b66829032..c725839fba 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -7,7 +7,6 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; -using Jellyfin.Api.Helpers; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Extensions; diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs index dac07429ff..d51a5325f5 100644 --- a/Jellyfin.Api/Controllers/FilterController.cs +++ b/Jellyfin.Api/Controllers/FilterController.cs @@ -1,6 +1,5 @@ using System; using System.Linq; -using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; diff --git a/Jellyfin.Api/Controllers/QuickConnectController.cs b/Jellyfin.Api/Controllers/QuickConnectController.cs index d7e54b5b6a..14f5265aa7 100644 --- a/Jellyfin.Api/Controllers/QuickConnectController.cs +++ b/Jellyfin.Api/Controllers/QuickConnectController.cs @@ -1,8 +1,6 @@ using System; using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; -using Jellyfin.Api.Constants; -using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Authentication; diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs index f638c31c39..387b3ea5a6 100644 --- a/Jellyfin.Api/Controllers/SearchController.cs +++ b/Jellyfin.Api/Controllers/SearchController.cs @@ -3,7 +3,6 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Linq; -using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs index 12d033ae63..2e9035d24f 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -5,7 +5,6 @@ using System.Globalization; using System.Linq; using System.Threading.Tasks; using Jellyfin.Api.Attributes; -using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.StreamingDtos; diff --git a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs index 63d3e8a04c..700e639700 100644 --- a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs +++ b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Net; using System.Threading.Tasks; -using EFCoreSecondLevelCacheInterceptor; using MediaBrowser.Controller; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index 40cd5a0446..939376dd8d 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -22,7 +22,6 @@ using MediaBrowser.Controller.Lyrics; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Security; using MediaBrowser.Model.Activity; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 155f9fc8c1..56f5c4796e 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -4,7 +4,6 @@ using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Net.Mime; -using System.Runtime.InteropServices; using System.Text; using Jellyfin.Api.Middleware; using Jellyfin.MediaEncoding.Hls.Extensions; diff --git a/MediaBrowser.Controller/Lyrics/LyricMetadata.cs b/MediaBrowser.Controller/Lyrics/LyricMetadata.cs index 6091ede52a..c4f0334892 100644 --- a/MediaBrowser.Controller/Lyrics/LyricMetadata.cs +++ b/MediaBrowser.Controller/Lyrics/LyricMetadata.cs @@ -1,5 +1,3 @@ -using System; - namespace MediaBrowser.Controller.Lyrics; /// <summary> diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index eefc5d222e..0c4719a0e5 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -7,7 +7,6 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities.Security; -using Jellyfin.Data.Events; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Session; diff --git a/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs index b86e482435..fcfc18a644 100644 --- a/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs +++ b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs @@ -1,7 +1,6 @@ #pragma warning disable CS1591 using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Entities; diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs index ea520b1d6b..55480253dd 100644 --- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs +++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs @@ -1,4 +1,3 @@ -using System; using System.IO; using System.Linq; using BDInfo.IO; diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 7cb07a2f7a..07f02d1879 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -2,7 +2,6 @@ #pragma warning disable CA1819 using System; -using System.Collections.Generic; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Updates; diff --git a/MediaBrowser.Providers/Lyric/TxtLyricProvider.cs b/MediaBrowser.Providers/Lyric/TxtLyricProvider.cs index 96a9e9dcf3..a9099d1927 100644 --- a/MediaBrowser.Providers/Lyric/TxtLyricProvider.cs +++ b/MediaBrowser.Providers/Lyric/TxtLyricProvider.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.IO; -using System.Linq; using System.Threading.Tasks; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Lyrics; diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonBoolStringConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonBoolStringConverter.cs index 2936fe4d6d..6895eadb86 100644 --- a/src/Jellyfin.Extensions/Json/Converters/JsonBoolStringConverter.cs +++ b/src/Jellyfin.Extensions/Json/Converters/JsonBoolStringConverter.cs @@ -1,7 +1,6 @@ using System; using System.Buffers; using System.Buffers.Text; -using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; diff --git a/src/Jellyfin.Extensions/StringExtensions.cs b/src/Jellyfin.Extensions/StringExtensions.cs index 7c61248757..b22eb7c4ea 100644 --- a/src/Jellyfin.Extensions/StringExtensions.cs +++ b/src/Jellyfin.Extensions/StringExtensions.cs @@ -1,6 +1,4 @@ using System; -using System.Globalization; -using System.Text; using System.Text.RegularExpressions; namespace Jellyfin.Extensions diff --git a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandlerTests.cs index 6669a6689c..1ea1797ba1 100644 --- a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandlerTests.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Threading.Tasks; using AutoFixture; using AutoFixture.AutoMoq; -using Jellyfin.Api.Auth.DefaultAuthorizationPolicy; using Jellyfin.Api.Auth.FirstTimeSetupPolicy; using Jellyfin.Api.Constants; using MediaBrowser.Common.Configuration; diff --git a/tests/Jellyfin.Api.Tests/Controllers/ImageControllerTests.cs b/tests/Jellyfin.Api.Tests/Controllers/ImageControllerTests.cs index d6428fb2cd..0254a1ec6e 100644 --- a/tests/Jellyfin.Api.Tests/Controllers/ImageControllerTests.cs +++ b/tests/Jellyfin.Api.Tests/Controllers/ImageControllerTests.cs @@ -1,4 +1,3 @@ -using System; using Jellyfin.Api.Controllers; using Xunit; From 7d7e177265a6725b58669a2717af85c031f67996 Mon Sep 17 00:00:00 2001 From: Stepan Goremykin <goremukin@gmail.com> Date: Thu, 6 Apr 2023 18:46:28 +0200 Subject: [PATCH 218/858] Remove redundant casts --- Emby.Server.Implementations/Data/BaseSqliteRepository.cs | 2 +- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 4 ++-- MediaBrowser.Model/Dlna/StreamInfo.cs | 2 +- tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs | 6 +++--- .../Parsers/SeriesNfoParserTests.cs | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs index 1d61667f86..47706a4401 100644 --- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs +++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs @@ -139,7 +139,7 @@ namespace Emby.Server.Implementations.Data if (JournalSizeLimit.HasValue) { - WriteConnection.Execute("PRAGMA journal_size_limit=" + (int)JournalSizeLimit.Value); + WriteConnection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value); } if (Synchronous.HasValue) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 0124c66c74..5430ea2043 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -2614,8 +2614,8 @@ namespace MediaBrowser.Controller.MediaEncoding if (outputWidth > maximumWidth || outputHeight > maximumHeight) { - var scaleW = (double)maximumWidth / (double)outputWidth; - var scaleH = (double)maximumHeight / (double)outputHeight; + var scaleW = (double)maximumWidth / outputWidth; + var scaleH = (double)maximumHeight / outputHeight; var scale = Math.Min(scaleW, scaleH); outputWidth = Math.Min(maximumWidth, (int)(outputWidth * scale)); outputHeight = Math.Min(maximumHeight, (int)(outputHeight * scale)); diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index 886b64a248..a78a28e13a 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -430,7 +430,7 @@ namespace MediaBrowser.Model.Dlna return totalBitrate.HasValue ? Convert.ToInt64(totalBitrate.Value * totalSeconds) : - (long?)null; + null; } return null; diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs index f05a0152e0..a1c73d57e0 100644 --- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs +++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs @@ -162,7 +162,7 @@ namespace Jellyfin.Model.Tests [InlineData("Tizen4-4K-5.1", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen4-4K-5.1", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen4-4K-5.1", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] - public async Task BuildVideoItemSimple(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = (TranscodeReason)0, string transcodeMode = "DirectStream", string transcodeProtocol = "") + public async Task BuildVideoItemSimple(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = 0, string transcodeMode = "DirectStream", string transcodeProtocol = "") { var options = await GetMediaOptions(deviceName, mediaSource); BuildVideoItemSimpleTest(options, playMethod, why, transcodeMode, transcodeProtocol); @@ -260,7 +260,7 @@ namespace Jellyfin.Model.Tests [InlineData("Tizen4-4K-5.1", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen4-4K-5.1", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen4-4K-5.1", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] - public async Task BuildVideoItemWithFirstExplicitStream(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = (TranscodeReason)0, string transcodeMode = "DirectStream", string transcodeProtocol = "") + public async Task BuildVideoItemWithFirstExplicitStream(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = 0, string transcodeMode = "DirectStream", string transcodeProtocol = "") { var options = await GetMediaOptions(deviceName, mediaSource); options.AudioStreamIndex = 1; @@ -296,7 +296,7 @@ namespace Jellyfin.Model.Tests // Tizen 4 4K 5.1 [InlineData("Tizen4-4K-5.1", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] [InlineData("Tizen4-4K-5.1", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] - public async Task BuildVideoItemWithDirectPlayExplicitStreams(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = (TranscodeReason)0, string transcodeMode = "DirectStream", string transcodeProtocol = "") + public async Task BuildVideoItemWithDirectPlayExplicitStreams(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = 0, string transcodeMode = "DirectStream", string transcodeProtocol = "") { var options = await GetMediaOptions(deviceName, mediaSource); var streamCount = options.MediaSources[0].MediaStreams.Count; diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeriesNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeriesNfoParserTests.cs index 542324e78b..f680d2dcc5 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeriesNfoParserTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeriesNfoParserTests.cs @@ -90,7 +90,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers }; _parser.Fetch(result, path, CancellationToken.None); - var item = (Series)result.Item; + var item = result.Item; Assert.Equal(id, item.ProviderIds[provider]); } From 1c0bb828d23859df363e46d18462c425ae1680fa Mon Sep 17 00:00:00 2001 From: Stepan Goremykin <goremukin@gmail.com> Date: Thu, 6 Apr 2023 19:05:05 +0200 Subject: [PATCH 219/858] Fix argument is not used in message template warning --- Emby.Server.Implementations/Session/SessionManager.cs | 4 ++-- MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index afa3721b88..5f6dc93fb3 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -606,7 +606,7 @@ namespace Emby.Server.Implementations.Session } catch (Exception ex) { - _logger.LogDebug("Error calling OnPlaybackStopped", ex); + _logger.LogDebug(ex, "Error calling OnPlaybackStopped"); } } @@ -953,7 +953,7 @@ namespace Emby.Server.Implementations.Session } catch (Exception ex) { - _logger.LogError("Error closing live stream", ex); + _logger.LogError(ex, "Error closing live stream"); } } diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 90bc491322..3b85988715 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -449,7 +449,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles { try { - _logger.LogInformation("Deleting converted subtitle due to failure: ", outputPath); + _logger.LogInformation("Deleting converted subtitle due to failure: {Path}", outputPath); _fileSystem.DeleteFile(outputPath); } catch (IOException ex) From 5508efc2e27400e05a22e75a08baf69c7cb1d808 Mon Sep 17 00:00:00 2001 From: Stepan Goremykin <goremukin@gmail.com> Date: Thu, 6 Apr 2023 19:08:53 +0200 Subject: [PATCH 220/858] Remove bitwise operator on enum that is not marked by [Flags] attribute --- MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs index 55480253dd..fca17d4c05 100644 --- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs +++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs @@ -104,7 +104,7 @@ public class BdInfoDirectoryInfo : IDirectoryInfo _impl.FullName, new[] { searchPattern }, false, - (searchOption & SearchOption.AllDirectories) == SearchOption.AllDirectories) + searchOption == SearchOption.AllDirectories) .Select(x => new BdInfoFileInfo(x)) .ToArray(); } From 08ce4772262cab7961ea1000b0a9dd536244a667 Mon Sep 17 00:00:00 2001 From: Stepan Goremykin <goremukin@gmail.com> Date: Thu, 6 Apr 2023 19:10:15 +0200 Subject: [PATCH 221/858] Fix duplicate properties in message template warning --- MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs index 80091bf5a8..ec65dd80f4 100644 --- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs +++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs @@ -233,7 +233,7 @@ namespace MediaBrowser.MediaEncoding.Attachments } else { - _logger.LogInformation("ffmpeg attachment extraction completed for {Path} to {Path}", inputPath, outputPath); + _logger.LogInformation("ffmpeg attachment extraction completed for {InputPath} to {OutputPath}", inputPath, outputPath); } } @@ -378,7 +378,7 @@ namespace MediaBrowser.MediaEncoding.Attachments } else { - _logger.LogInformation("ffmpeg attachment extraction completed for {Path} to {Path}", inputPath, outputPath); + _logger.LogInformation("ffmpeg attachment extraction completed for {InputPath} to {OutputPath}", inputPath, outputPath); } } From 9d738bb60159b4846554f7d15c325130296ad9eb Mon Sep 17 00:00:00 2001 From: Stepan Goremykin <goremukin@gmail.com> Date: Thu, 6 Apr 2023 19:11:45 +0200 Subject: [PATCH 222/858] Remove redundant ToString call --- MediaBrowser.Common/Plugins/BasePluginOfT.cs | 2 +- MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs | 2 +- RSSDP/SsdpDevicePublisher.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/MediaBrowser.Common/Plugins/BasePluginOfT.cs b/MediaBrowser.Common/Plugins/BasePluginOfT.cs index 152fa8b4a2..116e9cef80 100644 --- a/MediaBrowser.Common/Plugins/BasePluginOfT.cs +++ b/MediaBrowser.Common/Plugins/BasePluginOfT.cs @@ -50,7 +50,7 @@ namespace MediaBrowser.Common.Plugins if (Version is not null && !Directory.Exists(dataFolderPath)) { // Try again with the version number appended to the folder name. - dataFolderPath += "_" + Version.ToString(); + dataFolderPath += "_" + Version; } SetAttributes(assemblyFilePath, dataFolderPath, assemblyName.Version); diff --git a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs index a6fcdb9e17..f913b2320d 100644 --- a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs +++ b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs @@ -375,7 +375,7 @@ namespace MediaBrowser.LocalMetadata.Savers await writer.WriteStartElementAsync(null, "Person", null).ConfigureAwait(false); await writer.WriteElementStringAsync(null, "Name", null, person.Name).ConfigureAwait(false); await writer.WriteElementStringAsync(null, "Type", null, person.Type.ToString()).ConfigureAwait(false); - await writer.WriteElementStringAsync(null, "Role", null, person.Role.ToString()).ConfigureAwait(false); + await writer.WriteElementStringAsync(null, "Role", null, person.Role).ConfigureAwait(false); if (person.SortOrder.HasValue) { diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs index a7767b3c04..e073ffa96c 100644 --- a/RSSDP/SsdpDevicePublisher.cs +++ b/RSSDP/SsdpDevicePublisher.cs @@ -644,7 +644,7 @@ namespace Rssdp.Infrastructure public string Key { - get { return this.SearchTarget + ":" + this.EndPoint.ToString(); } + get { return this.SearchTarget + ":" + this.EndPoint; } } public bool IsOld() From 6ae1903453102a4bb6159b755e503ce0e0bae2f6 Mon Sep 17 00:00:00 2001 From: Stepan Goremykin <goremukin@gmail.com> Date: Thu, 6 Apr 2023 19:16:12 +0200 Subject: [PATCH 223/858] Use TryAdd --- MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs | 5 +---- MediaBrowser.Providers/Manager/MetadataService.cs | 5 +---- MediaBrowser.Providers/Manager/ProviderManager.cs | 5 +---- MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs | 5 +---- 4 files changed, 4 insertions(+), 16 deletions(-) diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs index 7eab00be4d..cb369d8377 100644 --- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs @@ -73,10 +73,7 @@ namespace MediaBrowser.LocalMetadata.Parsers foreach (var info in idInfos) { var id = info.Key + "Id"; - if (!_validProviderIds.ContainsKey(id)) - { - _validProviderIds.Add(id, info.Key); - } + _validProviderIds.TryAdd(id, info.Key); } // Additional Mappings diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index ebd653e349..bcc9b809c2 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -882,10 +882,7 @@ namespace MediaBrowser.Providers.Manager var key = providerId.Key; // Don't replace existing Id's. - if (!lookupInfo.ProviderIds.ContainsKey(key)) - { - lookupInfo.ProviderIds[key] = providerId.Value; - } + lookupInfo.ProviderIds.TryAdd(key, providerId.Value); } } diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index d8122511ee..1028da32ba 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -780,10 +780,7 @@ namespace MediaBrowser.Providers.Manager { foreach (var providerId in result.ProviderIds) { - if (!existingMatch.ProviderIds.ContainsKey(providerId.Key)) - { - existingMatch.ProviderIds.Add(providerId.Key, providerId.Value); - } + existingMatch.ProviderIds.TryAdd(providerId.Key, providerId.Value); } if (string.IsNullOrWhiteSpace(existingMatch.ImageUrl)) diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs index 111d0c5cbc..5b68924acb 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs @@ -100,10 +100,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers foreach (var info in idInfos) { var id = info.Key + "Id"; - if (!_validProviderIds.ContainsKey(id)) - { - _validProviderIds.Add(id, info.Key); - } + _validProviderIds.TryAdd(id, info.Key); } // Additional Mappings From c051736c800c56767e238ccf2b93054cf5a719f1 Mon Sep 17 00:00:00 2001 From: Stepan Goremykin <goremukin@gmail.com> Date: Thu, 6 Apr 2023 19:21:29 +0200 Subject: [PATCH 224/858] Inline out variable declaration --- Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs | 3 +-- Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs | 3 +-- RSSDP/HttpRequestParser.cs | 3 +-- RSSDP/HttpResponseParser.cs | 3 +-- RSSDP/SsdpDeviceLocator.cs | 6 ++---- RSSDP/SsdpDevicePublisher.cs | 6 ++---- .../Manager/MetadataServiceTests.cs | 7 ++----- 7 files changed, 10 insertions(+), 21 deletions(-) diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index e7f4d2f4eb..477fd9df1e 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -1866,8 +1866,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { await writer.WriteStartDocumentAsync(true).ConfigureAwait(false); await writer.WriteStartElementAsync(null, "tvshow", null).ConfigureAwait(false); - string id; - if (timer.SeriesProviderIds.TryGetValue(MetadataProvider.Tvdb.ToString(), out id)) + if (timer.SeriesProviderIds.TryGetValue(MetadataProvider.Tvdb.ToString(), out var id)) { await writer.WriteElementStringAsync(null, "id", null, id).ConfigureAwait(false); } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs index f2020e05fe..b418162304 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs @@ -170,9 +170,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts var nameInExtInf = nameParts.Length > 1 ? nameParts[^1].AsSpan().Trim() : ReadOnlySpan<char>.Empty; string numberString = null; - string attributeValue; - if (attributes.TryGetValue("tvg-chno", out attributeValue) + if (attributes.TryGetValue("tvg-chno", out var attributeValue) && double.TryParse(attributeValue, CultureInfo.InvariantCulture, out _)) { numberString = attributeValue; diff --git a/RSSDP/HttpRequestParser.cs b/RSSDP/HttpRequestParser.cs index a3e100796d..a1b4627a93 100644 --- a/RSSDP/HttpRequestParser.cs +++ b/RSSDP/HttpRequestParser.cs @@ -64,8 +64,7 @@ namespace Rssdp.Infrastructure } message.Method = new HttpMethod(parts[0].Trim()); - Uri requestUri; - if (Uri.TryCreate(parts[1].Trim(), UriKind.RelativeOrAbsolute, out requestUri)) + if (Uri.TryCreate(parts[1].Trim(), UriKind.RelativeOrAbsolute, out var requestUri)) { message.RequestUri = requestUri; } diff --git a/RSSDP/HttpResponseParser.cs b/RSSDP/HttpResponseParser.cs index 3e361465d7..71b7a7b990 100644 --- a/RSSDP/HttpResponseParser.cs +++ b/RSSDP/HttpResponseParser.cs @@ -77,8 +77,7 @@ namespace Rssdp.Infrastructure message.Version = ParseHttpVersion(parts[0].Trim()); - int statusCode = -1; - if (!Int32.TryParse(parts[1].Trim(), out statusCode)) + if (!Int32.TryParse(parts[1].Trim(), out var statusCode)) { throw new ArgumentException("data status line is invalid. Status code is not a valid integer.", nameof(data)); } diff --git a/RSSDP/SsdpDeviceLocator.cs b/RSSDP/SsdpDeviceLocator.cs index 681ef0a5c1..de48bd8798 100644 --- a/RSSDP/SsdpDeviceLocator.cs +++ b/RSSDP/SsdpDeviceLocator.cs @@ -483,8 +483,7 @@ namespace Rssdp.Infrastructure } } - Uri retVal; - Uri.TryCreate(value, UriKind.RelativeOrAbsolute, out retVal); + Uri.TryCreate(value, UriKind.RelativeOrAbsolute, out var retVal); return retVal; } @@ -501,8 +500,7 @@ namespace Rssdp.Infrastructure } } - Uri retVal; - Uri.TryCreate(value, UriKind.RelativeOrAbsolute, out retVal); + Uri.TryCreate(value, UriKind.RelativeOrAbsolute, out var retVal); return retVal; } diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs index e073ffa96c..0225d4f7db 100644 --- a/RSSDP/SsdpDevicePublisher.cs +++ b/RSSDP/SsdpDevicePublisher.cs @@ -244,7 +244,6 @@ namespace Rssdp.Infrastructure // Wait on random interval up to MX, as per SSDP spec. // Also, as per UPnP 1.1/SSDP spec ignore missing/bank MX header. If over 120, assume random value between 0 and 120. // Using 16 as minimum as that's often the minimum system clock frequency anyway. - int maxWaitInterval = 0; if (String.IsNullOrEmpty(mx)) { // Windows Explorer is poorly behaved and doesn't supply an MX header value. @@ -254,7 +253,7 @@ namespace Rssdp.Infrastructure // return; } - if (!Int32.TryParse(mx, out maxWaitInterval) || maxWaitInterval <= 0) + if (!Int32.TryParse(mx, out var maxWaitInterval) || maxWaitInterval <= 0) { return; } @@ -581,8 +580,7 @@ namespace Rssdp.Infrastructure private string GetFirstHeaderValue(System.Net.Http.Headers.HttpRequestHeaders httpRequestHeaders, string headerName) { string retVal = null; - IEnumerable<String> values = null; - if (httpRequestHeaders.TryGetValues(headerName, out values) && values != null) + if (httpRequestHeaders.TryGetValues(headerName, out var values) && values != null) { retVal = values.FirstOrDefault(); } diff --git a/tests/Jellyfin.Providers.Tests/Manager/MetadataServiceTests.cs b/tests/Jellyfin.Providers.Tests/Manager/MetadataServiceTests.cs index e18faa422d..ec4df9981c 100644 --- a/tests/Jellyfin.Providers.Tests/Manager/MetadataServiceTests.cs +++ b/tests/Jellyfin.Providers.Tests/Manager/MetadataServiceTests.cs @@ -238,9 +238,6 @@ namespace Jellyfin.Providers.Tests.Manager } }; - object? result; - List<PersonInfo> actual; - // overwrite provider id var overwriteNewValue = new List<PersonInfo> { @@ -249,9 +246,9 @@ namespace Jellyfin.Providers.Tests.Manager Name = "Name 2" } }; - Assert.False(TestMergeBaseItemDataPerson(GetOldValue(), overwriteNewValue, null, false, out result)); + Assert.False(TestMergeBaseItemDataPerson(GetOldValue(), overwriteNewValue, null, false, out var result)); // People not already in target are not merged into it from source - actual = (List<PersonInfo>)result!; + List<PersonInfo> actual = (List<PersonInfo>)result!; Assert.Single(actual); Assert.Equal("Name 1", actual[0].Name); From dabdc86a1bacfd55de3c38b04adbe9fe3643e27e Mon Sep 17 00:00:00 2001 From: Stepan Goremykin <goremukin@gmail.com> Date: Thu, 6 Apr 2023 19:25:25 +0200 Subject: [PATCH 225/858] Pass cancellation token --- .../LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs index 81eb083f6f..7bc209d6bd 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs @@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun public async Task<bool> CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken) { using var client = new TcpClient(); - await client.ConnectAsync(remoteIp, HdHomeRunPort).ConfigureAwait(false); + await client.ConnectAsync(remoteIp, HdHomeRunPort, cancellationToken).ConfigureAwait(false); using var stream = client.GetStream(); return await CheckTunerAvailability(stream, tuner, cancellationToken).ConfigureAwait(false); From 19e65269a24714469bb6a2b3e15602b73327e5d3 Mon Sep 17 00:00:00 2001 From: Stepan Goremykin <goremukin@gmail.com> Date: Thu, 6 Apr 2023 19:27:57 +0200 Subject: [PATCH 226/858] Simplify linq expressions (use All) --- .../Library/MediaSourceManager.cs | 4 ++-- .../Localization/LocalizationManager.cs | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index eadfa5dfe9..c9a26a30f5 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -154,8 +154,8 @@ namespace Emby.Server.Implementations.Library // If file is strm or main media stream is missing, force a metadata refresh with remote probing if (allowMediaProbe && mediaSources[0].Type != MediaSourceType.Placeholder && (item.Path.EndsWith(".strm", StringComparison.OrdinalIgnoreCase) - || (item.MediaType == MediaType.Video && !mediaSources[0].MediaStreams.Any(i => i.Type == MediaStreamType.Video)) - || (item.MediaType == MediaType.Audio && !mediaSources[0].MediaStreams.Any(i => i.Type == MediaStreamType.Audio)))) + || (item.MediaType == MediaType.Video && mediaSources[0].MediaStreams.All(i => i.Type != MediaStreamType.Video)) + || (item.MediaType == MediaType.Audio && mediaSources[0].MediaStreams.All(i => i.Type != MediaStreamType.Audio)))) { await item.RefreshMetadata( new MetadataRefreshOptions(_directoryService) diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index 166b71b4a3..96f4353998 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -198,25 +198,25 @@ namespace Emby.Server.Implementations.Localization } // Minimum rating possible - if (!ratings.Any(x => x.Value == 0)) + if (ratings.All(x => x.Value != 0)) { ratings.Add(new ParentalRating("Approved", 0)); } // Matches PG (this has different age restrictions depending on country) - if (!ratings.Any(x => x.Value == 10)) + if (ratings.All(x => x.Value != 10)) { ratings.Add(new ParentalRating("10", 10)); } // Matches PG-13 - if (!ratings.Any(x => x.Value == 13)) + if (ratings.All(x => x.Value != 13)) { ratings.Add(new ParentalRating("13", 13)); } // Matches TV-14 - if (!ratings.Any(x => x.Value == 14)) + if (ratings.All(x => x.Value != 14)) { ratings.Add(new ParentalRating("14", 14)); } @@ -229,13 +229,13 @@ namespace Emby.Server.Implementations.Localization } // A lot of countries don't excplicitly have a seperate rating for adult content - if (!ratings.Any(x => x.Value == 1000)) + if (ratings.All(x => x.Value != 1000)) { ratings.Add(new ParentalRating("XXX", 1000)); } // A lot of countries don't excplicitly have a seperate rating for banned content - if (!ratings.Any(x => x.Value == 1001)) + if (ratings.All(x => x.Value != 1001)) { ratings.Add(new ParentalRating("Banned", 1001)); } From b6cfdb8b92650aa4f5c33b9523700ffeaa7694d0 Mon Sep 17 00:00:00 2001 From: Stepan Goremykin <goremukin@gmail.com> Date: Thu, 6 Apr 2023 19:30:17 +0200 Subject: [PATCH 227/858] Simplify conditional expression --- Emby.Server.Implementations/Dto/DtoService.cs | 4 +--- Emby.Server.Implementations/Library/LibraryManager.cs | 4 +--- .../Migrations/Routines/MigrateDisplayPreferencesDb.cs | 4 +--- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index c725839fba..8fa2f05662 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -571,9 +571,7 @@ namespace Emby.Server.Implementations.Dto return null; } }).Where(i => i is not null) - .Where(i => user is null ? - true : - i.IsVisible(user)) + .Where(i => user is null || i.IsVisible(user)) .DistinctBy(x => x.Name, StringComparer.OrdinalIgnoreCase) .ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase); diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 36a300a572..8883aff0b0 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -2749,9 +2749,7 @@ namespace Emby.Server.Implementations.Library } }) .Where(i => i is not null) - .Where(i => query.User is null ? - true : - i.IsVisible(query.User)) + .Where(i => query.User is null || i.IsVisible(query.User)) .ToList(); } diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs index 7c4ffdbc00..8fe2b087d9 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs @@ -133,9 +133,7 @@ namespace Jellyfin.Server.Migrations.Routines SkipBackwardLength = dto.CustomPrefs.TryGetValue("skipBackLength", out length) && int.TryParse(length, out var skipBackwardLength) ? skipBackwardLength : 10000, - EnableNextVideoInfoOverlay = dto.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enabled) && !string.IsNullOrEmpty(enabled) - ? bool.Parse(enabled) - : true, + EnableNextVideoInfoOverlay = !dto.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enabled) || string.IsNullOrEmpty(enabled) || bool.Parse(enabled), DashboardTheme = dto.CustomPrefs.TryGetValue("dashboardtheme", out var theme) ? theme : string.Empty, TvHome = dto.CustomPrefs.TryGetValue("tvhome", out var home) ? home : string.Empty }; From 910617bbc3b960ff6a1e1c18c27a0b42a4be9ee0 Mon Sep 17 00:00:00 2001 From: Stepan Goremykin <goremukin@gmail.com> Date: Thu, 6 Apr 2023 19:38:34 +0200 Subject: [PATCH 228/858] Remove redundant 'else' keywords --- .../Data/SqliteItemRepository.cs | 42 ++++-- .../Library/Resolvers/BaseVideoResolver.cs | 3 +- .../LiveTv/EmbyTV/EmbyTV.cs | 6 +- .../LiveTv/Listings/SchedulesDirect.cs | 9 +- Emby.Server.Implementations/SyncPlay/Group.cs | 12 +- .../SyncPlay/SyncPlayManager.cs | 6 +- .../Controllers/SubtitleController.cs | 6 +- Jellyfin.Networking/Manager/NetworkManager.cs | 12 +- MediaBrowser.Controller/Entities/BaseItem.cs | 14 +- .../LiveTv/LiveTvProgram.cs | 6 +- .../MediaEncoding/EncodingHelper.cs | 132 +++++++++--------- .../SyncPlay/GroupStates/WaitingGroupState.cs | 8 +- .../SyncPlay/Queue/PlayQueueManager.cs | 29 ++-- .../Attachments/AttachmentExtractor.cs | 12 +- .../Encoder/EncoderValidator.cs | 6 +- .../Subtitles/SubtitleEncoder.cs | 6 +- .../Cryptography/PasswordHash.cs | 3 +- MediaBrowser.Model/Dlna/StreamBuilder.cs | 24 ++-- MediaBrowser.Model/MediaInfo/AudioCodec.cs | 6 +- RSSDP/HttpParserBase.cs | 6 +- RSSDP/SsdpDevice.cs | 6 +- RSSDP/SsdpDeviceLocator.cs | 6 +- RSSDP/SsdpDevicePublisher.cs | 6 +- .../AlphanumericComparator.cs | 15 +- 24 files changed, 188 insertions(+), 193 deletions(-) diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index fcff5f98c6..580fcee86f 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -1309,7 +1309,8 @@ namespace Emby.Server.Implementations.Data { return false; } - else if (type == typeof(UserRootFolder)) + + if (type == typeof(UserRootFolder)) { return false; } @@ -1319,55 +1320,68 @@ namespace Emby.Server.Implementations.Data { return false; } - else if (type == typeof(MusicArtist)) + + if (type == typeof(MusicArtist)) { return false; } - else if (type == typeof(Person)) + + if (type == typeof(Person)) { return false; } - else if (type == typeof(MusicGenre)) + + if (type == typeof(MusicGenre)) { return false; } - else if (type == typeof(Genre)) + + if (type == typeof(Genre)) { return false; } - else if (type == typeof(Studio)) + + if (type == typeof(Studio)) { return false; } - else if (type == typeof(PlaylistsFolder)) + + if (type == typeof(PlaylistsFolder)) { return false; } - else if (type == typeof(PhotoAlbum)) + + if (type == typeof(PhotoAlbum)) { return false; } - else if (type == typeof(Year)) + + if (type == typeof(Year)) { return false; } - else if (type == typeof(Book)) + + if (type == typeof(Book)) { return false; } - else if (type == typeof(LiveTvProgram)) + + if (type == typeof(LiveTvProgram)) { return false; } - else if (type == typeof(AudioBook)) + + if (type == typeof(AudioBook)) { return false; } - else if (type == typeof(Audio)) + + if (type == typeof(Audio)) { return false; } - else if (type == typeof(MusicAlbum)) + + if (type == typeof(MusicAlbum)) { return false; } diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs index 4fac91bf1e..381796d0e3 100644 --- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs @@ -78,7 +78,8 @@ namespace Emby.Server.Implementations.Library.Resolvers Set3DFormat(videoTmp); return videoTmp; } - else if (IsBluRayDirectory(filename)) + + if (IsBluRayDirectory(filename)) { var videoTmp = new TVideoType { diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index 477fd9df1e..b9d0f170ac 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -627,10 +627,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV _timerProvider.Update(existingTimer); return Task.FromResult(existingTimer.Id); } - else - { - throw new ArgumentException("A scheduled recording already exists for this program."); - } + + throw new ArgumentException("A scheduled recording already exists for this program."); } info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index b5e742f98f..ca3e45707b 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -415,14 +415,13 @@ namespace Emby.Server.Implementations.LiveTv.Listings { return null; } - else if (uri.IndexOf("http", StringComparison.OrdinalIgnoreCase) != -1) + + if (uri.IndexOf("http", StringComparison.OrdinalIgnoreCase) != -1) { return uri; } - else - { - return apiUrl + "/image/" + uri + "?token=" + token; - } + + return apiUrl + "/image/" + uri + "?token=" + token; } private static double GetAspectRatio(ImageDataDto i) diff --git a/Emby.Server.Implementations/SyncPlay/Group.cs b/Emby.Server.Implementations/SyncPlay/Group.cs index 7d7ea58106..da8f949326 100644 --- a/Emby.Server.Implementations/SyncPlay/Group.cs +++ b/Emby.Server.Implementations/SyncPlay/Group.cs @@ -620,10 +620,8 @@ namespace Emby.Server.Implementations.SyncPlay RestartCurrentItem(); return true; } - else - { - return false; - } + + return false; } /// <inheritdoc /> @@ -637,10 +635,8 @@ namespace Emby.Server.Implementations.SyncPlay RestartCurrentItem(); return true; } - else - { - return false; - } + + return false; } /// <inheritdoc /> diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs index 63c4a15564..00c655634a 100644 --- a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs +++ b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs @@ -339,10 +339,8 @@ namespace Emby.Server.Implementations.SyncPlay { return sessionsCounter > 0; } - else - { - return false; - } + + return false; } /// <summary> diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index e384213380..b3e9d62972 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -533,10 +533,8 @@ public class SubtitleController : BaseJellyfinApiController _logger.LogDebug("Fallback font size is {FileSize} Bytes", fileSize); return PhysicalFile(fontFile.FullName, MimeTypes.GetMimeType(fontFile.FullName)); } - else - { - _logger.LogWarning("The selected font is null or empty"); - } + + _logger.LogWarning("The selected font is null or empty"); } else { diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index ac9258ad13..a6d5252ffc 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -500,10 +500,8 @@ namespace Jellyfin.Networking.Manager { return true; } - else - { - return address.IsPrivateAddressRange(); - } + + return address.IsPrivateAddressRange(); } /// <inheritdoc/> @@ -1171,13 +1169,15 @@ namespace Jellyfin.Networking.Manager bindPreference = addr.Value; break; } - else if ((addr.Key.Address.Equals(IPAddress.Any) || addr.Key.Address.Equals(IPAddress.IPv6Any)) && isInExternalSubnet) + + if ((addr.Key.Address.Equals(IPAddress.Any) || addr.Key.Address.Equals(IPAddress.IPv6Any)) && isInExternalSubnet) { // External. bindPreference = addr.Value; break; } - else if (addr.Key.Contains(source)) + + if (addr.Key.Contains(source)) { // Match ip address. bindPreference = addr.Value; diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index a04f02bf91..adc7b2f953 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -801,16 +801,14 @@ namespace MediaBrowser.Controller.Entities { return allowed.Contains(ChannelId); } - else - { - var collectionFolders = LibraryManager.GetCollectionFolders(this, allCollectionFolders); - foreach (var folder in collectionFolders) + var collectionFolders = LibraryManager.GetCollectionFolders(this, allCollectionFolders); + + foreach (var folder in collectionFolders) + { + if (allowed.Contains(folder.Id)) { - if (allowed.Contains(folder.Id)) - { - return true; - } + return true; } } diff --git a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs index 514323238e..c721fb7785 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs @@ -197,10 +197,8 @@ namespace MediaBrowser.Controller.LiveTv { return 2.0 / 3; } - else - { - return 16.0 / 9; - } + + return 16.0 / 9; } public override string GetClientTypeName() diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 5430ea2043..53f48d9598 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -557,7 +557,8 @@ namespace MediaBrowser.Controller.MediaEncoding { return Array.FindIndex(_videoProfilesH264, x => string.Equals(x, profile, StringComparison.OrdinalIgnoreCase)); } - else if (string.Equals("hevc", videoCodec, StringComparison.OrdinalIgnoreCase)) + + if (string.Equals("hevc", videoCodec, StringComparison.OrdinalIgnoreCase)) { return Array.FindIndex(_videoProfilesH265, x => string.Equals(x, profile, StringComparison.OrdinalIgnoreCase)); } @@ -1109,19 +1110,19 @@ namespace MediaBrowser.Controller.MediaEncoding { return "-bsf:v h264_mp4toannexb"; } - else if (IsH265(stream)) + + if (IsH265(stream)) { return "-bsf:v hevc_mp4toannexb"; } - else if (IsAAC(stream)) + + if (IsAAC(stream)) { // Convert adts header(mpegts) to asc header(mp4). return "-bsf:a aac_adtstoasc"; } - else - { - return null; - } + + return null; } public static string GetAudioBitStreamArguments(EncodingJobInfo state, string segmentContainer, string mediaSourceContainer) @@ -1199,10 +1200,8 @@ namespace MediaBrowser.Controller.MediaEncoding { return FormattableString.Invariant($" -rc_mode CBR -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}"); } - else - { - return FormattableString.Invariant($" -rc_mode VBR -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}"); - } + + return FormattableString.Invariant($" -rc_mode VBR -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}"); } return FormattableString.Invariant($" -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}"); @@ -2762,79 +2761,76 @@ namespace MediaBrowser.Controller.MediaEncoding widthParam, heightParam); } - else - { - return GetFixedSwScaleFilter(threedFormat, requestedWidth.Value, requestedHeight.Value); - } + + return GetFixedSwScaleFilter(threedFormat, requestedWidth.Value, requestedHeight.Value); } // If Max dimensions were supplied, for width selects lowest even number between input width and width req size and selects lowest even number from in width*display aspect and requested size - else if (requestedMaxWidth.HasValue && requestedMaxHeight.HasValue) + + if (requestedMaxWidth.HasValue && requestedMaxHeight.HasValue) { var maxWidthParam = requestedMaxWidth.Value.ToString(CultureInfo.InvariantCulture); var maxHeightParam = requestedMaxHeight.Value.ToString(CultureInfo.InvariantCulture); return string.Format( - CultureInfo.InvariantCulture, - "scale=trunc(min(max(iw\\,ih*a)\\,min({0}\\,{1}*a))/{2})*{2}:trunc(min(max(iw/a\\,ih)\\,min({0}/a\\,{1}))/2)*2", - maxWidthParam, - maxHeightParam, - scaleVal); + CultureInfo.InvariantCulture, + "scale=trunc(min(max(iw\\,ih*a)\\,min({0}\\,{1}*a))/{2})*{2}:trunc(min(max(iw/a\\,ih)\\,min({0}/a\\,{1}))/2)*2", + maxWidthParam, + maxHeightParam, + scaleVal); } // If a fixed width was requested - else if (requestedWidth.HasValue) + if (requestedWidth.HasValue) { if (threedFormat.HasValue) { // This method can handle 0 being passed in for the requested height return GetFixedSwScaleFilter(threedFormat, requestedWidth.Value, 0); } - else - { - var widthParam = requestedWidth.Value.ToString(CultureInfo.InvariantCulture); - return string.Format( - CultureInfo.InvariantCulture, - "scale={0}:trunc(ow/a/2)*2", - widthParam); - } + var widthParam = requestedWidth.Value.ToString(CultureInfo.InvariantCulture); + + return string.Format( + CultureInfo.InvariantCulture, + "scale={0}:trunc(ow/a/2)*2", + widthParam); } // If a fixed height was requested - else if (requestedHeight.HasValue) + if (requestedHeight.HasValue) { var heightParam = requestedHeight.Value.ToString(CultureInfo.InvariantCulture); return string.Format( - CultureInfo.InvariantCulture, - "scale=trunc(oh*a/{1})*{1}:{0}", - heightParam, - scaleVal); + CultureInfo.InvariantCulture, + "scale=trunc(oh*a/{1})*{1}:{0}", + heightParam, + scaleVal); } // If a max width was requested - else if (requestedMaxWidth.HasValue) + if (requestedMaxWidth.HasValue) { var maxWidthParam = requestedMaxWidth.Value.ToString(CultureInfo.InvariantCulture); return string.Format( - CultureInfo.InvariantCulture, - "scale=trunc(min(max(iw\\,ih*a)\\,{0})/{1})*{1}:trunc(ow/a/2)*2", - maxWidthParam, - scaleVal); + CultureInfo.InvariantCulture, + "scale=trunc(min(max(iw\\,ih*a)\\,{0})/{1})*{1}:trunc(ow/a/2)*2", + maxWidthParam, + scaleVal); } // If a max height was requested - else if (requestedMaxHeight.HasValue) + if (requestedMaxHeight.HasValue) { var maxHeightParam = requestedMaxHeight.Value.ToString(CultureInfo.InvariantCulture); return string.Format( - CultureInfo.InvariantCulture, - "scale=trunc(oh*a/{1})*{1}:min(max(iw/a\\,ih)\\,{0})", - maxHeightParam, - scaleVal); + CultureInfo.InvariantCulture, + "scale=trunc(oh*a/{1})*{1}:min(max(iw/a\\,ih)\\,{0})", + maxHeightParam, + scaleVal); } return string.Empty; @@ -2908,18 +2904,21 @@ namespace MediaBrowser.Controller.MediaEncoding "yadif_cuda={0}:-1:0", doubleRateDeint ? "1" : "0"); } - else if (hwDeintSuffix.Contains("vaapi", StringComparison.OrdinalIgnoreCase)) + + if (hwDeintSuffix.Contains("vaapi", StringComparison.OrdinalIgnoreCase)) { return string.Format( CultureInfo.InvariantCulture, "deinterlace_vaapi=rate={0}", doubleRateDeint ? "field" : "frame"); } - else if (hwDeintSuffix.Contains("qsv", StringComparison.OrdinalIgnoreCase)) + + if (hwDeintSuffix.Contains("qsv", StringComparison.OrdinalIgnoreCase)) { return "deinterlace_qsv=mode=2"; } - else if (hwDeintSuffix.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase)) + + if (hwDeintSuffix.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase)) { return string.Format( CultureInfo.InvariantCulture, @@ -2950,7 +2949,8 @@ namespace MediaBrowser.Controller.MediaEncoding options.VppTonemappingBrightness, options.VppTonemappingContrast); } - else if (string.Equals(hwTonemapSuffix, "vulkan", StringComparison.OrdinalIgnoreCase)) + + if (string.Equals(hwTonemapSuffix, "vulkan", StringComparison.OrdinalIgnoreCase)) { args = "libplacebo=format={1}:tonemapping={2}:color_primaries=bt709:color_trc=bt709:colorspace=bt709:peak_detect=0:upscaler=none:downscaler=none"; @@ -4826,26 +4826,27 @@ namespace MediaBrowser.Controller.MediaEncoding { return videoStream.BitDepth.Value; } - else if (string.Equals(videoStream.PixelFormat, "yuv420p", StringComparison.OrdinalIgnoreCase) - || string.Equals(videoStream.PixelFormat, "yuvj420p", StringComparison.OrdinalIgnoreCase) - || string.Equals(videoStream.PixelFormat, "yuv444p", StringComparison.OrdinalIgnoreCase)) + + if (string.Equals(videoStream.PixelFormat, "yuv420p", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.PixelFormat, "yuvj420p", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.PixelFormat, "yuv444p", StringComparison.OrdinalIgnoreCase)) { return 8; } - else if (string.Equals(videoStream.PixelFormat, "yuv420p10le", StringComparison.OrdinalIgnoreCase) - || string.Equals(videoStream.PixelFormat, "yuv444p10le", StringComparison.OrdinalIgnoreCase)) + + if (string.Equals(videoStream.PixelFormat, "yuv420p10le", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.PixelFormat, "yuv444p10le", StringComparison.OrdinalIgnoreCase)) { return 10; } - else if (string.Equals(videoStream.PixelFormat, "yuv420p12le", StringComparison.OrdinalIgnoreCase) - || string.Equals(videoStream.PixelFormat, "yuv444p12le", StringComparison.OrdinalIgnoreCase)) + + if (string.Equals(videoStream.PixelFormat, "yuv420p12le", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.PixelFormat, "yuv444p12le", StringComparison.OrdinalIgnoreCase)) { return 12; } - else - { - return 8; - } + + return 8; } return 0; @@ -5077,11 +5078,9 @@ namespace MediaBrowser.Controller.MediaEncoding return " -hwaccel cuda" + (outputHwSurface ? " -hwaccel_output_format cuda" : string.Empty) + (nvdecNoInternalCopy ? " -hwaccel_flags +unsafe_output" : string.Empty) + " -threads 1" + (isAv1 ? " -c:v av1" : string.Empty); } - else - { - // cuvid decoder doesn't have threading issue. - return " -hwaccel cuda" + (outputHwSurface ? " -hwaccel_output_format cuda" : string.Empty); - } + + // cuvid decoder doesn't have threading issue. + return " -hwaccel cuda" + (outputHwSurface ? " -hwaccel_output_format cuda" : string.Empty); } } @@ -5439,7 +5438,8 @@ namespace MediaBrowser.Controller.MediaEncoding // Automatically set thread count return mustSetThreadCount ? Math.Max(Environment.ProcessorCount - 1, 1) : 0; } - else if (threads >= Environment.ProcessorCount) + + if (threads >= Environment.ProcessorCount) { return Environment.ProcessorCount; } diff --git a/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs b/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs index 2164945560..dcc06db1ed 100644 --- a/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs +++ b/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs @@ -533,11 +533,9 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates _logger.LogWarning("Session {SessionId} is seeking to wrong position, correcting.", session.Id); return; } - else - { - // Session is ready. - context.SetBuffering(session, false); - } + + // Session is ready. + context.SetBuffering(session, false); if (!context.IsBuffering()) { diff --git a/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs b/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs index ddbfeb8de8..bdebbbfd49 100644 --- a/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs +++ b/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs @@ -313,17 +313,13 @@ namespace MediaBrowser.Controller.SyncPlay.Queue return true; } - else - { - // Restoring playing item. - SetPlayingItemByPlaylistId(playingItem.PlaylistItemId); - return false; - } - } - else - { + + // Restoring playing item. + SetPlayingItemByPlaylistId(playingItem.PlaylistItemId); return false; } + + return false; } /// <summary> @@ -528,10 +524,8 @@ namespace MediaBrowser.Controller.SyncPlay.Queue { return _shuffledPlaylist; } - else - { - return _sortedPlaylist; - } + + return _sortedPlaylist; } /// <summary> @@ -544,14 +538,13 @@ namespace MediaBrowser.Controller.SyncPlay.Queue { return null; } - else if (ShuffleMode.Equals(GroupShuffleMode.Shuffle)) + + if (ShuffleMode.Equals(GroupShuffleMode.Shuffle)) { return _shuffledPlaylist[PlayingItemIndex]; } - else - { - return _sortedPlaylist[PlayingItemIndex]; - } + + return _sortedPlaylist[PlayingItemIndex]; } } } diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs index ec65dd80f4..989e386a51 100644 --- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs +++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs @@ -231,10 +231,8 @@ namespace MediaBrowser.MediaEncoding.Attachments throw new InvalidOperationException( string.Format(CultureInfo.InvariantCulture, "ffmpeg attachment extraction failed for {0} to {1}", inputPath, outputPath)); } - else - { - _logger.LogInformation("ffmpeg attachment extraction completed for {InputPath} to {OutputPath}", inputPath, outputPath); - } + + _logger.LogInformation("ffmpeg attachment extraction completed for {InputPath} to {OutputPath}", inputPath, outputPath); } private async Task<Stream> GetAttachmentStream( @@ -376,10 +374,8 @@ namespace MediaBrowser.MediaEncoding.Attachments throw new InvalidOperationException( string.Format(CultureInfo.InvariantCulture, "ffmpeg attachment extraction failed for {0} to {1}", inputPath, outputPath)); } - else - { - _logger.LogInformation("ffmpeg attachment extraction completed for {InputPath} to {OutputPath}", inputPath, outputPath); - } + + _logger.LogInformation("ffmpeg attachment extraction completed for {InputPath} to {OutputPath}", inputPath, outputPath); } private string GetAttachmentCachePath(string mediaPath, MediaSourceInfo mediaSource, int attachmentStreamIndex) diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index 53b8a6e678..d3843796f7 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -217,12 +217,14 @@ namespace MediaBrowser.MediaEncoding.Encoder return false; } - else if (version < MinVersion) // Version is below what we recommend + + if (version < MinVersion) // Version is below what we recommend { _logger.LogWarning("FFmpeg validation: The minimum recommended version is {MinVersion}", MinVersion); return false; } - else if (MaxVersion is not null && version > MaxVersion) // Version is above what we recommend + + if (MaxVersion is not null && version > MaxVersion) // Version is above what we recommend { _logger.LogWarning("FFmpeg validation: The maximum recommended version is {MaxVersion}", MaxVersion); return false; diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 3b85988715..794906c3b4 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -624,10 +624,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles throw new FfmpegException( string.Format(CultureInfo.InvariantCulture, "ffmpeg subtitle extraction failed for {0} to {1}", inputPath, outputPath)); } - else - { - _logger.LogInformation("ffmpeg subtitle extraction completed for {InputPath} to {OutputPath}", inputPath, outputPath); - } + + _logger.LogInformation("ffmpeg subtitle extraction completed for {InputPath} to {OutputPath}", inputPath, outputPath); if (string.Equals(outputCodec, "ass", StringComparison.OrdinalIgnoreCase)) { diff --git a/MediaBrowser.Model/Cryptography/PasswordHash.cs b/MediaBrowser.Model/Cryptography/PasswordHash.cs index 80a30684ab..ccb361c132 100644 --- a/MediaBrowser.Model/Cryptography/PasswordHash.cs +++ b/MediaBrowser.Model/Cryptography/PasswordHash.cs @@ -80,7 +80,8 @@ namespace MediaBrowser.Model.Cryptography { throw new FormatException("Hash string must contain a valid id"); } - else if (nextSegment == -1) + + if (nextSegment == -1) { return new PasswordHash(hashString.ToString(), Array.Empty<byte>()); } diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index db892a22c6..df185e40c5 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -1075,31 +1075,38 @@ namespace MediaBrowser.Model.Dlna { return 128000; } - else if (totalBitrate <= 2000000) + + if (totalBitrate <= 2000000) { return 384000; } - else if (totalBitrate <= 3000000) + + if (totalBitrate <= 3000000) { return 448000; } - else if (totalBitrate <= 4000000) + + if (totalBitrate <= 4000000) { return 640000; } - else if (totalBitrate <= 5000000) + + if (totalBitrate <= 5000000) { return 768000; } - else if (totalBitrate <= 10000000) + + if (totalBitrate <= 10000000) { return 1536000; } - else if (totalBitrate <= 15000000) + + if (totalBitrate <= 15000000) { return 2304000; } - else if (totalBitrate <= 20000000) + + if (totalBitrate <= 20000000) { return 3584000; } @@ -1443,7 +1450,8 @@ namespace MediaBrowser.Model.Dlna { return false; } - else if (ContainerProfile.ContainsContainer(normalizedContainers, "mkv") + + if (ContainerProfile.ContainsContainer(normalizedContainers, "mkv") || ContainerProfile.ContainsContainer(normalizedContainers, "matroska")) { return true; diff --git a/MediaBrowser.Model/MediaInfo/AudioCodec.cs b/MediaBrowser.Model/MediaInfo/AudioCodec.cs index 7b83b1b9df..4c22af4498 100644 --- a/MediaBrowser.Model/MediaInfo/AudioCodec.cs +++ b/MediaBrowser.Model/MediaInfo/AudioCodec.cs @@ -17,11 +17,13 @@ namespace MediaBrowser.Model.MediaInfo { return "Dolby Digital"; } - else if (string.Equals(codec, "eac3", StringComparison.OrdinalIgnoreCase)) + + if (string.Equals(codec, "eac3", StringComparison.OrdinalIgnoreCase)) { return "Dolby Digital+"; } - else if (string.Equals(codec, "dca", StringComparison.OrdinalIgnoreCase)) + + if (string.Equals(codec, "dca", StringComparison.OrdinalIgnoreCase)) { return "DTS"; } diff --git a/RSSDP/HttpParserBase.cs b/RSSDP/HttpParserBase.cs index 6b6c13d996..1949a9df33 100644 --- a/RSSDP/HttpParserBase.cs +++ b/RSSDP/HttpParserBase.cs @@ -221,10 +221,8 @@ namespace Rssdp.Infrastructure { return trimmedSegment.Substring(1, trimmedSegment.Length - 2); } - else - { - return trimmedSegment; - } + + return trimmedSegment; } } } diff --git a/RSSDP/SsdpDevice.cs b/RSSDP/SsdpDevice.cs index c826830f1d..3e4261b6a9 100644 --- a/RSSDP/SsdpDevice.cs +++ b/RSSDP/SsdpDevice.cs @@ -171,10 +171,8 @@ namespace Rssdp { return "uuid:" + this.Uuid; } - else - { - return _Udn; - } + + return _Udn; } set diff --git a/RSSDP/SsdpDeviceLocator.cs b/RSSDP/SsdpDeviceLocator.cs index de48bd8798..7afd325819 100644 --- a/RSSDP/SsdpDeviceLocator.cs +++ b/RSSDP/SsdpDeviceLocator.cs @@ -585,10 +585,8 @@ namespace Rssdp.Infrastructure { return OneSecond; } - else - { - return searchWaitTime.Subtract(OneSecond); - } + + return searchWaitTime.Subtract(OneSecond); } private DiscoveredSsdpDevice FindExistingDeviceNotification(IEnumerable<DiscoveredSsdpDevice> devices, string notificationType, string usn) diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs index 0225d4f7db..be66f5947d 100644 --- a/RSSDP/SsdpDevicePublisher.cs +++ b/RSSDP/SsdpDevicePublisher.cs @@ -571,10 +571,8 @@ namespace Rssdp.Infrastructure { return nonzeroCacheLifetimesQuery.Min(); } - else - { - return TimeSpan.Zero; - } + + return TimeSpan.Zero; } private string GetFirstHeaderValue(System.Net.Http.Headers.HttpRequestHeaders httpRequestHeaders, string headerName) diff --git a/src/Jellyfin.Extensions/AlphanumericComparator.cs b/src/Jellyfin.Extensions/AlphanumericComparator.cs index 6e451d40e9..299e2f94ae 100644 --- a/src/Jellyfin.Extensions/AlphanumericComparator.cs +++ b/src/Jellyfin.Extensions/AlphanumericComparator.cs @@ -20,11 +20,13 @@ namespace Jellyfin.Extensions { return 0; } - else if (s1 is null) + + if (s1 is null) { return -1; } - else if (s2 is null) + + if (s2 is null) { return 1; } @@ -37,11 +39,13 @@ namespace Jellyfin.Extensions { return 0; } - else if (len1 == 0) + + if (len1 == 0) { return -1; } - else if (len2 == 0) + + if (len2 == 0) { return 1; } @@ -82,7 +86,8 @@ namespace Jellyfin.Extensions { return -1; } - else if (span1Len > span2Len) + + if (span1Len > span2Len) { return 1; } From d8c5d5522150fbbc0368626b5b52697d40a232b2 Mon Sep 17 00:00:00 2001 From: argalion79 <lotms@mail.ru> Date: Thu, 6 Apr 2023 17:41:39 +0000 Subject: [PATCH 229/858] Translated using Weblate (Russian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ru/ --- Emby.Server.Implementations/Localization/Core/ru.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json index 839bbcb6d3..421513341a 100644 --- a/Emby.Server.Implementations/Localization/Core/ru.json +++ b/Emby.Server.Implementations/Localization/Core/ru.json @@ -42,7 +42,7 @@ "MusicVideos": "Муз. видео", "NameInstallFailed": "Установка {0} неудачна", "NameSeasonNumber": "Сезон {0}", - "NameSeasonUnknown": "Сезон неопознан", + "NameSeasonUnknown": "Сезон не опознан", "NewVersionIsAvailable": "Новая версия Jellyfin Server доступна для загрузки.", "NotificationOptionApplicationUpdateAvailable": "Имеется обновление приложения", "NotificationOptionApplicationUpdateInstalled": "Обновление приложения установлено", @@ -96,7 +96,7 @@ "TaskRefreshChannels": "Обновление каналов", "TaskCleanTranscode": "Очистка каталога перекодировки", "TaskUpdatePlugins": "Обновление плагинов", - "TaskRefreshPeople": "Подновление людей", + "TaskRefreshPeople": "Обновление информации о персонах", "TaskCleanLogs": "Очистка каталога журналов", "TaskRefreshLibrary": "Сканирование медиатеки", "TaskRefreshChapterImages": "Извлечение изображений сцен", From f47de6a9f9cb398fce8584ece5e10c89c59a37b6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 6 Apr 2023 20:00:19 +0000 Subject: [PATCH 230/858] Update github/codeql-action action to v2.2.11 --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index f7fef8e80b..821282e244 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@8c8d71dde4abced210732d8486586914b97752e8 # v2.2.10 + uses: github/codeql-action/init@d186a2a36cc67bfa1b860e6170d37fb9634742c7 # v2.2.11 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@8c8d71dde4abced210732d8486586914b97752e8 # v2.2.10 + uses: github/codeql-action/autobuild@d186a2a36cc67bfa1b860e6170d37fb9634742c7 # v2.2.11 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@8c8d71dde4abced210732d8486586914b97752e8 # v2.2.10 + uses: github/codeql-action/analyze@d186a2a36cc67bfa1b860e6170d37fb9634742c7 # v2.2.11 From 9d37c0feec9187145af8759e83175da0467fa96c Mon Sep 17 00:00:00 2001 From: Brad Beattie <bradbeattie@gmail.com> Date: Fri, 7 Apr 2023 11:55:02 -0700 Subject: [PATCH 231/858] Augment similarity with person matches The code comment says "genres, tags, studios, _person_, year?", but does no matching on common people between films. This PR augments similarity score treating people similar to tags. --- Emby.Server.Implementations/Data/SqliteItemRepository.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index fcff5f98c6..f3fb0e285b 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -2392,6 +2392,7 @@ namespace Emby.Server.Implementations.Data // genres, tags, studios, person, year? builder.Append("+ (Select count(1) * 10 from ItemValues where ItemId=Guid and CleanValue in (select CleanValue from ItemValues where ItemId=@SimilarItemId))"); + builder.Append("+ (Select count(1) * 10 from People where ItemId=Guid and Name in (select Name from People where ItemId=@SimilarItemId))"); if (item is MusicArtist) { From 7dd4201971f1bb19ea380f2ca83aed11206cfe97 Mon Sep 17 00:00:00 2001 From: AmbulantRex <21176662+AmbulantRex@users.noreply.github.com> Date: Sun, 9 Apr 2023 10:53:09 -0600 Subject: [PATCH 232/858] Reconcile pre-packaged meta.json against manifest on install --- .../Plugins/PluginManager.cs | 74 ++++++- .../Updates/InstallationManager.cs | 7 +- MediaBrowser.Common/Plugins/IPluginManager.cs | 2 +- MediaBrowser.Model/Updates/VersionInfo.cs | 6 - src/Jellyfin.Extensions/TypeExtensions.cs | 45 +++++ .../TypeExtensionsTests.cs | 68 +++++++ .../Plugins/PluginManagerTests.cs | 181 +++++++++++++++--- .../Test Data/Updates/manifest-stable.json | 6 +- .../Updates/InstallationManagerTests.cs | 14 -- 9 files changed, 341 insertions(+), 62 deletions(-) create mode 100644 src/Jellyfin.Extensions/TypeExtensions.cs create mode 100644 tests/Jellyfin.Extensions.Tests/TypeExtensionsTests.cs diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs index 0a7c144ed7..c6a7f45464 100644 --- a/Emby.Server.Implementations/Plugins/PluginManager.cs +++ b/Emby.Server.Implementations/Plugins/PluginManager.cs @@ -32,6 +32,8 @@ namespace Emby.Server.Implementations.Plugins /// </summary> public class PluginManager : IPluginManager { + private const string MetafileName = "meta.json"; + private readonly string _pluginsPath; private readonly Version _appVersion; private readonly List<AssemblyLoadContext> _assemblyLoadContexts; @@ -374,7 +376,7 @@ namespace Emby.Server.Implementations.Plugins try { var data = JsonSerializer.Serialize(manifest, _jsonOptions); - File.WriteAllText(Path.Combine(path, "meta.json"), data); + File.WriteAllText(Path.Combine(path, MetafileName), data); return true; } catch (ArgumentException e) @@ -385,7 +387,7 @@ namespace Emby.Server.Implementations.Plugins } /// <inheritdoc/> - public async Task<bool> GenerateManifest(PackageInfo packageInfo, Version version, string path, PluginStatus status) + public async Task<bool> PopulateManifest(PackageInfo packageInfo, Version version, string path, PluginStatus status) { var versionInfo = packageInfo.Versions.First(v => v.Version == version.ToString()); var imagePath = string.Empty; @@ -427,13 +429,75 @@ namespace Emby.Server.Implementations.Plugins Version = versionInfo.Version, Status = status == PluginStatus.Disabled ? PluginStatus.Disabled : PluginStatus.Active, // Keep disabled state. AutoUpdate = true, - ImagePath = imagePath, - Assemblies = versionInfo.Assemblies + ImagePath = imagePath }; + var metafile = Path.Combine(Path.Combine(path, MetafileName)); + if (File.Exists(metafile)) + { + var data = File.ReadAllBytes(metafile); + var localManifest = JsonSerializer.Deserialize<PluginManifest>(data, _jsonOptions) ?? new PluginManifest(); + + // Plugin installation is the typical cause for populating a manifest. Activate. + localManifest.Status = status == PluginStatus.Disabled ? PluginStatus.Disabled : PluginStatus.Active; + + if (!Equals(localManifest.Id, manifest.Id)) + { + _logger.LogError("The manifest ID {LocalUUID} did not match the package info ID {PackageUUID}.", localManifest.Id, manifest.Id); + localManifest.Status = PluginStatus.Malfunctioned; + } + + if (localManifest.Version != manifest.Version) + { + _logger.LogWarning("The version of the local manifest was {LocalVersion}, but {PackageVersion} was expected. The value will be replaced.", localManifest.Version, manifest.Version); + + // Correct the local version. + localManifest.Version = manifest.Version; + } + + // Reconcile missing data against repository manifest. + ReconcileManifest(localManifest, manifest); + + manifest = localManifest; + } + else + { + _logger.LogInformation("No local manifest exists for plugin {Plugin}. Populating from repository manifest.", manifest.Name); + } + return SaveManifest(manifest, path); } + /// <summary> + /// Resolve the target plugin manifest against the source. Values are mapped onto the + /// target only if they are default values or empty strings. ID and status fields are ignored. + /// </summary> + /// <param name="baseManifest">The base <see cref="PluginManifest"/> to be reconciled.</param> + /// <param name="projector">The <see cref="PluginManifest"/> to reconcile against.</param> + private void ReconcileManifest(PluginManifest baseManifest, PluginManifest projector) + { + var ignoredFields = new string[] + { + nameof(baseManifest.Id), + nameof(baseManifest.Status) + }; + + foreach (var property in baseManifest.GetType().GetProperties()) + { + var localValue = property.GetValue(baseManifest); + + if (property.PropertyType == typeof(bool) || ignoredFields.Any(s => Equals(s, property.Name))) + { + continue; + } + + if (property.PropertyType.IsNullOrDefault(localValue) || (property.PropertyType == typeof(string) && (string)localValue! == string.Empty)) + { + property.SetValue(baseManifest, property.GetValue(projector)); + } + } + } + /// <summary> /// Changes a plugin's load status. /// </summary> @@ -598,7 +662,7 @@ namespace Emby.Server.Implementations.Plugins { Version? version; PluginManifest? manifest = null; - var metafile = Path.Combine(dir, "meta.json"); + var metafile = Path.Combine(dir, MetafileName); if (File.Exists(metafile)) { // Only path where this stays null is when File.ReadAllBytes throws an IOException diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index 5e897833e0..6c198b6f99 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -183,7 +183,7 @@ namespace Emby.Server.Implementations.Updates var plugin = _pluginManager.GetPlugin(package.Id, version.VersionNumber); if (plugin is not null) { - await _pluginManager.GenerateManifest(package, version.VersionNumber, plugin.Path, plugin.Manifest.Status).ConfigureAwait(false); + await _pluginManager.PopulateManifest(package, version.VersionNumber, plugin.Path, plugin.Manifest.Status).ConfigureAwait(false); } // Remove versions with a target ABI greater then the current application version. @@ -555,7 +555,10 @@ namespace Emby.Server.Implementations.Updates stream.Position = 0; using var reader = new ZipArchive(stream); reader.ExtractToDirectory(targetDir, true); - await _pluginManager.GenerateManifest(package.PackageInfo, package.Version, targetDir, status).ConfigureAwait(false); + + // Ensure we create one or populate existing ones with missing data. + await _pluginManager.PopulateManifest(package.PackageInfo, package.Version, targetDir, status); + _pluginManager.ImportPluginFrom(targetDir); } diff --git a/MediaBrowser.Common/Plugins/IPluginManager.cs b/MediaBrowser.Common/Plugins/IPluginManager.cs index fa92d383a2..1d73de3c95 100644 --- a/MediaBrowser.Common/Plugins/IPluginManager.cs +++ b/MediaBrowser.Common/Plugins/IPluginManager.cs @@ -57,7 +57,7 @@ namespace MediaBrowser.Common.Plugins /// <param name="path">The path where to save the manifest.</param> /// <param name="status">Initial status of the plugin.</param> /// <returns>True if successful.</returns> - Task<bool> GenerateManifest(PackageInfo packageInfo, Version version, string path, PluginStatus status); + Task<bool> PopulateManifest(PackageInfo packageInfo, Version version, string path, PluginStatus status); /// <summary> /// Imports plugin details from a folder. diff --git a/MediaBrowser.Model/Updates/VersionInfo.cs b/MediaBrowser.Model/Updates/VersionInfo.cs index 8f76806450..53e1d29b01 100644 --- a/MediaBrowser.Model/Updates/VersionInfo.cs +++ b/MediaBrowser.Model/Updates/VersionInfo.cs @@ -75,11 +75,5 @@ namespace MediaBrowser.Model.Updates /// </summary> [JsonPropertyName("repositoryUrl")] public string RepositoryUrl { get; set; } = string.Empty; - - /// <summary> - /// Gets or sets the assemblies whitelist for this version. - /// </summary> - [JsonPropertyName("assemblies")] - public IReadOnlyList<string> Assemblies { get; set; } = Array.Empty<string>(); } } diff --git a/src/Jellyfin.Extensions/TypeExtensions.cs b/src/Jellyfin.Extensions/TypeExtensions.cs new file mode 100644 index 0000000000..5b1111d594 --- /dev/null +++ b/src/Jellyfin.Extensions/TypeExtensions.cs @@ -0,0 +1,45 @@ +using System; +using System.Globalization; + +namespace Jellyfin.Extensions; + +/// <summary> +/// Provides extensions methods for <see cref="Type" />. +/// </summary> +public static class TypeExtensions +{ + /// <summary> + /// Checks if the supplied value is the default or null value for that type. + /// </summary> + /// <typeparam name="T">The type of the value to compare.</typeparam> + /// <param name="type">The type.</param> + /// <param name="value">The value to check.</param> + /// <returns><see langword="true"/> if the value is the default for the type. Otherwise, <see langword="false"/>.</returns> + public static bool IsNullOrDefault<T>(this Type type, T value) + { + if (value is null) + { + return true; + } + + object? tmp = value; + object? defaultValue = type.IsValueType ? Activator.CreateInstance(type) : null; + if (type.IsAssignableTo(typeof(IConvertible))) + { + tmp = Convert.ChangeType(value, type, CultureInfo.InvariantCulture); + } + + return Equals(tmp, defaultValue); + } + + /// <summary> + /// Checks if the object is currently a default or null value. Boxed types will be unboxed prior to comparison. + /// </summary> + /// <param name="obj">The object to check.</param> + /// <returns><see langword="true"/> if the value is the default for the type. Otherwise, <see langword="false"/>.</returns> + public static bool IsNullOrDefault(this object? obj) + { + // Unbox the type and check. + return obj?.GetType().IsNullOrDefault(obj) ?? true; + } +} diff --git a/tests/Jellyfin.Extensions.Tests/TypeExtensionsTests.cs b/tests/Jellyfin.Extensions.Tests/TypeExtensionsTests.cs new file mode 100644 index 0000000000..747913fa1a --- /dev/null +++ b/tests/Jellyfin.Extensions.Tests/TypeExtensionsTests.cs @@ -0,0 +1,68 @@ +using System; +using Xunit; + +namespace Jellyfin.Extensions.Tests +{ + public class TypeExtensionsTests + { + [Theory] + [InlineData(typeof(byte), byte.MaxValue, false)] + [InlineData(typeof(short), short.MinValue, false)] + [InlineData(typeof(ushort), ushort.MaxValue, false)] + [InlineData(typeof(int), int.MinValue, false)] + [InlineData(typeof(uint), uint.MaxValue, false)] + [InlineData(typeof(long), long.MinValue, false)] + [InlineData(typeof(ulong), ulong.MaxValue, false)] + [InlineData(typeof(decimal), -1.0, false)] + [InlineData(typeof(bool), true, false)] + [InlineData(typeof(char), 'a', false)] + [InlineData(typeof(string), "", false)] + [InlineData(typeof(object), 1, false)] + [InlineData(typeof(byte), 0, true)] + [InlineData(typeof(short), 0, true)] + [InlineData(typeof(ushort), 0, true)] + [InlineData(typeof(int), 0, true)] + [InlineData(typeof(uint), 0, true)] + [InlineData(typeof(long), 0, true)] + [InlineData(typeof(ulong), 0, true)] + [InlineData(typeof(decimal), 0, true)] + [InlineData(typeof(bool), false, true)] + [InlineData(typeof(char), '\x0000', true)] + [InlineData(typeof(string), null, true)] + [InlineData(typeof(object), null, true)] + [InlineData(typeof(PhonyClass), null, true)] + [InlineData(typeof(DateTime), null, true)] // Special case handled within the test. + [InlineData(typeof(DateTime), null, false)] // Special case handled within the test. + [InlineData(typeof(byte?), null, true)] + [InlineData(typeof(short?), null, true)] + [InlineData(typeof(ushort?), null, true)] + [InlineData(typeof(int?), null, true)] + [InlineData(typeof(uint?), null, true)] + [InlineData(typeof(long?), null, true)] + [InlineData(typeof(ulong?), null, true)] + [InlineData(typeof(decimal?), null, true)] + [InlineData(typeof(bool?), null, true)] + [InlineData(typeof(char?), null, true)] + public void IsNullOrDefault_Matches_Expected(Type type, object? value, bool expectedResult) + { + if (type == typeof(DateTime)) + { + if (expectedResult) + { + value = default(DateTime); + } + else + { + value = DateTime.Now; + } + } + + Assert.Equal(expectedResult, type.IsNullOrDefault(value)); + Assert.Equal(expectedResult, value.IsNullOrDefault()); + } + + private class PhonyClass + { + } + } +} diff --git a/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs index d9fdc96f55..204d144214 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs @@ -1,12 +1,16 @@ using System; +using System.Globalization; using System.IO; using System.Text.Json; +using System.Threading.Tasks; +using AutoFixture; using Emby.Server.Implementations.Library; using Emby.Server.Implementations.Plugins; using Jellyfin.Extensions.Json; using Jellyfin.Extensions.Json.Converters; using MediaBrowser.Common.Plugins; using MediaBrowser.Model.Plugins; +using MediaBrowser.Model.Updates; using Microsoft.Extensions.Logging.Abstractions; using Xunit; @@ -16,6 +20,21 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins { private static readonly string _testPathRoot = Path.Combine(Path.GetTempPath(), "jellyfin-test-data"); + private string _tempPath = string.Empty; + + private string _pluginPath = string.Empty; + + private JsonSerializerOptions _options; + + public PluginManagerTests() + { + (_tempPath, _pluginPath) = GetTestPaths("plugin-" + Path.GetRandomFileName()); + + Directory.CreateDirectory(_pluginPath); + + _options = GetTestSerializerOptions(); + } + [Fact] public void SaveManifest_RoundTrip_Success() { @@ -25,12 +44,9 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins Version = "1.0" }; - var tempPath = Path.Combine(_testPathRoot, "manifest-" + Path.GetRandomFileName()); - Directory.CreateDirectory(tempPath); + Assert.True(pluginManager.SaveManifest(manifest, _pluginPath)); - Assert.True(pluginManager.SaveManifest(manifest, tempPath)); - - var res = pluginManager.LoadManifest(tempPath); + var res = pluginManager.LoadManifest(_pluginPath); Assert.Equal(manifest.Category, res.Manifest.Category); Assert.Equal(manifest.Changelog, res.Manifest.Changelog); @@ -66,28 +82,25 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins }; var filename = Path.GetFileName(dllFile)!; - var (tempPath, pluginPath) = GetTestPaths("safe"); + var dllPath = Path.GetDirectoryName(Path.Combine(_pluginPath, dllFile))!; - Directory.CreateDirectory(Path.Combine(pluginPath, dllFile.Replace(filename, string.Empty, StringComparison.OrdinalIgnoreCase))); - File.Create(Path.Combine(pluginPath, dllFile)); + Directory.CreateDirectory(dllPath); + File.Create(Path.Combine(dllPath, filename)); + var metafilePath = Path.Combine(_pluginPath, "meta.json"); - var options = GetTestSerializerOptions(); - var data = JsonSerializer.Serialize(manifest, options); - var metafilePath = Path.Combine(tempPath, "safe", "meta.json"); + File.WriteAllText(metafilePath, JsonSerializer.Serialize(manifest, _options)); - File.WriteAllText(metafilePath, data); + var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, _tempPath, new Version(1, 0)); - var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, tempPath, new Version(1, 0)); + var res = JsonSerializer.Deserialize<PluginManifest>(File.ReadAllText(metafilePath), _options); - var res = JsonSerializer.Deserialize<PluginManifest>(File.ReadAllText(metafilePath), options); - - var expectedFullPath = Path.Combine(pluginPath, dllFile).Canonicalize(); + var expectedFullPath = Path.Combine(_pluginPath, dllFile).Canonicalize(); Assert.NotNull(res); Assert.NotEmpty(pluginManager.Plugins); Assert.Equal(PluginStatus.Active, res!.Status); Assert.Equal(expectedFullPath, pluginManager.Plugins[0].DllFiles[0]); - Assert.StartsWith(Path.Combine(tempPath, "safe"), expectedFullPath, StringComparison.InvariantCulture); + Assert.StartsWith(_pluginPath, expectedFullPath, StringComparison.InvariantCulture); } /// <summary> @@ -109,7 +122,6 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins [InlineData("..\\.\\..\\.\\..\\some.dll")] // Windows traversal with current and parent [InlineData("\\\\network\\resource.dll")] // UNC Path [InlineData("https://jellyfin.org/some.dll")] // URL - [InlineData("....//....//some.dll")] // Path replacement risk if a single "../" replacement occurs. [InlineData("~/some.dll")] // Tilde poses a shell expansion risk, but is a valid path character. public void Constructor_DiscoversUnsafePluginAssembly_Status_Malfunctioned(string unsafePath) { @@ -120,10 +132,7 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins Assemblies = new string[] { unsafePath } }; - var (tempPath, pluginPath) = GetTestPaths("unsafe"); - - Directory.CreateDirectory(pluginPath); - + // Only create very specific files. Otherwise the test will be exploiting path traversal. var files = new string[] { "../other.dll", @@ -132,24 +141,136 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins foreach (var file in files) { - File.Create(Path.Combine(pluginPath, file)); + File.Create(Path.Combine(_pluginPath, file)); } - var options = GetTestSerializerOptions(); - var data = JsonSerializer.Serialize(manifest, options); - var metafilePath = Path.Combine(tempPath, "unsafe", "meta.json"); + var metafilePath = Path.Combine(_pluginPath, "meta.json"); - File.WriteAllText(metafilePath, data); + File.WriteAllText(metafilePath, JsonSerializer.Serialize(manifest, _options)); - var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, tempPath, new Version(1, 0)); + var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, _tempPath, new Version(1, 0)); - var res = JsonSerializer.Deserialize<PluginManifest>(File.ReadAllText(metafilePath), options); + var res = JsonSerializer.Deserialize<PluginManifest>(File.ReadAllText(metafilePath), _options); Assert.NotNull(res); Assert.Empty(pluginManager.Plugins); Assert.Equal(PluginStatus.Malfunctioned, res!.Status); } + [Fact] + public async Task PopulateManifest_ExistingMetafilePlugin_PopulatesMissingFields() + { + var packageInfo = GenerateTestPackage(); + + // Partial plugin without a name, but matching version and package ID + var partial = new PluginManifest + { + Id = packageInfo.Id, + AutoUpdate = false, // Turn off AutoUpdate + Status = PluginStatus.Restart, + Version = new Version(1, 0, 0).ToString(), + Assemblies = new[] { "Jellyfin.Test.dll" } + }; + + var metafilePath = Path.Combine(_pluginPath, "meta.json"); + File.WriteAllText(metafilePath, JsonSerializer.Serialize(partial, _options)); + + var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, _tempPath, new Version(1, 0)); + + await pluginManager.PopulateManifest(packageInfo, new Version(1, 0), _pluginPath, PluginStatus.Active); + + var resultBytes = File.ReadAllBytes(metafilePath); + var result = JsonSerializer.Deserialize<PluginManifest>(resultBytes, _options); + + Assert.NotNull(result); + Assert.Equal(packageInfo.Name, result.Name); + Assert.Equal(packageInfo.Owner, result.Owner); + Assert.Equal(PluginStatus.Active, result.Status); + Assert.NotEmpty(result.Assemblies); + Assert.False(result.AutoUpdate); + + // Preserved + Assert.Equal(packageInfo.Category, result.Category); + Assert.Equal(packageInfo.Description, result.Description); + Assert.Equal(packageInfo.Id, result.Id); + Assert.Equal(packageInfo.Overview, result.Overview); + Assert.Equal(partial.Assemblies[0], result.Assemblies[0]); + Assert.Equal(packageInfo.Versions[0].TargetAbi, result.TargetAbi); + Assert.Equal(DateTime.Parse(packageInfo.Versions[0].Timestamp!, CultureInfo.InvariantCulture), result.Timestamp); + Assert.Equal(packageInfo.Versions[0].Changelog, result.Changelog); + Assert.Equal(packageInfo.Versions[0].Version, result.Version); + } + + [Fact] + public async Task PopulateManifest_ExistingMetafileMismatchedIds_Status_Malfunctioned() + { + var packageInfo = GenerateTestPackage(); + + // Partial plugin without a name, but matching version and package ID + var partial = new PluginManifest + { + Id = Guid.NewGuid(), + Version = new Version(1, 0, 0).ToString() + }; + + var metafilePath = Path.Combine(_pluginPath, "meta.json"); + File.WriteAllText(metafilePath, JsonSerializer.Serialize(partial, _options)); + + var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, _tempPath, new Version(1, 0)); + + await pluginManager.PopulateManifest(packageInfo, new Version(1, 0), _pluginPath, PluginStatus.Active); + + var resultBytes = File.ReadAllBytes(metafilePath); + var result = JsonSerializer.Deserialize<PluginManifest>(resultBytes, _options); + + Assert.NotNull(result); + Assert.Equal(packageInfo.Name, result.Name); + Assert.Equal(PluginStatus.Malfunctioned, result.Status); + } + + [Fact] + public async Task PopulateManifest_ExistingMetafileMismatchedVersions_Updates_Version() + { + var packageInfo = GenerateTestPackage(); + + var partial = new PluginManifest + { + Id = packageInfo.Id, + Version = new Version(2, 0, 0).ToString() + }; + + var metafilePath = Path.Combine(_pluginPath, "meta.json"); + File.WriteAllText(metafilePath, JsonSerializer.Serialize(partial, _options)); + + var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, _tempPath, new Version(1, 0)); + + await pluginManager.PopulateManifest(packageInfo, new Version(1, 0), _pluginPath, PluginStatus.Active); + + var resultBytes = File.ReadAllBytes(metafilePath); + var result = JsonSerializer.Deserialize<PluginManifest>(resultBytes, _options); + + Assert.NotNull(result); + Assert.Equal(packageInfo.Name, result.Name); + Assert.Equal(PluginStatus.Active, result.Status); + Assert.Equal(packageInfo.Versions[0].Version, result.Version); + } + + private PackageInfo GenerateTestPackage() + { + var fixture = new Fixture(); + fixture.Customize<PackageInfo>(c => c.Without(x => x.Versions).Without(x => x.ImageUrl)); + fixture.Customize<VersionInfo>(c => c.Without(x => x.Version).Without(x => x.Timestamp)); + + var versionInfo = fixture.Create<VersionInfo>(); + versionInfo.Version = new Version(1, 0).ToString(); + versionInfo.Timestamp = DateTime.UtcNow.ToString(CultureInfo.InvariantCulture); + + var packageInfo = fixture.Create<PackageInfo>(); + packageInfo.Versions = new[] { versionInfo }; + + return packageInfo; + } + private JsonSerializerOptions GetTestSerializerOptions() { var options = new JsonSerializerOptions(JsonDefaults.Options) @@ -171,7 +292,7 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins private (string TempPath, string PluginPath) GetTestPaths(string pluginFolderName) { - var tempPath = Path.Combine(_testPathRoot, "plugins-" + Path.GetRandomFileName()); + var tempPath = Path.Combine(_testPathRoot, "plugin-manager" + Path.GetRandomFileName()); var pluginPath = Path.Combine(tempPath, pluginFolderName); return (tempPath, pluginPath); diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/manifest-stable.json b/tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/manifest-stable.json index 3aec299587..57367ce88c 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/manifest-stable.json +++ b/tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/manifest-stable.json @@ -13,8 +13,7 @@ "targetAbi": "10.6.0.0", "sourceUrl": "https://repo.jellyfin.org/releases/plugin/anime/anime_10.0.0.0.zip", "checksum": "93e969adeba1050423fc8817ed3c36f8", - "timestamp": "2020-08-17T01:41:13Z", - "assemblies": [ "Jellyfin.Plugin.Anime.dll" ] + "timestamp": "2020-08-17T01:41:13Z" }, { "version": "9.0.0.0", @@ -22,8 +21,7 @@ "targetAbi": "10.6.0.0", "sourceUrl": "https://repo.jellyfin.org/releases/plugin/anime/anime_9.0.0.0.zip", "checksum": "9b1cebff835813e15f414f44b40c41c8", - "timestamp": "2020-07-20T01:30:16Z", - "assemblies": [ "Jellyfin.Plugin.Anime.dll" ] + "timestamp": "2020-07-20T01:30:16Z" } ] }, diff --git a/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs index 70d03f8c4a..7abd2e685f 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs @@ -106,19 +106,5 @@ namespace Jellyfin.Server.Implementations.Tests.Updates var ex = await Record.ExceptionAsync(() => _installationManager.InstallPackage(packageInfo, CancellationToken.None)).ConfigureAwait(false); Assert.Null(ex); } - - [Fact] - public async Task InstallPackage_WithAssemblies_Success() - { - PackageInfo[] packages = await _installationManager.GetPackages( - "Jellyfin Stable", - "https://repo.jellyfin.org/releases/plugin/manifest-stable.json", - false); - - packages = _installationManager.FilterPackages(packages, "Anime").ToArray(); - Assert.Single(packages); - Assert.NotEmpty(packages[0].Versions[0].Assemblies); - Assert.Equal("Jellyfin.Plugin.Anime.dll", packages[0].Versions[0].Assemblies[0]); - } } } From 890fe183cf2f86e899671e78dcc25ef3eb9cc7e7 Mon Sep 17 00:00:00 2001 From: Stepan Goremykin <goremukin@gmail.com> Date: Mon, 10 Apr 2023 01:44:56 +0200 Subject: [PATCH 233/858] Use default instead of zero for TranscodeReason --- tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs index a1c73d57e0..c30dad6f90 100644 --- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs +++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs @@ -162,7 +162,7 @@ namespace Jellyfin.Model.Tests [InlineData("Tizen4-4K-5.1", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen4-4K-5.1", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen4-4K-5.1", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] - public async Task BuildVideoItemSimple(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = 0, string transcodeMode = "DirectStream", string transcodeProtocol = "") + public async Task BuildVideoItemSimple(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = default, string transcodeMode = "DirectStream", string transcodeProtocol = "") { var options = await GetMediaOptions(deviceName, mediaSource); BuildVideoItemSimpleTest(options, playMethod, why, transcodeMode, transcodeProtocol); @@ -260,7 +260,7 @@ namespace Jellyfin.Model.Tests [InlineData("Tizen4-4K-5.1", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen4-4K-5.1", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen4-4K-5.1", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] - public async Task BuildVideoItemWithFirstExplicitStream(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = 0, string transcodeMode = "DirectStream", string transcodeProtocol = "") + public async Task BuildVideoItemWithFirstExplicitStream(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = default, string transcodeMode = "DirectStream", string transcodeProtocol = "") { var options = await GetMediaOptions(deviceName, mediaSource); options.AudioStreamIndex = 1; @@ -296,7 +296,7 @@ namespace Jellyfin.Model.Tests // Tizen 4 4K 5.1 [InlineData("Tizen4-4K-5.1", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] [InlineData("Tizen4-4K-5.1", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] - public async Task BuildVideoItemWithDirectPlayExplicitStreams(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = 0, string transcodeMode = "DirectStream", string transcodeProtocol = "") + public async Task BuildVideoItemWithDirectPlayExplicitStreams(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = default, string transcodeMode = "DirectStream", string transcodeProtocol = "") { var options = await GetMediaOptions(deviceName, mediaSource); var streamCount = options.MediaSources[0].MediaStreams.Count; From c92ad0dc4bf3da2d6eeb87a47d3322bba2de1503 Mon Sep 17 00:00:00 2001 From: felix920506 <felix920506@gmail.com> Date: Sun, 9 Apr 2023 02:07:39 +0000 Subject: [PATCH 234/858] Translated using Weblate (Chinese (Traditional)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant/ --- Emby.Server.Implementations/Localization/Core/zh-TW.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json index 4949c5ab6d..36f4df93d1 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-TW.json +++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json @@ -91,14 +91,14 @@ "HeaderRecordingGroups": "錄製組", "Inherit": "繼承", "SubtitleDownloadFailureFromForItem": "無法為 {1} 從 {0} 下載字幕", - "TaskDownloadMissingSubtitlesDescription": "透過中繼資料從網路上搜尋遺失的字幕。", + "TaskDownloadMissingSubtitlesDescription": "透過媒體資訊從網路上搜尋遺失的字幕。", "TaskDownloadMissingSubtitles": "下載遺失的字幕", "TaskRefreshChannels": "重新整理頻道", "TaskUpdatePlugins": "更新附加元件", "TaskRefreshPeople": "更新人物", "TaskCleanLogsDescription": "刪除超過 {0} 天的日誌文件。", "TaskCleanLogs": "清空日誌資料夾", - "TaskRefreshLibraryDescription": "重新掃描媒體庫的新檔案並更新中繼資料。", + "TaskRefreshLibraryDescription": "重新掃描媒體庫的新檔案並更新媒體資訊。", "TaskRefreshLibrary": "重新掃描媒體庫", "TaskRefreshChapterImages": "擷取章節圖片", "TaskCleanCacheDescription": "刪除系統已不需要的快取。", @@ -108,7 +108,7 @@ "TaskCleanTranscodeDescription": "刪除超過一天的轉碼檔案。", "TaskCleanTranscode": "清除轉碼資料夾", "TaskUpdatePluginsDescription": "為已設置為自動更新的附加元件下載並安裝更新。", - "TaskRefreshPeopleDescription": "更新媒體庫中演員和導演的中繼資料。", + "TaskRefreshPeopleDescription": "更新媒體庫中演員和導演的資訊。", "TaskRefreshChapterImagesDescription": "為有章節的影片建立縮圖。", "TasksChannelsCategory": "網路頻道", "TasksApplicationCategory": "應用程式", From 3c22d5c9705921672a932192d016933ef5900001 Mon Sep 17 00:00:00 2001 From: JPVenson <JPVenson@users.noreply.github.com> Date: Mon, 10 Apr 2023 22:38:07 +0300 Subject: [PATCH 235/858] =?UTF-8?q?#7626=20Added=20handling=20for=20common?= =?UTF-8?q?=20FormatExceptions=20with=20Skia=20loading=20sv=E2=80=A6=20(#9?= =?UTF-8?q?581)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Shadowghost <Shadowghost@users.noreply.github.com> --- .../Library/LibraryManager.cs | 2 +- src/Jellyfin.Drawing.Skia/SkiaEncoder.cs | 18 ++++++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 8883aff0b0..75a1a5a4d8 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -1885,7 +1885,7 @@ namespace Emby.Server.Implementations.Library catch (Exception ex) { _logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path); - size = new ImageDimensions(0, 0); + size = default; image.Width = 0; image.Height = 0; } diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 6da77ad959..2d980db181 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -120,8 +120,18 @@ public class SkiaEncoder : IImageEncoder if (extension.Equals(".svg", StringComparison.OrdinalIgnoreCase)) { var svg = new SKSvg(); - svg.Load(path); - return new ImageDimensions(Convert.ToInt32(svg.Picture.CullRect.Width), Convert.ToInt32(svg.Picture.CullRect.Height)); + try + { + svg.Load(path); + return new ImageDimensions(Convert.ToInt32(svg.Picture.CullRect.Width), Convert.ToInt32(svg.Picture.CullRect.Height)); + } + catch (FormatException skiaColorException) + { + // This exception is known to be thrown on vector images that define custom styles + // Skia SVG is not able to handle that and as the repository is quite stale and has not received updates we just catch them + _logger.LogDebug(skiaColorException, "There was a issue loading the requested svg file"); + return default; + } } using var codec = SKCodec.Create(path, out SKCodecResult result); @@ -132,10 +142,10 @@ public class SkiaEncoder : IImageEncoder return new ImageDimensions(info.Width, info.Height); case SKCodecResult.Unimplemented: _logger.LogDebug("Image format not supported: {FilePath}", path); - return new ImageDimensions(0, 0); + return default; default: _logger.LogError("Unable to determine image dimensions for {FilePath}: {SkCodecResult}", path, result); - return new ImageDimensions(0, 0); + return default; } } From 7432f9fab2ed98c2b5ce3fe6d076e3356abbe3e3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 11 Apr 2023 12:57:43 -0600 Subject: [PATCH 236/858] Update dotnet monorepo to v7.0.5 (#9629) * Update dotnet monorepo to v7.0.5 * Update docker sdk --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Cody Robibero <cody@robibe.ro> --- Directory.Packages.props | 16 ++++++++-------- deployment/Dockerfile.centos.amd64 | 2 +- deployment/Dockerfile.fedora.amd64 | 2 +- deployment/Dockerfile.ubuntu.amd64 | 2 +- deployment/Dockerfile.ubuntu.arm64 | 2 +- deployment/Dockerfile.ubuntu.armhf | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index f37eaa11e7..55948cb4a4 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -23,13 +23,13 @@ <PackageVersion Include="libse" Version="3.6.11" /> <PackageVersion Include="LrcParser" Version="2023.308.0" /> <PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" /> - <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.4" /> - <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.4" /> + <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.5" /> + <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.5" /> <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.4" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.4" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.4" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.4" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.5" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.5" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.5" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.5" /> <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" /> @@ -38,8 +38,8 @@ <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" /> - <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.4" /> - <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.4" /> + <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.5" /> + <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.5" /> <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Http" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" /> diff --git a/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64 index 36435194ff..16bb99a3d0 100644 --- a/deployment/Dockerfile.centos.amd64 +++ b/deployment/Dockerfile.centos.amd64 @@ -13,7 +13,7 @@ RUN yum update -yq \ && yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget # Install DotNET SDK -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/bda88810-e1a6-4cf0-8139-7fd7fe7b2c7a/7a9ffa3e12e5f1c3d8b640e326c1eb14/dotnet-sdk-7.0.202-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ebfd0bf8-79bd-480a-9e81-0b217463738d/9adc6bf0614ce02670101e278a2d8555/dotnet-sdk-7.0.203-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.fedora.amd64 b/deployment/Dockerfile.fedora.amd64 index c8943a3993..275b379b3d 100644 --- a/deployment/Dockerfile.fedora.amd64 +++ b/deployment/Dockerfile.fedora.amd64 @@ -12,7 +12,7 @@ RUN dnf update -yq \ && dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd wget make # Install DotNET SDK -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/bda88810-e1a6-4cf0-8139-7fd7fe7b2c7a/7a9ffa3e12e5f1c3d8b640e326c1eb14/dotnet-sdk-7.0.202-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ebfd0bf8-79bd-480a-9e81-0b217463738d/9adc6bf0614ce02670101e278a2d8555/dotnet-sdk-7.0.203-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64 index 7b9a3de4e3..b440a21421 100644 --- a/deployment/Dockerfile.ubuntu.amd64 +++ b/deployment/Dockerfile.ubuntu.amd64 @@ -17,7 +17,7 @@ RUN apt-get update -yqq \ libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/bda88810-e1a6-4cf0-8139-7fd7fe7b2c7a/7a9ffa3e12e5f1c3d8b640e326c1eb14/dotnet-sdk-7.0.202-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ebfd0bf8-79bd-480a-9e81-0b217463738d/9adc6bf0614ce02670101e278a2d8555/dotnet-sdk-7.0.203-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.arm64 b/deployment/Dockerfile.ubuntu.arm64 index 32695e3f12..f195d70045 100644 --- a/deployment/Dockerfile.ubuntu.arm64 +++ b/deployment/Dockerfile.ubuntu.arm64 @@ -16,7 +16,7 @@ RUN apt-get update -yqq \ mmv build-essential lsb-release # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/bda88810-e1a6-4cf0-8139-7fd7fe7b2c7a/7a9ffa3e12e5f1c3d8b640e326c1eb14/dotnet-sdk-7.0.202-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ebfd0bf8-79bd-480a-9e81-0b217463738d/9adc6bf0614ce02670101e278a2d8555/dotnet-sdk-7.0.203-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.armhf b/deployment/Dockerfile.ubuntu.armhf index 8ffbeafade..0fb59d5ca1 100644 --- a/deployment/Dockerfile.ubuntu.armhf +++ b/deployment/Dockerfile.ubuntu.armhf @@ -16,7 +16,7 @@ RUN apt-get update -yqq \ mmv build-essential lsb-release # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/bda88810-e1a6-4cf0-8139-7fd7fe7b2c7a/7a9ffa3e12e5f1c3d8b640e326c1eb14/dotnet-sdk-7.0.202-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ebfd0bf8-79bd-480a-9e81-0b217463738d/9adc6bf0614ce02670101e278a2d8555/dotnet-sdk-7.0.203-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet From 486a4f14964525dae2b0747f91b2d88873c83328 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 12 Apr 2023 18:36:52 +0000 Subject: [PATCH 237/858] Update actions/checkout action to v3.5.1 --- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/commands.yml | 4 ++-- .github/workflows/openapi.yml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 821282e244..044d02e23a 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0 + uses: actions/checkout@83b7061638ee4956cf7545a6f7efe594e5ad0247 # v3.5.1 - name: Setup .NET uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # v3.0.3 with: diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index 589383e830..cee70ad580 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -24,7 +24,7 @@ jobs: reactions: '+1' - name: Checkout the latest code - uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0 + uses: actions/checkout@83b7061638ee4956cf7545a6f7efe594e5ad0247 # v3.5.1 with: token: ${{ secrets.JF_BOT_TOKEN }} fetch-depth: 0 @@ -51,7 +51,7 @@ jobs: reactions: eyes - name: Checkout the latest code - uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0 + uses: actions/checkout@83b7061638ee4956cf7545a6f7efe594e5ad0247 # v3.5.1 with: token: ${{ secrets.JF_BOT_TOKEN }} fetch-depth: 0 diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index 2ce47555b2..2192af2760 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -14,7 +14,7 @@ jobs: permissions: read-all steps: - name: Checkout repository - uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0 + uses: actions/checkout@83b7061638ee4956cf7545a6f7efe594e5ad0247 # v3.5.1 with: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} @@ -39,7 +39,7 @@ jobs: permissions: read-all steps: - name: Checkout repository - uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0 + uses: actions/checkout@83b7061638ee4956cf7545a6f7efe594e5ad0247 # v3.5.1 with: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} From b69abd4d7dfdbdaa62b5c7e12aa36bfdc9e15774 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Thu, 13 Apr 2023 01:50:57 +0200 Subject: [PATCH 238/858] Properly dispose prepared statements --- .../Data/BaseSqliteRepository.cs | 12 --- .../Data/SqliteItemRepository.cs | 99 ++++++++----------- 2 files changed, 39 insertions(+), 72 deletions(-) diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs index 47706a4401..bc520b86e7 100644 --- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs +++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs @@ -166,18 +166,6 @@ namespace Emby.Server.Implementations.Data public IStatement PrepareStatement(IDatabaseConnection connection, string sql) => connection.PrepareStatement(sql); - public IStatement[] PrepareAll(IDatabaseConnection connection, IReadOnlyList<string> sql) - { - int len = sql.Count; - IStatement[] statements = new IStatement[len]; - for (int i = 0; i < len; i++) - { - statements[i] = connection.PrepareStatement(sql[i]); - } - - return statements; - } - protected bool TableExists(ManagedConnection connection, string name) { return connection.RunInTransaction( diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 80c4e5b103..71788a3e43 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -624,14 +624,8 @@ namespace Emby.Server.Implementations.Data private void SaveItemsInTransaction(IDatabaseConnection db, IEnumerable<(BaseItem Item, List<Guid> AncestorIds, BaseItem TopParent, string UserDataKey, List<string> InheritedTags)> tuples) { - var statements = PrepareAll(db, new string[] - { - SaveItemCommandText, - "delete from AncestorIds where ItemId=@ItemId" - }); - - using (var saveItemStatement = statements[0]) - using (var deleteAncestorsStatement = statements[1]) + using (var saveItemStatement = PrepareStatement(db, SaveItemCommandText)) + using (var deleteAncestorsStatement = PrepareStatement(db, "delete from AncestorIds where ItemId=@ItemId")) { var requiresReset = false; foreach (var tuple in tuples) @@ -1286,15 +1280,13 @@ namespace Emby.Server.Implementations.Data CheckDisposed(); using (var connection = GetConnection(true)) + using (var statement = PrepareStatement(connection, _retrieveItemColumnsSelectQuery)) { - using (var statement = PrepareStatement(connection, _retrieveItemColumnsSelectQuery)) - { - statement.TryBind("@guid", id); + statement.TryBind("@guid", id); - foreach (var row in statement.ExecuteQuery()) - { - return GetItem(row, new InternalItemsQuery()); - } + foreach (var row in statement.ExecuteQuery()) + { + return GetItem(row, new InternalItemsQuery()); } } @@ -1972,22 +1964,19 @@ namespace Emby.Server.Implementations.Data { CheckDisposed(); + var chapters = new List<ChapterInfo>(); using (var connection = GetConnection(true)) + using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId order by ChapterIndex asc")) { - var chapters = new List<ChapterInfo>(); + statement.TryBind("@ItemId", item.Id); - using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId order by ChapterIndex asc")) + foreach (var row in statement.ExecuteQuery()) { - statement.TryBind("@ItemId", item.Id); - - foreach (var row in statement.ExecuteQuery()) - { - chapters.Add(GetChapter(row, item)); - } + chapters.Add(GetChapter(row, item)); } - - return chapters; } + + return chapters; } /// <inheritdoc /> @@ -1996,16 +1985,14 @@ namespace Emby.Server.Implementations.Data CheckDisposed(); using (var connection = GetConnection(true)) + using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId and ChapterIndex=@ChapterIndex")) { - using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId and ChapterIndex=@ChapterIndex")) - { - statement.TryBind("@ItemId", item.Id); - statement.TryBind("@ChapterIndex", index); + statement.TryBind("@ItemId", item.Id); + statement.TryBind("@ChapterIndex", index); - foreach (var row in statement.ExecuteQuery()) - { - return GetChapter(row, item); - } + foreach (var row in statement.ExecuteQuery()) + { + return GetChapter(row, item); } } @@ -2858,13 +2845,10 @@ namespace Emby.Server.Implementations.Data connection.RunInTransaction( db => { - var itemQueryStatement = PrepareStatement(db, itemQuery); - var totalRecordCountQueryStatement = PrepareStatement(db, totalRecordCountQuery); - if (!isReturningZeroItems) { using (new QueryTimeLogger(Logger, itemQuery, "GetItems.ItemQuery")) - using (var statement = itemQueryStatement) + using (var statement = PrepareStatement(db, itemQuery)) { if (EnableJoinUserData(query)) { @@ -2899,7 +2883,7 @@ namespace Emby.Server.Implementations.Data if (query.EnableTotalRecordCount) { using (new QueryTimeLogger(Logger, totalRecordCountQuery, "GetItems.TotalRecordCount")) - using (var statement = totalRecordCountQueryStatement) + using (var statement = PrepareStatement(db, totalRecordCountQuery)) { if (EnableJoinUserData(query)) { @@ -4768,22 +4752,20 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type commandText.Append(" LIMIT ").Append(query.Limit); } + var list = new List<string>(); using (var connection = GetConnection(true)) + using (var statement = PrepareStatement(connection, commandText.ToString())) { - var list = new List<string>(); - using (var statement = PrepareStatement(connection, commandText.ToString())) + // Run this again to bind the params + GetPeopleWhereClauses(query, statement); + + foreach (var row in statement.ExecuteQuery()) { - // Run this again to bind the params - GetPeopleWhereClauses(query, statement); - - foreach (var row in statement.ExecuteQuery()) - { - list.Add(row.GetString(0)); - } + list.Add(row.GetString(0)); } - - return list; } + + return list; } public List<PersonInfo> GetPeople(InternalPeopleQuery query) @@ -4808,23 +4790,20 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type commandText += " LIMIT " + query.Limit; } + var list = new List<PersonInfo>(); using (var connection = GetConnection(true)) + using (var statement = PrepareStatement(connection, commandText)) { - var list = new List<PersonInfo>(); + // Run this again to bind the params + GetPeopleWhereClauses(query, statement); - using (var statement = PrepareStatement(connection, commandText)) + foreach (var row in statement.ExecuteQuery()) { - // Run this again to bind the params - GetPeopleWhereClauses(query, statement); - - foreach (var row in statement.ExecuteQuery()) - { - list.Add(GetPerson(row)); - } + list.Add(GetPerson(row)); } - - return list; } + + return list; } private List<string> GetPeopleWhereClauses(InternalPeopleQuery query, IStatement statement) From 643755cfa37352926fe4538d8598a0c4513f7412 Mon Sep 17 00:00:00 2001 From: David Pereira Cruz <david.cruz@gmail.com> Date: Tue, 11 Apr 2023 16:24:11 +0000 Subject: [PATCH 239/858] Translated using Weblate (Portuguese) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt/ --- Emby.Server.Implementations/Localization/Core/pt.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json index 92e0d34aec..2281e80c8a 100644 --- a/Emby.Server.Implementations/Localization/Core/pt.json +++ b/Emby.Server.Implementations/Localization/Core/pt.json @@ -122,5 +122,6 @@ "TaskOptimizeDatabaseDescription": "Base de dados compacta e corta espaço livre. A execução desta tarefa depois de digitalizar a biblioteca ou de fazer outras alterações que impliquem modificações na base de dados pode melhorar o desempenho.", "External": "Externo", "HearingImpaired": "Problemas auditivos", - "TaskKeyframeExtractor": "Extrator de quadro-chave" + "TaskKeyframeExtractor": "Extrator de quadro-chave", + "TaskKeyframeExtractorDescription": "Retira frames chave do video para criar listas HLS precisas. Esta tarefa pode correr durante algum tempo." } From 39af7f668c4ec2c283b063f71550baebb5454d3f Mon Sep 17 00:00:00 2001 From: Aman Alam <amanpreet.alam@gmail.com> Date: Wed, 12 Apr 2023 02:44:20 +0000 Subject: [PATCH 240/858] Translated using Weblate (Punjabi) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pa/ --- .../Localization/Core/pa.json | 42 ++++++++++--------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/pa.json b/Emby.Server.Implementations/Localization/Core/pa.json index 4ac57b630d..1f982feaf4 100644 --- a/Emby.Server.Implementations/Localization/Core/pa.json +++ b/Emby.Server.Implementations/Localization/Core/pa.json @@ -28,22 +28,22 @@ "ValueHasBeenAddedToLibrary": "{0} ਤੁਹਾਡੀ ਮੀਡੀਆ ਲਾਇਬ੍ਰੇਰੀ ਵਿੱਚ ਸ਼ਾਮਲ ਕੀਤਾ ਗਿਆ ਹੈ", "UserStoppedPlayingItemWithValues": "{0} ਨੇ {2} 'ਤੇ {1} ਖੇਡਣਾ ਪੂਰਾ ਕਰ ਲਿਆ ਹੈ", "UserStartedPlayingItemWithValues": "{0} {2} 'ਤੇ {1} ਖੇਡ ਰਿਹਾ ਹੈ", - "UserPolicyUpdatedWithName": "ਉਪਭੋਗਤਾ ਨੀਤੀ ਨੂੰ {0} ਲਈ ਅਪਡੇਟ ਕੀਤਾ ਗਿਆ ਹੈ", - "UserPasswordChangedWithName": "ਪਾਸਵਰਡ ਯੂਜ਼ਰ ਲਈ ਬਦਲਿਆ ਗਿਆ ਹੈ {0}", - "UserOnlineFromDevice": "{0} ਤੋਂ isਨਲਾਈਨ ਹੈ {1}", + "UserPolicyUpdatedWithName": "ਵਰਤੋਂਕਾਰ ਨੀਤੀ ਨੂੰ {0} ਲਈ ਅਪਡੇਟ ਕੀਤਾ ਗਿਆ ਹੈ", + "UserPasswordChangedWithName": "{0} ਵਰਤੋਂਕਾਰ ਲਈ ਪਾਸਵਰਡ ਬਦਲਿਆ ਗਿਆ ਸੀ", + "UserOnlineFromDevice": "{0} ਨੂੰ {1} ਤੋਂ ਆਨਲਾਈਨ ਹੈ", "UserOfflineFromDevice": "{0} ਤੋਂ ਡਿਸਕਨੈਕਟ ਹੋ ਗਿਆ ਹੈ {1}", - "UserLockedOutWithName": "ਯੂਜ਼ਰ {0} ਨੂੰ ਲਾਕ ਆਉਟ ਕਰ ਦਿੱਤਾ ਗਿਆ ਹੈ", - "UserDownloadingItemWithValues": "{0} ਡਾ{ਨਲੋਡ ਕਰ ਰਿਹਾ ਹੈ {1}", - "UserDeletedWithName": "ਯੂਜ਼ਰ {0} ਨੂੰ ਮਿਟਾ ਦਿੱਤਾ ਗਿਆ ਹੈ", - "UserCreatedWithName": "ਯੂਜ਼ਰ {0} ਬਣਾਇਆ ਗਿਆ ਹੈ", - "User": "ਯੂਜ਼ਰ", + "UserLockedOutWithName": "ਵਰਤੋਂਕਾਰ {0} ਨੂੰ ਲਾਕ ਕੀਤਾ ਗਿਆ ਹੈ", + "UserDownloadingItemWithValues": "{0} {1} ਨੂੰ ਡਾਊਨਲੋਡ ਕਰ ਰਿਹਾ ਹੈ", + "UserDeletedWithName": "ਵਰਤੋਂਕਾਰ {0} ਨੂੰ ਹਟਾਇਆ ਗਿਆ", + "UserCreatedWithName": "ਵਰਤੋਂਕਾਰ {0} ਬਣਾਇਆ ਗਿਆ ਹੈ", + "User": "ਵਰਤੋਂਕਾਰ", "Undefined": "ਪਰਿਭਾਸ਼ਤ", - "TvShows": "ਟੀਵੀ ਸ਼ੋਅਜ਼", + "TvShows": "ਟੀਵੀ ਸ਼ੋਅ", "System": "ਸਿਸਟਮ", "Sync": "ਸਿੰਕ", - "SubtitleDownloadFailureFromForItem": "ਉਪਸਿਰਲੇਖ {1} ਲਈ {0} ਤੋਂ ਡਾ toਨਲੋਡ ਕਰਨ ਵਿੱਚ ਅਸਫਲ ਰਹੇ", - "StartupEmbyServerIsLoading": "ਜੈਲੀਫਿਨ ਸਰਵਰ ਲੋਡ ਹੋ ਰਿਹਾ ਹੈ. ਕਿਰਪਾ ਕਰਕੇ ਜਲਦੀ ਹੀ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ.", - "Songs": "ਗਾਣੇਂ", + "SubtitleDownloadFailureFromForItem": "ਉਪਸਿਰਲੇਖ {1} ਲਈ {0} ਤੋਂ ਡਾਊਨਲੋਡ ਕਰਨ ਵਿੱਚ ਅਸਫਲ ਰਹੇ", + "StartupEmbyServerIsLoading": "Jellyfin ਸਰਵਰ ਲੋਡ ਹੋ ਰਿਹਾ ਹੈ। ਛੇਤੀ ਹੀ ਫ਼ੇਰ ਕੋਸ਼ਿਸ਼ ਕਰੋ।", + "Songs": "ਗਾਣੇ", "Shows": "ਸ਼ੋਅ", "ServerNameNeedsToBeRestarted": "{0} ਮੁੜ ਚਾਲੂ ਕਰਨ ਦੀ ਲੋੜ ਹੈ", "ScheduledTaskStartedWithName": "{0} ਸ਼ੁਰੂ ਹੋਇਆ", @@ -57,12 +57,12 @@ "Photos": "ਫੋਟੋਆਂ", "NotificationOptionVideoPlaybackStopped": "ਵੀਡੀਓ ਪਲੇਬੈਕ ਰੋਕਿਆ ਗਿਆ", "NotificationOptionVideoPlayback": "ਵੀਡੀਓ ਪਲੇਬੈਕ ਸ਼ੁਰੂ ਹੋਇਆ", - "NotificationOptionUserLockedOut": "ਉਪਭੋਗਤਾ ਨੂੰ ਲਾਕ ਆਉਟ ਕੀਤਾ ਗਿਆ", + "NotificationOptionUserLockedOut": "ਵਰਤੋਂਕਾਰ ਨੂੰ ਲਾਕ ਕੀਤਾ", "NotificationOptionTaskFailed": "ਨਿਰਧਾਰਤ ਕਾਰਜ ਅਸਫਲਤਾ", "NotificationOptionServerRestartRequired": "ਸਰਵਰ ਨੂੰ ਮੁੜ ਚਾਲੂ ਕਰਨ ਦੀ ਲੋੜ ਹੈ", "NotificationOptionPluginUpdateInstalled": "ਪਲੱਗਇਨ ਅਪਡੇਟ ਇੰਸਟੌਲ ਕੀਤਾ ਗਿਆ", "NotificationOptionPluginUninstalled": "ਪਲੱਗਇਨ ਅਣਇੰਸਟੌਲ ਕੀਤਾ", - "NotificationOptionPluginInstalled": "ਪਲੱਗਇਨ ਸਥਾਪਿਤ ਕੀਤਾ", + "NotificationOptionPluginInstalled": "ਪਲੱਗਇਨ ਇੰਸਟਾਲ ਕੀਤੀ", "NotificationOptionPluginError": "ਪਲੱਗਇਨ ਅਸਫਲ", "NotificationOptionNewLibraryContent": "ਨਵੀਂ ਸਮੱਗਰੀ ਸ਼ਾਮਲ ਕੀਤੀ ਗਈ", "NotificationOptionInstallationFailed": "ਇੰਸਟਾਲੇਸ਼ਨ ਅਸਫਲ", @@ -92,7 +92,7 @@ "HomeVideos": "ਘਰੇਲੂ ਵੀਡੀਓ", "HeaderRecordingGroups": "ਰਿਕਾਰਡਿੰਗ ਸਮੂਹ", "HeaderNextUp": "ਅੱਗੇ", - "HeaderLiveTV": "ਲਾਈਵ ਟੀ", + "HeaderLiveTV": "ਲਾਈਵ ਟੀਵੀ", "HeaderFavoriteSongs": "ਮਨਪਸੰਦ ਗਾਣੇ", "HeaderFavoriteShows": "ਮਨਪਸੰਦ ਸ਼ੋਅ", "HeaderFavoriteEpisodes": "ਮਨਪਸੰਦ ਐਪੀਸੋਡ", @@ -102,20 +102,22 @@ "HeaderAlbumArtists": "ਐਲਬਮ ਕਲਾਕਾਰ", "Genres": "ਸ਼ੈਲੀਆਂ", "Forced": "ਮਜਬੂਰ", - "Folders": "ਫੋਲਡਰਸ", + "Folders": "ਫੋਲਡਰ", "Favorites": "ਮਨਪਸੰਦ", - "FailedLoginAttemptWithUserName": "ਤੋਂ ਲਾਗਇਨ ਕੋਸ਼ਿਸ਼ ਫੇਲ ਹੋਈ {0}", + "FailedLoginAttemptWithUserName": "{0} ਤੋਂ ਲਾਗਇਨ ਕੋਸ਼ਿਸ਼ ਫੇਲ ਹੋਈ", "DeviceOnlineWithName": "{0} ਜੁੜਿਆ ਹੋਇਆ ਹੈ", "DeviceOfflineWithName": "{0} ਡਿਸਕਨੈਕਟ ਹੋ ਗਿਆ ਹੈ", "Default": "ਡਿਫੌਲਟ", "Collections": "ਸੰਗ੍ਰਹਿਣ", - "ChapterNameValue": "ਅਧਿਆਇ {0}", + "ChapterNameValue": "ਚੈਪਟਰ {0}", "Channels": "ਚੈਨਲ", - "CameraImageUploadedFrom": "ਤੋਂ ਇੱਕ ਨਵਾਂ ਕੈਮਰਾ ਚਿੱਤਰ ਅਪਲੋਡ ਕੀਤਾ ਗਿਆ ਹੈ {0}", + "CameraImageUploadedFrom": "{0} ਤੋਂ ਇੱਕ ਨਵਾਂ ਕੈਮਰਾ ਚਿੱਤਰ ਅਪਲੋਡ ਕੀਤਾ ਗਿਆ ਹੈ", "Books": "ਕਿਤਾਬਾਂ", "AuthenticationSucceededWithUserName": "{0} ਸਫਲਤਾਪੂਰਕ ਪ੍ਰਮਾਣਿਤ", "Artists": "ਕਲਾਕਾਰ", "Application": "ਐਪਲੀਕੇਸ਼ਨ", "AppDeviceValues": "ਐਪ: {0}, ਜੰਤਰ: {1}", - "Albums": "ਐਲਬਮਾਂ" + "Albums": "ਐਲਬਮਾਂ", + "TaskOptimizeDatabase": "ਡਾਟਾਬੇਸ ਅਨੁਕੂਲ ਬਣਾਓ", + "External": "ਬਾਹਰੀ" } From 648240dad0adf91dd53acd2e0edc3e23c5901a1c Mon Sep 17 00:00:00 2001 From: SuperDumbTM <eltonlochunkit@gmail.com> Date: Fri, 14 Apr 2023 12:31:03 +0000 Subject: [PATCH 241/858] Translated using Weblate (Chinese (Traditional, Hong Kong)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant_HK/ --- .../Localization/Core/zh-HK.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json index b08119d0a6..c65568382a 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -66,13 +66,13 @@ "PluginInstalledWithName": "已安裝 {0}", "PluginUninstalledWithName": "已移除 {0}", "PluginUpdatedWithName": "已更新 {0}", - "ProviderValue": "提供者: {0}", - "ScheduledTaskFailedWithName": "{0} 任務失敗", - "ScheduledTaskStartedWithName": "{0} 任務開始", + "ProviderValue": "提供者:{0}", + "ScheduledTaskFailedWithName": "{0} 執行失敗", + "ScheduledTaskStartedWithName": "{0} 開始執行", "ServerNameNeedsToBeRestarted": "{0} 需要重啟", "Shows": "節目", "Songs": "歌曲", - "StartupEmbyServerIsLoading": "Jellyfin 載入中,請稍後再試。", + "StartupEmbyServerIsLoading": "正在載入 Jellyfin,請稍後再試。", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "無法從 {0} 下載 {1} 的字幕", "Sync": "同步", @@ -104,8 +104,8 @@ "TaskCleanTranscode": "清理轉碼目錄", "TaskUpdatePluginsDescription": "下載並更新能夠被自動更新的插件。", "TaskRefreshPeopleDescription": "更新媒體庫中演員和導演的元數據。", - "TaskCleanLogsDescription": "刪除超過{0}天的日誌文件。", - "TaskCleanLogs": "清理日誌目錄", + "TaskCleanLogsDescription": "刪除超過{0}天的紀錄檔。", + "TaskCleanLogs": "清理紀錄檔目錄", "TaskRefreshLibrary": "掃描媒體庫", "TaskRefreshChapterImagesDescription": "為帶有章節的影片建立縮圖。", "TaskRefreshChapterImages": "提取章節圖像", @@ -120,7 +120,7 @@ "Default": "預設", "TaskOptimizeDatabaseDescription": "壓縮數據庫並截斷可用空間。在掃描媒體庫或執行其他數據庫的修改後運行此任務可能會提高性能。", "TaskOptimizeDatabase": "最佳化數據庫", - "TaskCleanActivityLogDescription": "刪除早於設定時間的日誌記錄。", + "TaskCleanActivityLogDescription": "刪除早於設定時間的活動記錄。", "TaskKeyframeExtractorDescription": "提取關鍵幀以建立更準確的 HLS 播放列表。此工作或需要使用較長時間來完成。", "TaskKeyframeExtractor": "關鍵幀提取器", "External": "外部", From 5c370ff323028d093cb4a9c6c0d79d04a6c9ac0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=8C=E7=A6=8F=E5=AE=A2=E6=A0=88=E8=B7=91=E5=A0=82?= =?UTF-8?q?=E8=98=B8=E7=B3=96?= <2316804089@qq.com> Date: Sat, 15 Apr 2023 11:15:21 -0400 Subject: [PATCH 242/858] Added translation using Weblate (Chinese (Literary)) --- Emby.Server.Implementations/Localization/Core/lzh.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 Emby.Server.Implementations/Localization/Core/lzh.json diff --git a/Emby.Server.Implementations/Localization/Core/lzh.json b/Emby.Server.Implementations/Localization/Core/lzh.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/lzh.json @@ -0,0 +1 @@ +{} From d622fde89a192394f6229aeabb50b81b44a7f4fb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 15 Apr 2023 16:56:12 -0600 Subject: [PATCH 243/858] Update dependency EFCoreSecondLevelCacheInterceptor to v3.8.8 (#9648) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 55948cb4a4..f35ffb062c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -17,7 +17,7 @@ <PackageVersion Include="Diacritics" Version="3.3.18" /> <PackageVersion Include="DiscUtils.Udf" Version="0.16.13" /> <PackageVersion Include="DotNet.Glob" Version="3.1.3" /> - <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.8.6" /> + <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.8.8" /> <PackageVersion Include="FsCheck.Xunit" Version="2.16.5" /> <PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" /> <PackageVersion Include="libse" Version="3.6.11" /> From eb7e1a9a5aacafc614352dfee3a668598569762f Mon Sep 17 00:00:00 2001 From: Nyanmisaka <nst799610810@gmail.com> Date: Sun, 16 Apr 2023 06:57:01 +0800 Subject: [PATCH 244/858] Update issue template to help HWA debugging (#9645) Signed-off-by: nyanmisaka <nst799610810@gmail.com> --- .github/ISSUE_TEMPLATE/issue report.yml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/issue report.yml b/.github/ISSUE_TEMPLATE/issue report.yml index fd377df9db..5878028330 100644 --- a/.github/ISSUE_TEMPLATE/issue report.yml +++ b/.github/ISSUE_TEMPLATE/issue report.yml @@ -30,9 +30,9 @@ body: label: Jellyfin Version description: What version of Jellyfin are you running? options: - - 10.8.0 + - 10.8.z + - 10.8.9 - 10.7.7 - - 10.7.z - 10.6.4 - Other validations: @@ -47,13 +47,15 @@ body: label: Environment description: | Examples: - - **OS**: [e.g. Debian, Windows] + - **OS**: [e.g. Debian 11, Windows 10] + - **Linux Kernel**: [e.g. none, 5.15, 6.1, etc.] - **Virtualization**: [e.g. Docker, KVM, LXC] - **Clients**: [Browser, Android, Fire Stick, etc.] - **Browser**: [e.g. Firefox 91, Chrome 93, Safari 13] - - **FFmpeg Version**: [e.g. 4.3.2-Jellyfin] + - **FFmpeg Version**: [e.g. 5.1.2-Jellyfin] - **Playback**: [Direct Play, Remux, Direct Stream, Transcode] - **Hardware Acceleration**: [e.g. none, VAAPI, NVENC, etc.] + - **GPU Model**: [e.g. none, UHD630, GTX1050, etc.] - **Installed Plugins**: [e.g. none, Fanart, Anime, etc.] - **Reverse Proxy**: [e.g. none, nginx, apache, etc.] - **Base URL**: [e.g. none, yes: /example] @@ -61,12 +63,14 @@ body: - **Storage**: [e.g. local, NFS, cloud] value: | - OS: + - Linux Kernel: - Virtualization: - Clients: - Browser: - FFmpeg Version: - Playback Method: - Hardware Acceleration: + - GPU Model: - Plugins: - Reverse Proxy: - Base URL: @@ -84,8 +88,8 @@ body: id: ffmpeg-logs attributes: label: FFmpeg logs - description: Please copy and paste any relevant log output. This can be found in Dashboard > Logs. - placeholder: It's important to include the specific codec details. If no FFmpeg logs appear, the file was Direct Played and did not use FFmpeg. + description: Please copy and paste recent FFmpeg log output. This can be found in Dashboard > Logs > FFmpeg*.log. + placeholder: This field is mandatory for debugging hardware transcoding issues. It's important to include the specific codec details. If no FFmpeg logs appear, the file was Direct Played and did not use FFmpeg. render: shell - type: textarea id: browserlogs From 1fb4d94973d1130a47bd472d97a7932999c85b9f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 15 Apr 2023 16:57:25 -0600 Subject: [PATCH 245/858] Update CI dependencies (#9640) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 8 ++++---- .github/workflows/commands.yml | 4 ++-- .github/workflows/openapi.yml | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 044d02e23a..05773d1d99 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -20,18 +20,18 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@83b7061638ee4956cf7545a6f7efe594e5ad0247 # v3.5.1 + uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 - name: Setup .NET uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # v3.0.3 with: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@d186a2a36cc67bfa1b860e6170d37fb9634742c7 # v2.2.11 + uses: github/codeql-action/init@7df0ce34898d659f95c0c4a09eaa8d4e32ee64db # v2.2.12 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@d186a2a36cc67bfa1b860e6170d37fb9634742c7 # v2.2.11 + uses: github/codeql-action/autobuild@7df0ce34898d659f95c0c4a09eaa8d4e32ee64db # v2.2.12 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@d186a2a36cc67bfa1b860e6170d37fb9634742c7 # v2.2.11 + uses: github/codeql-action/analyze@7df0ce34898d659f95c0c4a09eaa8d4e32ee64db # v2.2.12 diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index cee70ad580..79b1c0e8c6 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -24,7 +24,7 @@ jobs: reactions: '+1' - name: Checkout the latest code - uses: actions/checkout@83b7061638ee4956cf7545a6f7efe594e5ad0247 # v3.5.1 + uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 with: token: ${{ secrets.JF_BOT_TOKEN }} fetch-depth: 0 @@ -51,7 +51,7 @@ jobs: reactions: eyes - name: Checkout the latest code - uses: actions/checkout@83b7061638ee4956cf7545a6f7efe594e5ad0247 # v3.5.1 + uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 with: token: ${{ secrets.JF_BOT_TOKEN }} fetch-depth: 0 diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index 2192af2760..79c001e8b2 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -14,7 +14,7 @@ jobs: permissions: read-all steps: - name: Checkout repository - uses: actions/checkout@83b7061638ee4956cf7545a6f7efe594e5ad0247 # v3.5.1 + uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 with: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} @@ -39,7 +39,7 @@ jobs: permissions: read-all steps: - name: Checkout repository - uses: actions/checkout@83b7061638ee4956cf7545a6f7efe594e5ad0247 # v3.5.1 + uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 with: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} From 922e04dbf347611ac813a6ca823c0c4fde622c54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=8C=E7=A6=8F=E5=AE=A2=E6=A0=88=E8=B7=91=E5=A0=82?= =?UTF-8?q?=E8=98=B8=E7=B3=96?= <2316804089@qq.com> Date: Sat, 15 Apr 2023 15:18:57 +0000 Subject: [PATCH 246/858] Translated using Weblate (Chinese (Literary)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/lzh/ --- Emby.Server.Implementations/Localization/Core/lzh.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/lzh.json b/Emby.Server.Implementations/Localization/Core/lzh.json index 0967ef424b..031a4dac76 100644 --- a/Emby.Server.Implementations/Localization/Core/lzh.json +++ b/Emby.Server.Implementations/Localization/Core/lzh.json @@ -1 +1,6 @@ -{} +{ + "Albums": "辑册", + "Artists": "艺人", + "AuthenticationSucceededWithUserName": "{0} 授之权矣", + "Books": "册" +} From 92f50054b28c85afbee0dfa99016c4b71548de6f Mon Sep 17 00:00:00 2001 From: AmbulantRex <21176662+AmbulantRex@users.noreply.github.com> Date: Sun, 16 Apr 2023 07:46:12 -0600 Subject: [PATCH 247/858] Add explicit mapping instead of reflection to manifest reconciliation. --- .../Plugins/PluginManager.cs | 94 +++++++++---------- MediaBrowser.Model/Updates/VersionInfo.cs | 2 - .../Plugins/PluginManagerTests.cs | 67 ++++++++++--- 3 files changed, 98 insertions(+), 65 deletions(-) diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs index 2d2ad26d29..10d5ea906e 100644 --- a/Emby.Server.Implementations/Plugins/PluginManager.cs +++ b/Emby.Server.Implementations/Plugins/PluginManager.cs @@ -432,69 +432,67 @@ namespace Emby.Server.Implementations.Plugins ImagePath = imagePath }; - var metafile = Path.Combine(Path.Combine(path, MetafileName)); - if (File.Exists(metafile)) + if (!ReconcileManifest(manifest, path)) { - var data = File.ReadAllBytes(metafile); - var localManifest = JsonSerializer.Deserialize<PluginManifest>(data, _jsonOptions) ?? new PluginManifest(); - - // Plugin installation is the typical cause for populating a manifest. Activate. - localManifest.Status = status == PluginStatus.Disabled ? PluginStatus.Disabled : PluginStatus.Active; - - if (!Equals(localManifest.Id, manifest.Id)) - { - _logger.LogError("The manifest ID {LocalUUID} did not match the package info ID {PackageUUID}.", localManifest.Id, manifest.Id); - localManifest.Status = PluginStatus.Malfunctioned; - } - - if (localManifest.Version != manifest.Version) - { - _logger.LogWarning("The version of the local manifest was {LocalVersion}, but {PackageVersion} was expected. The value will be replaced.", localManifest.Version, manifest.Version); - - // Correct the local version. - localManifest.Version = manifest.Version; - } - - // Reconcile missing data against repository manifest. - ReconcileManifest(localManifest, manifest); - - manifest = localManifest; - } - else - { - _logger.LogInformation("No local manifest exists for plugin {Plugin}. Populating from repository manifest.", manifest.Name); + // An error occurred during reconciliation and saving could be undesirable. + return false; } return SaveManifest(manifest, path); } /// <summary> - /// Resolve the target plugin manifest against the source. Values are mapped onto the - /// target only if they are default values or empty strings. ID and status fields are ignored. + /// Reconciles the manifest against any properties that exist locally in a pre-packaged meta.json found at the path. + /// If no file is found, no reconciliation occurs. /// </summary> - /// <param name="baseManifest">The base <see cref="PluginManifest"/> to be reconciled.</param> - /// <param name="projector">The <see cref="PluginManifest"/> to reconcile against.</param> - private void ReconcileManifest(PluginManifest baseManifest, PluginManifest projector) + /// <param name="manifest">The <see cref="PluginManifest"/> to reconcile against.</param> + /// <param name="path">The plugin path.</param> + /// <returns>The reconciled <see cref="PluginManifest"/>.</returns> + private bool ReconcileManifest(PluginManifest manifest, string path) { - var ignoredFields = new string[] + try { - nameof(baseManifest.Id), - nameof(baseManifest.Status) - }; - - foreach (var property in baseManifest.GetType().GetProperties()) - { - var localValue = property.GetValue(baseManifest); - - if (property.PropertyType == typeof(bool) || ignoredFields.Any(s => Equals(s, property.Name))) + var metafile = Path.Combine(path, MetafileName); + if (!File.Exists(metafile)) { - continue; + _logger.LogInformation("No local manifest exists for plugin {Plugin}. Skipping manifest reconciliation.", manifest.Name); + return true; } - if (property.PropertyType.IsNullOrDefault(localValue) || (property.PropertyType == typeof(string) && (string)localValue! == string.Empty)) + var data = File.ReadAllBytes(metafile); + var localManifest = JsonSerializer.Deserialize<PluginManifest>(data, _jsonOptions) ?? new PluginManifest(); + + if (!Equals(localManifest.Id, manifest.Id)) { - property.SetValue(baseManifest, property.GetValue(projector)); + _logger.LogError("The manifest ID {LocalUUID} did not match the package info ID {PackageUUID}.", localManifest.Id, manifest.Id); + manifest.Status = PluginStatus.Malfunctioned; } + + if (localManifest.Version != manifest.Version) + { + // Package information provides the version and is the source of truth. Pre-packages meta.json is assumed to be a mistake in this regard. + _logger.LogWarning("The version of the local manifest was {LocalVersion}, but {PackageVersion} was expected. The value will be replaced.", localManifest.Version, manifest.Version); + } + + // Explicitly mapping properties instead of using reflection is preferred here. + manifest.Category = string.IsNullOrEmpty(localManifest.Category) ? manifest.Category : localManifest.Category; + manifest.AutoUpdate = localManifest.AutoUpdate; // Preserve whatever is local. Package info does not have this property. + manifest.Changelog = string.IsNullOrEmpty(localManifest.Changelog) ? manifest.Changelog : localManifest.Changelog; + manifest.Description = string.IsNullOrEmpty(localManifest.Description) ? manifest.Description : localManifest.Description; + manifest.Name = string.IsNullOrEmpty(localManifest.Name) ? manifest.Name : localManifest.Name; + manifest.Overview = string.IsNullOrEmpty(localManifest.Overview) ? manifest.Overview : localManifest.Overview; + manifest.Owner = string.IsNullOrEmpty(localManifest.Owner) ? manifest.Owner : localManifest.Owner; + manifest.TargetAbi = string.IsNullOrEmpty(localManifest.TargetAbi) ? manifest.TargetAbi : localManifest.TargetAbi; + manifest.Timestamp = localManifest.Timestamp.IsNullOrDefault() ? manifest.Timestamp : localManifest.Timestamp; + manifest.ImagePath = string.IsNullOrEmpty(localManifest.ImagePath) ? manifest.ImagePath : localManifest.ImagePath; + manifest.Assemblies = localManifest.Assemblies; + + return true; + } + catch (Exception e) + { + _logger.LogWarning(e, "Unable to reconcile plugin manifest due to an error. {Path}", path); + return false; } } diff --git a/MediaBrowser.Model/Updates/VersionInfo.cs b/MediaBrowser.Model/Updates/VersionInfo.cs index 53e1d29b01..320199f984 100644 --- a/MediaBrowser.Model/Updates/VersionInfo.cs +++ b/MediaBrowser.Model/Updates/VersionInfo.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using System.Text.Json.Serialization; using SysVersion = System.Version; diff --git a/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs index 204d144214..d4b90dac02 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs @@ -172,6 +172,24 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins Assemblies = new[] { "Jellyfin.Test.dll" } }; + var expectedManifest = new PluginManifest + { + Id = partial.Id, + Name = packageInfo.Name, + AutoUpdate = partial.AutoUpdate, + Status = PluginStatus.Active, + Owner = packageInfo.Owner, + Assemblies = partial.Assemblies, + Category = packageInfo.Category, + Description = packageInfo.Description, + Overview = packageInfo.Overview, + TargetAbi = packageInfo.Versions[0].TargetAbi!, + Timestamp = DateTime.Parse(packageInfo.Versions[0].Timestamp!, CultureInfo.InvariantCulture), + Changelog = packageInfo.Versions[0].Changelog!, + Version = new Version(1, 0).ToString(), + ImagePath = string.Empty + }; + var metafilePath = Path.Combine(_pluginPath, "meta.json"); File.WriteAllText(metafilePath, JsonSerializer.Serialize(partial, _options)); @@ -183,22 +201,41 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins var result = JsonSerializer.Deserialize<PluginManifest>(resultBytes, _options); Assert.NotNull(result); - Assert.Equal(packageInfo.Name, result.Name); - Assert.Equal(packageInfo.Owner, result.Owner); - Assert.Equal(PluginStatus.Active, result.Status); - Assert.NotEmpty(result.Assemblies); - Assert.False(result.AutoUpdate); + Assert.Equivalent(expectedManifest, result); + } - // Preserved - Assert.Equal(packageInfo.Category, result.Category); - Assert.Equal(packageInfo.Description, result.Description); - Assert.Equal(packageInfo.Id, result.Id); - Assert.Equal(packageInfo.Overview, result.Overview); - Assert.Equal(partial.Assemblies[0], result.Assemblies[0]); - Assert.Equal(packageInfo.Versions[0].TargetAbi, result.TargetAbi); - Assert.Equal(DateTime.Parse(packageInfo.Versions[0].Timestamp!, CultureInfo.InvariantCulture), result.Timestamp); - Assert.Equal(packageInfo.Versions[0].Changelog, result.Changelog); - Assert.Equal(packageInfo.Versions[0].Version, result.Version); + [Fact] + public async Task PopulateManifest_NoMetafile_PreservesManifest() + { + var packageInfo = GenerateTestPackage(); + var expectedManifest = new PluginManifest + { + Id = packageInfo.Id, + Name = packageInfo.Name, + AutoUpdate = true, + Status = PluginStatus.Active, + Owner = packageInfo.Owner, + Assemblies = Array.Empty<string>(), + Category = packageInfo.Category, + Description = packageInfo.Description, + Overview = packageInfo.Overview, + TargetAbi = packageInfo.Versions[0].TargetAbi!, + Timestamp = DateTime.Parse(packageInfo.Versions[0].Timestamp!, CultureInfo.InvariantCulture), + Changelog = packageInfo.Versions[0].Changelog!, + Version = packageInfo.Versions[0].Version, + ImagePath = string.Empty + }; + + var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, null!, new Version(1, 0)); + + await pluginManager.PopulateManifest(packageInfo, new Version(1, 0), _pluginPath, PluginStatus.Active); + + var metafilePath = Path.Combine(_pluginPath, "meta.json"); + var resultBytes = File.ReadAllBytes(metafilePath); + var result = JsonSerializer.Deserialize<PluginManifest>(resultBytes, _options); + + Assert.NotNull(result); + Assert.Equivalent(expectedManifest, result); } [Fact] From ba5e51700e753f20d568418b8f0f7b48f4ca271c Mon Sep 17 00:00:00 2001 From: Brad Beattie <bradbeattie@gmail.com> Date: Sun, 16 Apr 2023 10:09:54 -0700 Subject: [PATCH 248/858] Fix null parental rating comparison (#9618) --- Emby.Server.Implementations/Data/SqliteItemRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 71788a3e43..e59f2a198e 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -2379,7 +2379,7 @@ namespace Emby.Server.Implementations.Data else { builder.Append( - @"(SELECT CASE WHEN InheritedParentalRatingValue=0 + @"(SELECT CASE WHEN COALESCE(InheritedParentalRatingValue, 0)=0 THEN 0 ELSE 10.0 / (1.0 + ABS(InheritedParentalRatingValue - @InheritedParentalRatingValue)) END)"); From dfcf0cf292b3e615a7869227417ea35a924acc01 Mon Sep 17 00:00:00 2001 From: Brett Petch <brettpetch@icloud.com> Date: Sun, 16 Apr 2023 13:10:14 -0400 Subject: [PATCH 249/858] fix: dead link (#9626) --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 53f48d9598..70018bb365 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -1415,7 +1415,7 @@ namespace MediaBrowser.Controller.MediaEncoding var param = string.Empty; // Tutorials: Enable Intel GuC / HuC firmware loading for Low Power Encoding. - // https://01.org/linuxgraphics/downloads/firmware + // https://01.org/group/43/downloads/firmware // https://wiki.archlinux.org/title/intel_graphics#Enable_GuC_/_HuC_firmware_loading // Intel Low Power Encoding can save unnecessary CPU-GPU synchronization, // which will reduce overhead in performance intensive tasks such as 4k transcoding and tonemapping. From 0cc8bf38ac1dc1845401aa28c6f06c5ce0f88a3d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 16 Apr 2023 19:02:02 +0000 Subject: [PATCH 250/858] chore(deps): update actions/stale action to v8 --- .github/workflows/repo-stale.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/repo-stale.yaml b/.github/workflows/repo-stale.yaml index acbdcd1b75..c753c1600a 100644 --- a/.github/workflows/repo-stale.yaml +++ b/.github/workflows/repo-stale.yaml @@ -37,7 +37,7 @@ jobs: runs-on: ubuntu-latest if: ${{ contains(github.repository, 'jellyfin/') }} steps: - - uses: actions/stale@6f05e4244c9a0b2ed3401882b05d701dd0a7289b # v7.0.0 + - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0 with: repo-token: ${{ secrets.JF_BOT_TOKEN }} operations-per-run: 75 From c7174255498e28272fbe6d4d6867a774a3327eff Mon Sep 17 00:00:00 2001 From: AmbulantRex <21176662+AmbulantRex@users.noreply.github.com> Date: Sun, 16 Apr 2023 18:47:57 -0600 Subject: [PATCH 251/858] Remove unnecessary type extension and handle feedback. --- .../Plugins/PluginManager.cs | 13 ++-- src/Jellyfin.Extensions/TypeExtensions.cs | 45 ------------ .../TypeExtensionsTests.cs | 68 ------------------- 3 files changed, 7 insertions(+), 119 deletions(-) delete mode 100644 src/Jellyfin.Extensions/TypeExtensions.cs delete mode 100644 tests/Jellyfin.Extensions.Tests/TypeExtensionsTests.cs diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs index 10d5ea906e..48584ae0cb 100644 --- a/Emby.Server.Implementations/Plugins/PluginManager.cs +++ b/Emby.Server.Implementations/Plugins/PluginManager.cs @@ -432,7 +432,7 @@ namespace Emby.Server.Implementations.Plugins ImagePath = imagePath }; - if (!ReconcileManifest(manifest, path)) + if (!await ReconcileManifest(manifest, path)) { // An error occurred during reconciliation and saving could be undesirable. return false; @@ -448,7 +448,7 @@ namespace Emby.Server.Implementations.Plugins /// <param name="manifest">The <see cref="PluginManifest"/> to reconcile against.</param> /// <param name="path">The plugin path.</param> /// <returns>The reconciled <see cref="PluginManifest"/>.</returns> - private bool ReconcileManifest(PluginManifest manifest, string path) + private async Task<bool> ReconcileManifest(PluginManifest manifest, string path) { try { @@ -459,8 +459,9 @@ namespace Emby.Server.Implementations.Plugins return true; } - var data = File.ReadAllBytes(metafile); - var localManifest = JsonSerializer.Deserialize<PluginManifest>(data, _jsonOptions) ?? new PluginManifest(); + using var metaStream = File.OpenRead(metafile); + var localManifest = await JsonSerializer.DeserializeAsync<PluginManifest>(metaStream, _jsonOptions); + localManifest ??= new PluginManifest(); if (!Equals(localManifest.Id, manifest.Id)) { @@ -483,7 +484,7 @@ namespace Emby.Server.Implementations.Plugins manifest.Overview = string.IsNullOrEmpty(localManifest.Overview) ? manifest.Overview : localManifest.Overview; manifest.Owner = string.IsNullOrEmpty(localManifest.Owner) ? manifest.Owner : localManifest.Owner; manifest.TargetAbi = string.IsNullOrEmpty(localManifest.TargetAbi) ? manifest.TargetAbi : localManifest.TargetAbi; - manifest.Timestamp = localManifest.Timestamp.IsNullOrDefault() ? manifest.Timestamp : localManifest.Timestamp; + manifest.Timestamp = localManifest.Timestamp.Equals(default) ? manifest.Timestamp : localManifest.Timestamp; manifest.ImagePath = string.IsNullOrEmpty(localManifest.ImagePath) ? manifest.ImagePath : localManifest.ImagePath; manifest.Assemblies = localManifest.Assemblies; @@ -842,7 +843,7 @@ namespace Emby.Server.Implementations.Plugins var canonicalized = Path.Combine(plugin.Path, path).Canonicalize(); // Ensure we stay in the plugin directory. - if (!canonicalized.StartsWith(plugin.Path.NormalizePath()!, StringComparison.Ordinal)) + if (!canonicalized.StartsWith(plugin.Path.NormalizePath(), StringComparison.Ordinal)) { _logger.LogError("Assembly path {Path} is not inside the plugin directory.", path); return false; diff --git a/src/Jellyfin.Extensions/TypeExtensions.cs b/src/Jellyfin.Extensions/TypeExtensions.cs deleted file mode 100644 index 5b1111d594..0000000000 --- a/src/Jellyfin.Extensions/TypeExtensions.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Globalization; - -namespace Jellyfin.Extensions; - -/// <summary> -/// Provides extensions methods for <see cref="Type" />. -/// </summary> -public static class TypeExtensions -{ - /// <summary> - /// Checks if the supplied value is the default or null value for that type. - /// </summary> - /// <typeparam name="T">The type of the value to compare.</typeparam> - /// <param name="type">The type.</param> - /// <param name="value">The value to check.</param> - /// <returns><see langword="true"/> if the value is the default for the type. Otherwise, <see langword="false"/>.</returns> - public static bool IsNullOrDefault<T>(this Type type, T value) - { - if (value is null) - { - return true; - } - - object? tmp = value; - object? defaultValue = type.IsValueType ? Activator.CreateInstance(type) : null; - if (type.IsAssignableTo(typeof(IConvertible))) - { - tmp = Convert.ChangeType(value, type, CultureInfo.InvariantCulture); - } - - return Equals(tmp, defaultValue); - } - - /// <summary> - /// Checks if the object is currently a default or null value. Boxed types will be unboxed prior to comparison. - /// </summary> - /// <param name="obj">The object to check.</param> - /// <returns><see langword="true"/> if the value is the default for the type. Otherwise, <see langword="false"/>.</returns> - public static bool IsNullOrDefault(this object? obj) - { - // Unbox the type and check. - return obj?.GetType().IsNullOrDefault(obj) ?? true; - } -} diff --git a/tests/Jellyfin.Extensions.Tests/TypeExtensionsTests.cs b/tests/Jellyfin.Extensions.Tests/TypeExtensionsTests.cs deleted file mode 100644 index 747913fa1a..0000000000 --- a/tests/Jellyfin.Extensions.Tests/TypeExtensionsTests.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using Xunit; - -namespace Jellyfin.Extensions.Tests -{ - public class TypeExtensionsTests - { - [Theory] - [InlineData(typeof(byte), byte.MaxValue, false)] - [InlineData(typeof(short), short.MinValue, false)] - [InlineData(typeof(ushort), ushort.MaxValue, false)] - [InlineData(typeof(int), int.MinValue, false)] - [InlineData(typeof(uint), uint.MaxValue, false)] - [InlineData(typeof(long), long.MinValue, false)] - [InlineData(typeof(ulong), ulong.MaxValue, false)] - [InlineData(typeof(decimal), -1.0, false)] - [InlineData(typeof(bool), true, false)] - [InlineData(typeof(char), 'a', false)] - [InlineData(typeof(string), "", false)] - [InlineData(typeof(object), 1, false)] - [InlineData(typeof(byte), 0, true)] - [InlineData(typeof(short), 0, true)] - [InlineData(typeof(ushort), 0, true)] - [InlineData(typeof(int), 0, true)] - [InlineData(typeof(uint), 0, true)] - [InlineData(typeof(long), 0, true)] - [InlineData(typeof(ulong), 0, true)] - [InlineData(typeof(decimal), 0, true)] - [InlineData(typeof(bool), false, true)] - [InlineData(typeof(char), '\x0000', true)] - [InlineData(typeof(string), null, true)] - [InlineData(typeof(object), null, true)] - [InlineData(typeof(PhonyClass), null, true)] - [InlineData(typeof(DateTime), null, true)] // Special case handled within the test. - [InlineData(typeof(DateTime), null, false)] // Special case handled within the test. - [InlineData(typeof(byte?), null, true)] - [InlineData(typeof(short?), null, true)] - [InlineData(typeof(ushort?), null, true)] - [InlineData(typeof(int?), null, true)] - [InlineData(typeof(uint?), null, true)] - [InlineData(typeof(long?), null, true)] - [InlineData(typeof(ulong?), null, true)] - [InlineData(typeof(decimal?), null, true)] - [InlineData(typeof(bool?), null, true)] - [InlineData(typeof(char?), null, true)] - public void IsNullOrDefault_Matches_Expected(Type type, object? value, bool expectedResult) - { - if (type == typeof(DateTime)) - { - if (expectedResult) - { - value = default(DateTime); - } - else - { - value = DateTime.Now; - } - } - - Assert.Equal(expectedResult, type.IsNullOrDefault(value)); - Assert.Equal(expectedResult, value.IsNullOrDefault()); - } - - private class PhonyClass - { - } - } -} From 9c88752f227e4f3905505c070f8e14818ad4654f Mon Sep 17 00:00:00 2001 From: Juan B <benitesjn@gmail.com> Date: Mon, 17 Apr 2023 01:53:11 +0000 Subject: [PATCH 252/858] Translated using Weblate (Spanish) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/es/ --- Emby.Server.Implementations/Localization/Core/es.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json index 5e41462db1..f5636a0af0 100644 --- a/Emby.Server.Implementations/Localization/Core/es.json +++ b/Emby.Server.Implementations/Localization/Core/es.json @@ -31,7 +31,7 @@ "ItemRemovedWithName": "{0} ha sido eliminado de la biblioteca", "LabelIpAddressValue": "Dirección IP: {0}", "LabelRunningTimeValue": "Tiempo de funcionamiento: {0}", - "Latest": "Último contenido en", + "Latest": "Últimas", "MessageApplicationUpdated": "Se ha actualizado el servidor Jellyfin", "MessageApplicationUpdatedTo": "Se ha actualizado el servidor Jellyfin a la versión {0}", "MessageNamedServerConfigurationUpdatedWithValue": "La sección {0} de configuración del servidor ha sido actualizada", From 4b2b46c8f3d98227b01d57cd3f4805e65ddac727 Mon Sep 17 00:00:00 2001 From: SuperDumbTM <eltonlochunkit@gmail.com> Date: Sun, 16 Apr 2023 11:18:00 +0000 Subject: [PATCH 253/858] Translated using Weblate (Chinese (Traditional, Hong Kong)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant_HK/ --- Emby.Server.Implementations/Localization/Core/zh-HK.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json index c65568382a..610f503ddb 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -4,11 +4,11 @@ "Application": "應用程式", "Artists": "藝人", "AuthenticationSucceededWithUserName": "{0} 授權成功", - "Books": "圖書", + "Books": "書籍", "CameraImageUploadedFrom": "{0} 成功上傳一張新照片", "Channels": "頻道", "ChapterNameValue": "第 {0} 章", - "Collections": "合輯", + "Collections": "系列", "DeviceOfflineWithName": "{0} 已斷開連接", "DeviceOnlineWithName": "{0} 已連接", "FailedLoginAttemptWithUserName": "{0} 登入失敗", From 20cf27f637af745827348b4853d1e91bdf29eb38 Mon Sep 17 00:00:00 2001 From: Penelope Gwen / Pogmommy <support@pogmom.me> Date: Thu, 20 Apr 2023 07:39:34 -0600 Subject: [PATCH 254/858] Add artist to artist split whitelist (#9659) --- MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index dce3f0e39d..55ffb1f0de 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -68,6 +68,7 @@ namespace MediaBrowser.MediaEncoding.Probing "諭吉佳作/men", "//dARTH nULL", "Phantom/Ghost", + "She/Her/Hers", }; public MediaInfo GetMediaInfo(InternalMediaInfoResult data, VideoType? videoType, bool isAudio, string path, MediaProtocol protocol) From 858dadcdd1caadb5fa8cc13a02eb227098f39c3c Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Fri, 14 Apr 2023 13:43:56 +0200 Subject: [PATCH 255/858] POC sql connection pool --- .../ApplicationHost.cs | 6 +- .../Data/BaseSqliteRepository.cs | 115 +++++++++++++----- .../Data/ConnectionPool.cs | 68 +++++++++++ .../Data/ManagedConnection.cs | 13 +- .../Data/SqliteItemRepository.cs | 9 +- .../Data/SqliteUserDataRepository.cs | 38 ++---- 6 files changed, 174 insertions(+), 75 deletions(-) create mode 100644 Emby.Server.Implementations/Data/ConnectionPool.cs diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 080c448299..7969577bc0 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -627,6 +627,9 @@ namespace Emby.Server.Implementations } } + ((SqliteItemRepository)Resolve<IItemRepository>()).Initialize(); + ((SqliteUserDataRepository)Resolve<IUserDataRepository>()).Initialize(); + var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>(); await localizationManager.LoadAll().ConfigureAwait(false); @@ -634,9 +637,6 @@ namespace Emby.Server.Implementations SetStaticProperties(); - var userDataRepo = (SqliteUserDataRepository)Resolve<IUserDataRepository>(); - ((SqliteItemRepository)Resolve<IItemRepository>()).Initialize(userDataRepo, Resolve<IUserManager>()); - FindParts(); } diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs index bc520b86e7..859a3c7464 100644 --- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs +++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; -using System.Threading; using Jellyfin.Extensions; using Microsoft.Extensions.Logging; using SQLitePCL.pretty; @@ -27,9 +26,19 @@ namespace Emby.Server.Implementations.Data /// <summary> /// Gets or sets the path to the DB file. /// </summary> - /// <value>Path to the DB file.</value> protected string DbFilePath { get; set; } + /// <summary> + /// Gets or sets the number of write connections to create. + /// </summary> + /// <value>Path to the DB file.</value> + protected int WriteConnectionsCount { get; set; } = 1; + + /// <summary> + /// Gets or sets the number of read connections to create. + /// </summary> + protected int ReadConnectionsCount { get; set; } = 1; + /// <summary> /// Gets the logger. /// </summary> @@ -63,7 +72,7 @@ namespace Emby.Server.Implementations.Data /// <summary> /// Gets the locking mode. <see href="https://www.sqlite.org/pragma.html#pragma_locking_mode" />. /// </summary> - protected virtual string LockingMode => "EXCLUSIVE"; + protected virtual string LockingMode => "NORMAL"; /// <summary> /// Gets the journal mode. <see href="https://www.sqlite.org/pragma.html#pragma_journal_mode" />. @@ -88,7 +97,7 @@ namespace Emby.Server.Implementations.Data /// </summary> /// <value>The temp store mode.</value> /// <see cref="TempStoreMode"/> - protected virtual TempStoreMode TempStore => TempStoreMode.Default; + protected virtual TempStoreMode TempStore => TempStoreMode.Memory; /// <summary> /// Gets the synchronous mode. @@ -101,63 +110,115 @@ namespace Emby.Server.Implementations.Data /// Gets or sets the write lock. /// </summary> /// <value>The write lock.</value> - protected SemaphoreSlim WriteLock { get; set; } = new SemaphoreSlim(1, 1); + protected ConnectionPool WriteConnections { get; set; } /// <summary> /// Gets or sets the write connection. /// </summary> /// <value>The write connection.</value> - protected SQLiteDatabaseConnection WriteConnection { get; set; } + protected ConnectionPool ReadConnections { get; set; } + + public virtual void Initialize() + { + WriteConnections = new ConnectionPool(WriteConnectionsCount, CreateWriteConnection); + ReadConnections = new ConnectionPool(ReadConnectionsCount, CreateReadConnection); + } protected ManagedConnection GetConnection(bool readOnly = false) { - WriteLock.Wait(); - if (WriteConnection is not null) + if (readOnly) { - return new ManagedConnection(WriteConnection, WriteLock); + return ReadConnections.GetConnection(); } - WriteConnection = SQLite3.Open( + return WriteConnections.GetConnection(); + } + + protected SQLiteDatabaseConnection CreateWriteConnection() + { + var writeConnection = SQLite3.Open( DbFilePath, DefaultConnectionFlags | ConnectionFlags.Create | ConnectionFlags.ReadWrite, null); if (CacheSize.HasValue) { - WriteConnection.Execute("PRAGMA cache_size=" + CacheSize.Value); + writeConnection.Execute("PRAGMA cache_size=" + CacheSize.Value); } if (!string.IsNullOrWhiteSpace(LockingMode)) { - WriteConnection.Execute("PRAGMA locking_mode=" + LockingMode); + writeConnection.Execute("PRAGMA locking_mode=" + LockingMode); } if (!string.IsNullOrWhiteSpace(JournalMode)) { - WriteConnection.Execute("PRAGMA journal_mode=" + JournalMode); + writeConnection.Execute("PRAGMA journal_mode=" + JournalMode); } if (JournalSizeLimit.HasValue) { - WriteConnection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value); + writeConnection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value); } if (Synchronous.HasValue) { - WriteConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value); + writeConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value); } if (PageSize.HasValue) { - WriteConnection.Execute("PRAGMA page_size=" + PageSize.Value); + writeConnection.Execute("PRAGMA page_size=" + PageSize.Value); } - WriteConnection.Execute("PRAGMA temp_store=" + (int)TempStore); + writeConnection.Execute("PRAGMA temp_store=" + (int)TempStore); // Configuration and pragmas can affect VACUUM so it needs to be last. - WriteConnection.Execute("VACUUM"); + writeConnection.Execute("VACUUM"); - return new ManagedConnection(WriteConnection, WriteLock); + return writeConnection; + } + + protected SQLiteDatabaseConnection CreateReadConnection() + { + var writeConnection = SQLite3.Open( + DbFilePath, + DefaultConnectionFlags | ConnectionFlags.ReadOnly, + null); + + if (CacheSize.HasValue) + { + writeConnection.Execute("PRAGMA cache_size=" + CacheSize.Value); + } + + if (!string.IsNullOrWhiteSpace(LockingMode)) + { + writeConnection.Execute("PRAGMA locking_mode=" + LockingMode); + } + + if (!string.IsNullOrWhiteSpace(JournalMode)) + { + writeConnection.Execute("PRAGMA journal_mode=" + JournalMode); + } + + if (JournalSizeLimit.HasValue) + { + writeConnection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value); + } + + if (Synchronous.HasValue) + { + writeConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value); + } + + if (PageSize.HasValue) + { + writeConnection.Execute("PRAGMA page_size=" + PageSize.Value); + } + + writeConnection.Execute("PRAGMA temp_store=" + (int)TempStore); + + return writeConnection; } public IStatement PrepareStatement(ManagedConnection connection, string sql) @@ -240,22 +301,10 @@ namespace Emby.Server.Implementations.Data if (dispose) { - WriteLock.Wait(); - try - { - WriteConnection?.Dispose(); - } - finally - { - WriteLock.Release(); - } - - WriteLock.Dispose(); + WriteConnections.Dispose(); + ReadConnections.Dispose(); } - WriteConnection = null; - WriteLock = null; - _disposed = true; } } diff --git a/Emby.Server.Implementations/Data/ConnectionPool.cs b/Emby.Server.Implementations/Data/ConnectionPool.cs new file mode 100644 index 0000000000..86a125ba51 --- /dev/null +++ b/Emby.Server.Implementations/Data/ConnectionPool.cs @@ -0,0 +1,68 @@ +#pragma warning disable CS1591 + +using System; +using System.Collections.Concurrent; +using System.Threading; +using SQLitePCL.pretty; + +namespace Emby.Server.Implementations.Data; + +public sealed class ConnectionPool : IDisposable +{ + private readonly int _count; + private readonly SemaphoreSlim _lock; + private readonly ConcurrentQueue<SQLiteDatabaseConnection> _connections = new ConcurrentQueue<SQLiteDatabaseConnection>(); + private bool _disposed; + + public ConnectionPool(int count, Func<SQLiteDatabaseConnection> factory) + { + _count = count; + _lock = new SemaphoreSlim(count, count); + for (int i = 0; i < count; i++) + { + _connections.Enqueue(factory.Invoke()); + } + } + + public ManagedConnection GetConnection() + { + _lock.Wait(); + if (!_connections.TryDequeue(out var connection)) + { + _lock.Release(); + throw new InvalidOperationException(); + } + + return new ManagedConnection(connection, this); + } + + public void Return(SQLiteDatabaseConnection connection) + { + _connections.Enqueue(connection); + _lock.Release(); + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + for (int i = 0; i < _count; i++) + { + _lock.Wait(); + if (!_connections.TryDequeue(out var connection)) + { + _lock.Release(); + throw new InvalidOperationException(); + } + + connection.Dispose(); + } + + _lock.Dispose(); + + _disposed = true; + } +} diff --git a/Emby.Server.Implementations/Data/ManagedConnection.cs b/Emby.Server.Implementations/Data/ManagedConnection.cs index 11e33278d4..e84ed8f918 100644 --- a/Emby.Server.Implementations/Data/ManagedConnection.cs +++ b/Emby.Server.Implementations/Data/ManagedConnection.cs @@ -2,23 +2,22 @@ using System; using System.Collections.Generic; -using System.Threading; using SQLitePCL.pretty; namespace Emby.Server.Implementations.Data { public sealed class ManagedConnection : IDisposable { - private readonly SemaphoreSlim _writeLock; + private readonly ConnectionPool _pool; - private SQLiteDatabaseConnection? _db; + private SQLiteDatabaseConnection _db; private bool _disposed = false; - public ManagedConnection(SQLiteDatabaseConnection db, SemaphoreSlim writeLock) + public ManagedConnection(SQLiteDatabaseConnection db, ConnectionPool pool) { _db = db; - _writeLock = writeLock; + _pool = pool; } public IStatement PrepareStatement(string sql) @@ -73,9 +72,9 @@ namespace Emby.Server.Implementations.Data return; } - _writeLock.Release(); + _pool.Return(_db); - _db = null; // Don't dispose it + _db = null!; // Don't dispose it _disposed = true; } } diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index e59f2a198e..33466c34b0 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -336,6 +336,7 @@ namespace Emby.Server.Implementations.Data _jsonOptions = JsonDefaults.Options; DbFilePath = Path.Combine(_config.ApplicationPaths.DataPath, "library.db"); + ReadConnectionsCount = 5; } /// <inheritdoc /> @@ -347,10 +348,10 @@ namespace Emby.Server.Implementations.Data /// <summary> /// Opens the connection to the database. /// </summary> - /// <param name="userDataRepo">The user data repository.</param> - /// <param name="userManager">The user manager.</param> - public void Initialize(SqliteUserDataRepository userDataRepo, IUserManager userManager) + public override void Initialize() { + base.Initialize(); + const string CreateMediaStreamsTableCommand = "create table if not exists mediastreams (ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, CodecTag TEXT NULL, Comment TEXT NULL, NalLengthSize TEXT NULL, IsAvc BIT NULL, Title TEXT NULL, TimeBase TEXT NULL, CodecTimeBase TEXT NULL, ColorPrimaries TEXT NULL, ColorSpace TEXT NULL, ColorTransfer TEXT NULL, DvVersionMajor INT NULL, DvVersionMinor INT NULL, DvProfile INT NULL, DvLevel INT NULL, RpuPresentFlag INT NULL, ElPresentFlag INT NULL, BlPresentFlag INT NULL, DvBlSignalCompatibilityId INT NULL, IsHearingImpaired BIT NULL, PRIMARY KEY (ItemId, StreamIndex))"; @@ -551,8 +552,6 @@ namespace Emby.Server.Implementations.Data connection.RunQueries(postQueries); } - - userDataRepo.Initialize(userManager, WriteLock, WriteConnection); } public void SaveImages(BaseItem item) diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs index 5f2c3c9dcc..a1e217ad14 100644 --- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs @@ -7,7 +7,7 @@ using System.Collections.Generic; using System.IO; using System.Threading; using Jellyfin.Data.Entities; -using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; @@ -18,33 +18,32 @@ namespace Emby.Server.Implementations.Data { public class SqliteUserDataRepository : BaseSqliteRepository, IUserDataRepository { + private readonly IUserManager _userManager; + public SqliteUserDataRepository( ILogger<SqliteUserDataRepository> logger, - IApplicationPaths appPaths) + IServerConfigurationManager config, + IUserManager userManager) : base(logger) { - DbFilePath = Path.Combine(appPaths.DataPath, "library.db"); + _userManager = userManager; + + DbFilePath = Path.Combine(config.ApplicationPaths.DataPath, "library.db"); } /// <summary> /// Opens the connection to the database. /// </summary> - /// <param name="userManager">The user manager.</param> - /// <param name="dbLock">The lock to use for database IO.</param> - /// <param name="dbConnection">The connection to use for database IO.</param> - public void Initialize(IUserManager userManager, SemaphoreSlim dbLock, SQLiteDatabaseConnection dbConnection) + public override void Initialize() { - WriteLock.Dispose(); - WriteLock = dbLock; - WriteConnection?.Dispose(); - WriteConnection = dbConnection; + base.Initialize(); using (var connection = GetConnection()) { var userDatasTableExists = TableExists(connection, "UserDatas"); var userDataTableExists = TableExists(connection, "userdata"); - var users = userDatasTableExists ? null : userManager.Users; + var users = userDatasTableExists ? null : _userManager.Users; connection.RunInTransaction( db => @@ -371,20 +370,5 @@ namespace Emby.Server.Implementations.Data return userData; } - -#pragma warning disable CA2215 - /// <inheritdoc/> - /// <remarks> - /// There is nothing to dispose here since <see cref="BaseSqliteRepository.WriteLock"/> and - /// <see cref="BaseSqliteRepository.WriteConnection"/> are managed by <see cref="SqliteItemRepository"/>. - /// See <see cref="Initialize(IUserManager, SemaphoreSlim, SQLiteDatabaseConnection)"/>. - /// </remarks> - protected override void Dispose(bool dispose) - { - // The write lock and connection for the item repository are shared with the user data repository - // since they point to the same database. The item repo has responsibility for disposing these two objects, - // so the user data repo should not attempt to dispose them as well - } -#pragma warning restore CA2215 } } From 33f97045f957179c907a22af96dac0261b3651d8 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Fri, 14 Apr 2023 21:38:12 +0200 Subject: [PATCH 256/858] Use BlockingCollection --- .../Data/BaseSqliteRepository.cs | 39 +++++++---------- .../Data/ConnectionPool.cs | 42 ++++++++----------- 2 files changed, 33 insertions(+), 48 deletions(-) diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs index 859a3c7464..ce0d03b2b5 100644 --- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs +++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs @@ -122,17 +122,16 @@ namespace Emby.Server.Implementations.Data { WriteConnections = new ConnectionPool(WriteConnectionsCount, CreateWriteConnection); ReadConnections = new ConnectionPool(ReadConnectionsCount, CreateReadConnection); + + // Configuration and pragmas can affect VACUUM so it needs to be last. + using (var connection = GetConnection(true)) + { + connection.Execute("VACUUM"); + } } protected ManagedConnection GetConnection(bool readOnly = false) - { - if (readOnly) - { - return ReadConnections.GetConnection(); - } - - return WriteConnections.GetConnection(); - } + => readOnly ? ReadConnections.GetConnection() : WriteConnections.GetConnection(); protected SQLiteDatabaseConnection CreateWriteConnection() { @@ -173,52 +172,44 @@ namespace Emby.Server.Implementations.Data writeConnection.Execute("PRAGMA temp_store=" + (int)TempStore); - // Configuration and pragmas can affect VACUUM so it needs to be last. - writeConnection.Execute("VACUUM"); - return writeConnection; } protected SQLiteDatabaseConnection CreateReadConnection() { - var writeConnection = SQLite3.Open( + var connection = SQLite3.Open( DbFilePath, DefaultConnectionFlags | ConnectionFlags.ReadOnly, null); if (CacheSize.HasValue) { - writeConnection.Execute("PRAGMA cache_size=" + CacheSize.Value); + connection.Execute("PRAGMA cache_size=" + CacheSize.Value); } if (!string.IsNullOrWhiteSpace(LockingMode)) { - writeConnection.Execute("PRAGMA locking_mode=" + LockingMode); + connection.Execute("PRAGMA locking_mode=" + LockingMode); } if (!string.IsNullOrWhiteSpace(JournalMode)) { - writeConnection.Execute("PRAGMA journal_mode=" + JournalMode); + connection.Execute("PRAGMA journal_mode=" + JournalMode); } if (JournalSizeLimit.HasValue) { - writeConnection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value); + connection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value); } if (Synchronous.HasValue) { - writeConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value); + connection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value); } - if (PageSize.HasValue) - { - writeConnection.Execute("PRAGMA page_size=" + PageSize.Value); - } + connection.Execute("PRAGMA temp_store=" + (int)TempStore); - writeConnection.Execute("PRAGMA temp_store=" + (int)TempStore); - - return writeConnection; + return connection; } public IStatement PrepareStatement(ManagedConnection connection, string sql) diff --git a/Emby.Server.Implementations/Data/ConnectionPool.cs b/Emby.Server.Implementations/Data/ConnectionPool.cs index 86a125ba51..091a1b74f3 100644 --- a/Emby.Server.Implementations/Data/ConnectionPool.cs +++ b/Emby.Server.Implementations/Data/ConnectionPool.cs @@ -2,44 +2,47 @@ using System; using System.Collections.Concurrent; -using System.Threading; using SQLitePCL.pretty; namespace Emby.Server.Implementations.Data; public sealed class ConnectionPool : IDisposable { - private readonly int _count; - private readonly SemaphoreSlim _lock; - private readonly ConcurrentQueue<SQLiteDatabaseConnection> _connections = new ConcurrentQueue<SQLiteDatabaseConnection>(); + private readonly BlockingCollection<SQLiteDatabaseConnection> _connections = new(); private bool _disposed; public ConnectionPool(int count, Func<SQLiteDatabaseConnection> factory) { - _count = count; - _lock = new SemaphoreSlim(count, count); for (int i = 0; i < count; i++) { - _connections.Enqueue(factory.Invoke()); + _connections.Add(factory.Invoke()); } } public ManagedConnection GetConnection() { - _lock.Wait(); - if (!_connections.TryDequeue(out var connection)) + if (_disposed) { - _lock.Release(); - throw new InvalidOperationException(); + ThrowObjectDisposedException(); } - return new ManagedConnection(connection, this); + return new ManagedConnection(_connections.Take(), this); + + void ThrowObjectDisposedException() + { + throw new ObjectDisposedException(GetType().Name); + } } public void Return(SQLiteDatabaseConnection connection) { - _connections.Enqueue(connection); - _lock.Release(); + if (_disposed) + { + connection.Dispose(); + return; + } + + _connections.Add(connection); } public void Dispose() @@ -49,20 +52,11 @@ public sealed class ConnectionPool : IDisposable return; } - for (int i = 0; i < _count; i++) + foreach (var connection in _connections) { - _lock.Wait(); - if (!_connections.TryDequeue(out var connection)) - { - _lock.Release(); - throw new InvalidOperationException(); - } - connection.Dispose(); } - _lock.Dispose(); - _disposed = true; } } From 8dba3a44fd3f4771acb0cade2c91696179ad21d9 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Fri, 14 Apr 2023 22:43:14 +0200 Subject: [PATCH 257/858] Get write connection for vacuum --- Emby.Server.Implementations/Data/BaseSqliteRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs index ce0d03b2b5..4b9ab53ae4 100644 --- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs +++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs @@ -124,7 +124,7 @@ namespace Emby.Server.Implementations.Data ReadConnections = new ConnectionPool(ReadConnectionsCount, CreateReadConnection); // Configuration and pragmas can affect VACUUM so it needs to be last. - using (var connection = GetConnection(true)) + using (var connection = GetConnection()) { connection.Execute("VACUUM"); } From 13152bf09dcadf8807e1240be7ca84662d2ed397 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Fri, 21 Apr 2023 14:05:27 +0200 Subject: [PATCH 258/858] Change number of read connections based on # of threads and add comments --- .../Data/ConnectionPool.cs | 19 +++++++++++++++++-- .../Data/SqliteItemRepository.cs | 2 +- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Data/ConnectionPool.cs b/Emby.Server.Implementations/Data/ConnectionPool.cs index 091a1b74f3..6d28a5e43d 100644 --- a/Emby.Server.Implementations/Data/ConnectionPool.cs +++ b/Emby.Server.Implementations/Data/ConnectionPool.cs @@ -1,16 +1,22 @@ -#pragma warning disable CS1591 - using System; using System.Collections.Concurrent; using SQLitePCL.pretty; namespace Emby.Server.Implementations.Data; +/// <summary> +/// A pool of SQLite Database connections. +/// </summary> public sealed class ConnectionPool : IDisposable { private readonly BlockingCollection<SQLiteDatabaseConnection> _connections = new(); private bool _disposed; + /// <summary> + /// Initializes a new instance of the <see cref="ConnectionPool" /> class. + /// </summary> + /// <param name="count">The number of database connection to create.</param> + /// <param name="factory">Factory function to create the database connections.</param> public ConnectionPool(int count, Func<SQLiteDatabaseConnection> factory) { for (int i = 0; i < count; i++) @@ -19,6 +25,10 @@ public sealed class ConnectionPool : IDisposable } } + /// <summary> + /// Gets a database connection from the pool if one is available, otherwise blocks. + /// </summary> + /// <returns>A database connection.</returns> public ManagedConnection GetConnection() { if (_disposed) @@ -34,6 +44,10 @@ public sealed class ConnectionPool : IDisposable } } + /// <summary> + /// Return a database connection to the pool. + /// </summary> + /// <param name="connection">The database connection to return.</param> public void Return(SQLiteDatabaseConnection connection) { if (_disposed) @@ -45,6 +59,7 @@ public sealed class ConnectionPool : IDisposable _connections.Add(connection); } + /// <inheritdoc /> public void Dispose() { if (_disposed) diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 33466c34b0..22d485d335 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -336,7 +336,7 @@ namespace Emby.Server.Implementations.Data _jsonOptions = JsonDefaults.Options; DbFilePath = Path.Combine(_config.ApplicationPaths.DataPath, "library.db"); - ReadConnectionsCount = 5; + ReadConnectionsCount = Environment.ProcessorCount * 2; } /// <inheritdoc /> From b031448ef52414b348eab897bd7371ad9e614bb3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 21 Apr 2023 19:09:45 +0000 Subject: [PATCH 259/858] chore(deps): update github/codeql-action action to v2.3.0 --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 05773d1d99..88c8dd5af0 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@7df0ce34898d659f95c0c4a09eaa8d4e32ee64db # v2.2.12 + uses: github/codeql-action/init@b2c19fb9a2a485599ccf4ed5d65527d94bc57226 # v2.3.0 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@7df0ce34898d659f95c0c4a09eaa8d4e32ee64db # v2.2.12 + uses: github/codeql-action/autobuild@b2c19fb9a2a485599ccf4ed5d65527d94bc57226 # v2.3.0 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@7df0ce34898d659f95c0c4a09eaa8d4e32ee64db # v2.2.12 + uses: github/codeql-action/analyze@b2c19fb9a2a485599ccf4ed5d65527d94bc57226 # v2.3.0 From 0d67901e373ff8a2ab279d51c3c1f88adf68bfcf Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Mon, 24 Apr 2023 13:08:46 +0200 Subject: [PATCH 260/858] Dispose BlockingCollection --- Emby.Server.Implementations/Data/ConnectionPool.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Emby.Server.Implementations/Data/ConnectionPool.cs b/Emby.Server.Implementations/Data/ConnectionPool.cs index 6d28a5e43d..a671eb165d 100644 --- a/Emby.Server.Implementations/Data/ConnectionPool.cs +++ b/Emby.Server.Implementations/Data/ConnectionPool.cs @@ -72,6 +72,8 @@ public sealed class ConnectionPool : IDisposable connection.Dispose(); } + _connections.Dispose(); + _disposed = true; } } From 29889159e80f66bcecd5403a756c5c853cae1a3b Mon Sep 17 00:00:00 2001 From: Mark Lopez <m@silvenga.com> Date: Sat, 22 Apr 2023 09:18:12 -0500 Subject: [PATCH 261/858] Increased the max journal_size_limit to reduce the number of truncation operations. --- Emby.Server.Implementations/Data/BaseSqliteRepository.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs index bc520b86e7..fa58249ec2 100644 --- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs +++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs @@ -73,9 +73,10 @@ namespace Emby.Server.Implementations.Data /// <summary> /// Gets the journal size limit. <see href="https://www.sqlite.org/pragma.html#pragma_journal_size_limit" />. + /// The default (-1) is overriden to prevent unconstrained WAL size, as reported by users. /// </summary> /// <value>The journal size limit.</value> - protected virtual int? JournalSizeLimit => 0; + protected virtual int? JournalSizeLimit => 134_217_728; // 128MiB /// <summary> /// Gets the page size. From a7054df54b3acc3c9b7d8790a68605596f8df658 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 27 Apr 2023 13:06:02 +0000 Subject: [PATCH 262/858] chore(deps): update dependency efcoresecondlevelcacheinterceptor to v3.9.0 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index f35ffb062c..b37460bac0 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -17,7 +17,7 @@ <PackageVersion Include="Diacritics" Version="3.3.18" /> <PackageVersion Include="DiscUtils.Udf" Version="0.16.13" /> <PackageVersion Include="DotNet.Glob" Version="3.1.3" /> - <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.8.8" /> + <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.9.0" /> <PackageVersion Include="FsCheck.Xunit" Version="2.16.5" /> <PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" /> <PackageVersion Include="libse" Version="3.6.11" /> From 427b42c5bb2b2daccdf6900d6b47c73bee822489 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 27 Apr 2023 07:43:57 -0600 Subject: [PATCH 263/858] chore(deps): update ci dependencies (#9684) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 6 +++--- .github/workflows/openapi.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 88c8dd5af0..2f91d40892 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@b2c19fb9a2a485599ccf4ed5d65527d94bc57226 # v2.3.0 + uses: github/codeql-action/init@8662eabe0e9f338a07350b7fd050732745f93848 # v2.3.1 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@b2c19fb9a2a485599ccf4ed5d65527d94bc57226 # v2.3.0 + uses: github/codeql-action/autobuild@8662eabe0e9f338a07350b7fd050732745f93848 # v2.3.1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@b2c19fb9a2a485599ccf4ed5d65527d94bc57226 # v2.3.0 + uses: github/codeql-action/analyze@8662eabe0e9f338a07350b7fd050732745f93848 # v2.3.1 diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index 79c001e8b2..d87eccb9fb 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -103,7 +103,7 @@ jobs: body="${body//$'\r'/'%0D'}" echo ::set-output name=body::$body - name: Find difference comment - uses: peter-evans/find-comment@034abe94d3191f9c89d870519735beae326f2bdb # v2.3.0 + uses: peter-evans/find-comment@a54c31d7fa095754bfef525c0c8e5e5674c4b4b1 # v2.4.0 id: find-comment with: issue-number: ${{ github.event.pull_request.number }} From 73d69279eff8302d570f1849253297f5159ef2ef Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 28 Apr 2023 01:00:44 +0000 Subject: [PATCH 264/858] chore(deps): update github/codeql-action action to v2.3.2 --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 2f91d40892..f605388720 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@8662eabe0e9f338a07350b7fd050732745f93848 # v2.3.1 + uses: github/codeql-action/init@f3feb00acb00f31a6f60280e6ace9ca31d91c76a # v2.3.2 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@8662eabe0e9f338a07350b7fd050732745f93848 # v2.3.1 + uses: github/codeql-action/autobuild@f3feb00acb00f31a6f60280e6ace9ca31d91c76a # v2.3.2 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@8662eabe0e9f338a07350b7fd050732745f93848 # v2.3.1 + uses: github/codeql-action/analyze@f3feb00acb00f31a6f60280e6ace9ca31d91c76a # v2.3.2 From a59ae91d5d679044d17759fc2721bf381c1a5262 Mon Sep 17 00:00:00 2001 From: Michael Fuchs <fooxl@posteo.at> Date: Sat, 29 Apr 2023 13:47:09 +0200 Subject: [PATCH 265/858] Add artist to artist split whitelist # Changes Added an artist whose name has forward slashes in it to the artist "/" split whitelist # Issues Related to issue #2305 --- MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 55ffb1f0de..c6d8a4ccee 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -69,6 +69,7 @@ namespace MediaBrowser.MediaEncoding.Probing "//dARTH nULL", "Phantom/Ghost", "She/Her/Hers", + "5/8erl in Ehr'n", }; public MediaInfo GetMediaInfo(InternalMediaInfoResult data, VideoType? videoType, bool isAudio, string path, MediaProtocol protocol) From 2c4f7ec52c3f99143f0d316e791b03bded17d714 Mon Sep 17 00:00:00 2001 From: Oskari Lavinto <olavinto@protonmail.com> Date: Sun, 30 Apr 2023 08:37:24 +0000 Subject: [PATCH 266/858] Translated using Weblate (Finnish) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fi/ --- Emby.Server.Implementations/Localization/Core/fi.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json index ec72d58dd6..8672cfb9ff 100644 --- a/Emby.Server.Implementations/Localization/Core/fi.json +++ b/Emby.Server.Implementations/Localization/Core/fi.json @@ -118,7 +118,7 @@ "TaskCleanActivityLogDescription": "Poistaa määritettyä ikää vanhemmat tapahtumat toimintahistoriasta.", "TaskCleanActivityLog": "Tyhjennä toimintahistoria", "Undefined": "Määrittelemätön", - "TaskOptimizeDatabaseDescription": "Tiivistää ja puhdistaa tietokannan. Tämän toiminnon suorittaminen kirjastojen skannauksen tai muiden tietokantaan liittyvien muutoksien jälkeen voi parantaa suorituskykyä.", + "TaskOptimizeDatabaseDescription": "Tiivistää ja puhdistaa tietokannan. Tämän toiminnon suorittaminen kirjastopäivityksen tai muiden mahdollisten tietokantamuutosten jälkeen voi parantaa suorituskykyä.", "TaskOptimizeDatabase": "Optimoi tietokanta", "TaskKeyframeExtractorDescription": "Purkaa videotiedostojen avainkuvat tarkempien HLS-toistolistojen luomiseksi. Tehtävä saattaa kestää huomattavan pitkään.", "TaskKeyframeExtractor": "Avainkuvien purkain", From 08aac575290d0b94c0c4b7e6af6167867dc93efa Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 07:56:03 -0600 Subject: [PATCH 267/858] chore(deps): update dependency efcoresecondlevelcacheinterceptor to v3.9.1 (#9712) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index b37460bac0..817ae81757 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -17,7 +17,7 @@ <PackageVersion Include="Diacritics" Version="3.3.18" /> <PackageVersion Include="DiscUtils.Udf" Version="0.16.13" /> <PackageVersion Include="DotNet.Glob" Version="3.1.3" /> - <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.9.0" /> + <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.9.1" /> <PackageVersion Include="FsCheck.Xunit" Version="2.16.5" /> <PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" /> <PackageVersion Include="libse" Version="3.6.11" /> From 8bb44b85d7008ee167f2260934a1f325abb06e2d Mon Sep 17 00:00:00 2001 From: herby2212 <12448284+herby2212@users.noreply.github.com> Date: Mon, 1 May 2023 16:24:15 +0200 Subject: [PATCH 268/858] close inactive sessions after 10 minutes --- .../Session/SessionManager.cs | 76 +++++++++++++++++-- .../Session/SessionInfo.cs | 6 ++ 2 files changed, 77 insertions(+), 5 deletions(-) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 5f6dc93fb3..32eff350b1 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -63,6 +63,9 @@ namespace Emby.Server.Implementations.Session private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections = new(StringComparer.OrdinalIgnoreCase); private Timer _idleTimer; + private Timer _inactiveTimer; + + private int inactiveMinutesThreshold = 10; private DtoOptions _itemInfoDtoOptions; private bool _disposed = false; @@ -171,9 +174,11 @@ namespace Emby.Server.Implementations.Session if (disposing) { _idleTimer?.Dispose(); + _inactiveTimer?.Dispose(); } _idleTimer = null; + _inactiveTimer = null; _deviceManager.DeviceOptionsUpdated -= OnDeviceManagerDeviceOptionsUpdated; @@ -397,6 +402,15 @@ namespace Emby.Server.Implementations.Session session.LastPlaybackCheckIn = DateTime.UtcNow; } + if (info.IsPaused && session.LastPausedDate.HasValue == false) + { + session.LastPausedDate = DateTime.UtcNow; + } + else if (!info.IsPaused) + { + session.LastPausedDate = null; + } + session.PlayState.IsPaused = info.IsPaused; session.PlayState.PositionTicks = info.PositionTicks; session.PlayState.MediaSourceId = info.MediaSourceId; @@ -564,9 +578,10 @@ namespace Emby.Server.Implementations.Session return users; } - private void StartIdleCheckTimer() + private void StartCheckTimers() { _idleTimer ??= new Timer(CheckForIdlePlayback, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5)); + _inactiveTimer ??= new Timer(CheckForInactiveSteams, null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1)); } private void StopIdleCheckTimer() @@ -578,6 +593,15 @@ namespace Emby.Server.Implementations.Session } } + private void StopInactiveCheckTimer() + { + if (_inactiveTimer is not null) + { + _inactiveTimer.Dispose(); + _inactiveTimer = null; + } + } + private async void CheckForIdlePlayback(object state) { var playingSessions = Sessions.Where(i => i.NowPlayingItem is not null) @@ -613,13 +637,55 @@ namespace Emby.Server.Implementations.Session playingSessions = Sessions.Where(i => i.NowPlayingItem is not null) .ToList(); } - - if (playingSessions.Count == 0) + else { StopIdleCheckTimer(); } } + private async void CheckForInactiveSteams(object state) + { + var pausedSessions = Sessions.Where(i => + (i.NowPlayingItem is not null) && + (i.PlayState.IsPaused == true) && + (i.LastPausedDate is not null)).ToList(); + + if (pausedSessions.Count > 0) + { + var inactiveSessions = Sessions.Where(i => (DateTime.UtcNow - i.LastPausedDate).Value.TotalMinutes > inactiveMinutesThreshold).ToList(); + + foreach (var session in inactiveSessions) + { + _logger.LogDebug("Session {0} has been inactive for {1} minutes. Stopping it.", session.Id, inactiveMinutesThreshold); + + try + { + await SendPlaystateCommand( + session.Id, + session.Id, + new PlaystateRequest() + { + Command = PlaystateCommand.Stop, + ControllingUserId = session.UserId.ToString(), + SeekPositionTicks = session.PlayState?.PositionTicks + }, + CancellationToken.None).ConfigureAwait(true); + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Error calling SendPlaystateCommand for stopping inactive session {0}.", session.Id); + } + } + } + + var playingSessions = Sessions.Where(i => i.NowPlayingItem is not null) + .ToList(); + if (playingSessions.Count == 0) + { + StopInactiveCheckTimer(); + } + } + private BaseItem GetNowPlayingItem(SessionInfo session, Guid itemId) { var item = session.FullNowPlayingItem; @@ -696,7 +762,7 @@ namespace Emby.Server.Implementations.Session eventArgs, _logger); - StartIdleCheckTimer(); + StartCheckTimers(); } /// <summary> @@ -790,7 +856,7 @@ namespace Emby.Server.Implementations.Session session.StartAutomaticProgress(info); } - StartIdleCheckTimer(); + StartCheckTimers(); } private void OnPlaybackProgress(User user, BaseItem item, PlaybackProgressInfo info) diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs index 25bf23d61b..172d79a599 100644 --- a/MediaBrowser.Controller/Session/SessionInfo.cs +++ b/MediaBrowser.Controller/Session/SessionInfo.cs @@ -109,6 +109,12 @@ namespace MediaBrowser.Controller.Session /// <value>The last playback check in.</value> public DateTime LastPlaybackCheckIn { get; set; } + /// <summary> + /// Gets or sets the last paused date. + /// </summary> + /// <value>The last paused date.</value> + public DateTime? LastPausedDate { get; set; } + /// <summary> /// Gets or sets the name of the device. /// </summary> From e1190d15d6ca0b7cad2b4991e524b35ecca35949 Mon Sep 17 00:00:00 2001 From: herby2212 <12448284+herby2212@users.noreply.github.com> Date: Mon, 1 May 2023 20:11:22 +0200 Subject: [PATCH 269/858] option to disable and configure inactive session threshold --- .../Session/SessionManager.cs | 20 ++++++++++++++----- .../Configuration/ServerConfiguration.cs | 6 ++++++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 32eff350b1..056c3e4b61 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -19,6 +19,7 @@ using MediaBrowser.Common.Events; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller; using MediaBrowser.Controller.Authentication; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; @@ -46,6 +47,7 @@ namespace Emby.Server.Implementations.Session public class SessionManager : ISessionManager, IDisposable { private readonly IUserDataManager _userDataManager; + private readonly IServerConfigurationManager _config; private readonly ILogger<SessionManager> _logger; private readonly IEventManager _eventManager; private readonly ILibraryManager _libraryManager; @@ -65,8 +67,6 @@ namespace Emby.Server.Implementations.Session private Timer _idleTimer; private Timer _inactiveTimer; - private int inactiveMinutesThreshold = 10; - private DtoOptions _itemInfoDtoOptions; private bool _disposed = false; @@ -74,6 +74,7 @@ namespace Emby.Server.Implementations.Session ILogger<SessionManager> logger, IEventManager eventManager, IUserDataManager userDataManager, + IServerConfigurationManager config, ILibraryManager libraryManager, IUserManager userManager, IMusicManager musicManager, @@ -86,6 +87,7 @@ namespace Emby.Server.Implementations.Session _logger = logger; _eventManager = eventManager; _userDataManager = userDataManager; + _config = config; _libraryManager = libraryManager; _userManager = userManager; _musicManager = musicManager; @@ -581,7 +583,15 @@ namespace Emby.Server.Implementations.Session private void StartCheckTimers() { _idleTimer ??= new Timer(CheckForIdlePlayback, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5)); - _inactiveTimer ??= new Timer(CheckForInactiveSteams, null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1)); + + if (_config.Configuration.InactiveSessionThreshold > 0) + { + _inactiveTimer ??= new Timer(CheckForInactiveSteams, null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1)); + } + else + { + StopInactiveCheckTimer(); + } } private void StopIdleCheckTimer() @@ -652,11 +662,11 @@ namespace Emby.Server.Implementations.Session if (pausedSessions.Count > 0) { - var inactiveSessions = Sessions.Where(i => (DateTime.UtcNow - i.LastPausedDate).Value.TotalMinutes > inactiveMinutesThreshold).ToList(); + var inactiveSessions = pausedSessions.Where(i => (DateTime.UtcNow - i.LastPausedDate).Value.TotalMinutes > _config.Configuration.InactiveSessionThreshold).ToList(); foreach (var session in inactiveSessions) { - _logger.LogDebug("Session {0} has been inactive for {1} minutes. Stopping it.", session.Id, inactiveMinutesThreshold); + _logger.LogDebug("Session {0} has been inactive for {1} minutes. Stopping it.", session.Id, _config.Configuration.InactiveSessionThreshold); try { diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 07f02d1879..f619f384c6 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -157,6 +157,12 @@ namespace MediaBrowser.Model.Configuration /// <value>The remaining time in minutes.</value> public int MaxAudiobookResume { get; set; } = 5; + /// <summary> + /// Gets or sets the threshold in minutes after a inactive session gets closed automatically. + /// </summary> + /// <value>The close inactive session threshold in minutes.</value> + public int InactiveSessionThreshold { get; set; } = 10; + /// <summary> /// Gets or sets the delay in seconds that we will wait after a file system change to try and discover what has been added/removed /// Some delay is necessary with some items because their creation is not atomic. It involves the creation of several From 4cddd642964ca8ee996850a45bcca594191d2a34 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 2 May 2023 03:28:33 +0000 Subject: [PATCH 270/858] chore(deps): update peter-evans/create-or-update-comment action to v3.0.1 --- .github/workflows/commands.yml | 10 +++++----- .github/workflows/openapi.yml | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index 79b1c0e8c6..ae06c4141b 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Notify as seen - uses: peter-evans/create-or-update-comment@3383acd359705b10cb1eeef05c0e88c056ea4666 # v3.0.0 + uses: peter-evans/create-or-update-comment@ca08ebd5dc95aa0cd97021e9708fcd6b87138c9b # v3.0.1 with: token: ${{ secrets.JF_BOT_TOKEN }} comment-id: ${{ github.event.comment.id }} @@ -43,7 +43,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Notify as seen - uses: peter-evans/create-or-update-comment@3383acd359705b10cb1eeef05c0e88c056ea4666 # v3.0.0 + uses: peter-evans/create-or-update-comment@ca08ebd5dc95aa0cd97021e9708fcd6b87138c9b # v3.0.1 if: ${{ github.event.comment != null }} with: token: ${{ secrets.JF_BOT_TOKEN }} @@ -58,7 +58,7 @@ jobs: - name: Notify as running id: comment_running - uses: peter-evans/create-or-update-comment@3383acd359705b10cb1eeef05c0e88c056ea4666 # v3.0.0 + uses: peter-evans/create-or-update-comment@ca08ebd5dc95aa0cd97021e9708fcd6b87138c9b # v3.0.1 if: ${{ github.event.comment != null }} with: token: ${{ secrets.JF_BOT_TOKEN }} @@ -93,7 +93,7 @@ jobs: exit ${retcode} - name: Notify with result success - uses: peter-evans/create-or-update-comment@3383acd359705b10cb1eeef05c0e88c056ea4666 # v3.0.0 + uses: peter-evans/create-or-update-comment@ca08ebd5dc95aa0cd97021e9708fcd6b87138c9b # v3.0.1 if: ${{ github.event.comment != null && success() }} with: token: ${{ secrets.JF_BOT_TOKEN }} @@ -108,7 +108,7 @@ jobs: reactions: hooray - name: Notify with result failure - uses: peter-evans/create-or-update-comment@3383acd359705b10cb1eeef05c0e88c056ea4666 # v3.0.0 + uses: peter-evans/create-or-update-comment@ca08ebd5dc95aa0cd97021e9708fcd6b87138c9b # v3.0.1 if: ${{ github.event.comment != null && failure() }} with: token: ${{ secrets.JF_BOT_TOKEN }} diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index d87eccb9fb..e970bc7062 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -110,7 +110,7 @@ jobs: direction: last body-includes: openapi-diff-workflow-comment - name: Reply or edit difference comment (changed) - uses: peter-evans/create-or-update-comment@3383acd359705b10cb1eeef05c0e88c056ea4666 # v3.0.0 + uses: peter-evans/create-or-update-comment@ca08ebd5dc95aa0cd97021e9708fcd6b87138c9b # v3.0.1 if: ${{ steps.read-diff.outputs.body != '' }} with: issue-number: ${{ github.event.pull_request.number }} @@ -125,7 +125,7 @@ jobs: </details> - name: Edit difference comment (unchanged) - uses: peter-evans/create-or-update-comment@3383acd359705b10cb1eeef05c0e88c056ea4666 # v3.0.0 + uses: peter-evans/create-or-update-comment@ca08ebd5dc95aa0cd97021e9708fcd6b87138c9b # v3.0.1 if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }} with: issue-number: ${{ github.event.pull_request.number }} From 8b6fca59a1129c161e0b2ca44813921df20771b5 Mon Sep 17 00:00:00 2001 From: JPVenson <JPVenson@users.noreply.github.com> Date: Tue, 2 May 2023 14:26:55 +0200 Subject: [PATCH 271/858] Update Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs Co-authored-by: Cody Robibero <cody@robibe.ro> --- .../ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs index 83e07478d2..989c37242e 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs @@ -79,7 +79,7 @@ public class CleanupCollectionPathsTask : IScheduledTask var collection = collections[index]; _logger.LogDebug("Check Boxset {CollectionName}", collection.Name); var itemsToRemove = new List<LinkedChild>(); - foreach (var collectionLinkedChild in collection.LinkedChildren.ToArray()) + foreach (var collectionLinkedChild in collection.LinkedChildren) { if (!File.Exists(collectionLinkedChild.Path)) { From 42eae764a64ba0b7fa7bd85b036d23f0eece0c5a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 2 May 2023 15:06:13 +0000 Subject: [PATCH 272/858] chore(deps): update dependency sharpfuzz to v2.0.2 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 817ae81757..ee0b9a8dc8 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -64,7 +64,7 @@ <PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" /> <PackageVersion Include="Serilog.Sinks.Graylog" Version="2.3.0" /> <PackageVersion Include="SerilogAnalyzer" Version="0.15.0" /> - <PackageVersion Include="SharpFuzz" Version="2.0.1" /> + <PackageVersion Include="SharpFuzz" Version="2.0.2" /> <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.3" /> <PackageVersion Include="SkiaSharp.Svg" Version="1.60.0" /> <PackageVersion Include="SkiaSharp" Version="2.88.3" /> From 7b109572230e664b40b6271ee3a0fa7e760f1b4b Mon Sep 17 00:00:00 2001 From: RJS <zibenscilveeks@aicis.id.lv> Date: Tue, 2 May 2023 21:53:30 +0000 Subject: [PATCH 273/858] Translated using Weblate (Latvian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/lv/ --- Emby.Server.Implementations/Localization/Core/lv.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/lv.json b/Emby.Server.Implementations/Localization/Core/lv.json index e460fd7197..f753a369ab 100644 --- a/Emby.Server.Implementations/Localization/Core/lv.json +++ b/Emby.Server.Implementations/Localization/Core/lv.json @@ -120,5 +120,6 @@ "Default": "Noklusējuma", "TaskOptimizeDatabaseDescription": "Saspiež datubāzi un atbrīvo atmiņu. Uzdevum palaišana pēc bibliotēku skenēšanas vai citām, ar datubāzi saistītām, izmaiņām iespējams uzlabos ātrdarbību.", "TaskOptimizeDatabase": "Optimizēt datubāzi", - "External": "Ārējais" + "External": "Ārējais", + "HearingImpaired": "Ar dzirdes traucējumiem" } From 5e2a1fb4478d5324961a3f822a1057628e75d2a8 Mon Sep 17 00:00:00 2001 From: Meyu-Sys <sarveshkate0@gmail.com> Date: Tue, 2 May 2023 13:07:48 +0000 Subject: [PATCH 274/858] Translated using Weblate (Marathi) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/mr/ --- Emby.Server.Implementations/Localization/Core/mr.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/mr.json b/Emby.Server.Implementations/Localization/Core/mr.json index b2227e4543..a8fb26b91a 100644 --- a/Emby.Server.Implementations/Localization/Core/mr.json +++ b/Emby.Server.Implementations/Localization/Core/mr.json @@ -122,5 +122,6 @@ "External": "बाहेरचा", "DeviceOnlineWithName": "{0} कनेक्ट झाले", "DeviceOfflineWithName": "{0} डिस्कनेक्ट झाला आहे", - "AuthenticationSucceededWithUserName": "{0} यशस्वीरित्या प्रमाणीकृत" + "AuthenticationSucceededWithUserName": "{0} यशस्वीरित्या प्रमाणीकृत", + "HearingImpaired": "कर्णबधीर" } From 8e1f0d53c1efe286628a4119b3c595f219513d23 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Thu, 4 May 2023 14:42:39 +0200 Subject: [PATCH 275/858] nameof instead of GetType().Name --- Emby.Server.Implementations/Data/ConnectionPool.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Data/ConnectionPool.cs b/Emby.Server.Implementations/Data/ConnectionPool.cs index a671eb165d..5ea7e934ff 100644 --- a/Emby.Server.Implementations/Data/ConnectionPool.cs +++ b/Emby.Server.Implementations/Data/ConnectionPool.cs @@ -38,9 +38,9 @@ public sealed class ConnectionPool : IDisposable return new ManagedConnection(_connections.Take(), this); - void ThrowObjectDisposedException() + static void ThrowObjectDisposedException() { - throw new ObjectDisposedException(GetType().Name); + throw new ObjectDisposedException(nameof(ConnectionPool)); } } From ba336c25e1dba164eebf3839cc77a18cbc91d7e2 Mon Sep 17 00:00:00 2001 From: Joe Rogers <1337joe@gmail.com> Date: Thu, 4 May 2023 15:04:38 -0400 Subject: [PATCH 276/858] Whitelist Smith/Kotzen --- MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index c6d8a4ccee..7d655240b7 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -70,6 +70,7 @@ namespace MediaBrowser.MediaEncoding.Probing "Phantom/Ghost", "She/Her/Hers", "5/8erl in Ehr'n", + "Smith/Kotzen", }; public MediaInfo GetMediaInfo(InternalMediaInfoResult data, VideoType? videoType, bool isAudio, string path, MediaProtocol protocol) From d53eb51758ca472328372039930762b56b27c1c9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 4 May 2023 20:20:08 +0000 Subject: [PATCH 277/858] chore(deps): update github/codeql-action action to v2.3.3 --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index f605388720..8a806f9419 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@f3feb00acb00f31a6f60280e6ace9ca31d91c76a # v2.3.2 + uses: github/codeql-action/init@29b1f65c5e92e24fe6b6647da1eaabe529cec70f # v2.3.3 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@f3feb00acb00f31a6f60280e6ace9ca31d91c76a # v2.3.2 + uses: github/codeql-action/autobuild@29b1f65c5e92e24fe6b6647da1eaabe529cec70f # v2.3.3 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@f3feb00acb00f31a6f60280e6ace9ca31d91c76a # v2.3.2 + uses: github/codeql-action/analyze@29b1f65c5e92e24fe6b6647da1eaabe529cec70f # v2.3.3 From f2a41fe79fd6e6e42a43eebb8f2ea978c6b7e023 Mon Sep 17 00:00:00 2001 From: Alan Azar <alanazar304@hotmail.com> Date: Fri, 5 May 2023 11:10:03 +0000 Subject: [PATCH 278/858] Translated using Weblate (Urdu (Pakistan)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ur_PK/ --- Emby.Server.Implementations/Localization/Core/ur_PK.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/ur_PK.json b/Emby.Server.Implementations/Localization/Core/ur_PK.json index 7fe0c4c4be..5d3f194329 100644 --- a/Emby.Server.Implementations/Localization/Core/ur_PK.json +++ b/Emby.Server.Implementations/Localization/Core/ur_PK.json @@ -12,7 +12,7 @@ "HeaderContinueWatching": "دیکھنا جاری رکھیں", "Playlists": "پلے لسٹس", "ValueSpecialEpisodeName": "خصوصی - {0}", - "Shows": "دکھاتا ہے۔", + "Shows": "دکھاتا ہے", "Genres": "انواع", "Artists": "فنکار", "Sync": "مطابقت پذیری", @@ -123,5 +123,5 @@ "TaskCleanActivityLogDescription": "تشکیل شدہ عمر سے زیادہ پرانی سرگرمی لاگ اندراجات کو حذف کرتا ہے۔", "External": "بیرونی", "HearingImpaired": "قوت سماعت سے محروم", - "TaskCleanActivityLog": "سرگرمی لاگ کو صاف کریں۔" + "TaskCleanActivityLog": "سرگرمی لاگ کو صاف کریں" } From fa0a48b54eab58d77fce4b0e3653f88633944342 Mon Sep 17 00:00:00 2001 From: Subham Jena <subhamjena8465@gmail.com> Date: Sat, 6 May 2023 01:42:35 -0400 Subject: [PATCH 279/858] Added translation using Weblate (Odia) --- Emby.Server.Implementations/Localization/Core/or.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 Emby.Server.Implementations/Localization/Core/or.json diff --git a/Emby.Server.Implementations/Localization/Core/or.json b/Emby.Server.Implementations/Localization/Core/or.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/or.json @@ -0,0 +1 @@ +{} From 9ce152ecf6159a77e39c7231f8a6b4f77bacd65e Mon Sep 17 00:00:00 2001 From: TheSharingBrother <thesharingbrother@proton.me> Date: Sat, 6 May 2023 15:53:18 +0000 Subject: [PATCH 280/858] Translated using Weblate (Hindi) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hi/ --- .../Localization/Core/hi.json | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/hi.json b/Emby.Server.Implementations/Localization/Core/hi.json index cc0a926d03..4bbb1dd352 100644 --- a/Emby.Server.Implementations/Localization/Core/hi.json +++ b/Emby.Server.Implementations/Localization/Core/hi.json @@ -92,6 +92,17 @@ "System": "प्रणाली", "TvShows": "टीवी शो", "HearingImpaired": "मूक बधिर", - "ValueSpecialEpisodeName": "विशेष", - "TasksMaintenanceCategory": "रखरखाव" + "ValueSpecialEpisodeName": "विशेष - {0}", + "TasksMaintenanceCategory": "रखरखाव", + "Sync": "समाकलयति", + "VersionNumber": "{0} पाठान्तर", + "ValueHasBeenAddedToLibrary": "{0} आपके माध्यम ग्रन्थालय में उपजात हो गया हैं", + "TasksLibraryCategory": "संग्रहालय", + "TaskOptimizeDatabase": "जानकारी प्रवृद्धि", + "TaskDownloadMissingSubtitles": "असमेत अनुलेख को अवाहरति करें", + "TaskRefreshLibrary": "माध्यम संग्राहत को छाने", + "TaskCleanActivityLog": "क्रियाकलाप लॉग साफ करें", + "TasksChannelsCategory": "इंटरनेट प्रणाली", + "TasksApplicationCategory": "अनुप्रयोग", + "TaskRefreshPeople": "लोगोकी जानकारी ताज़ी करें" } From 898a04c9e31a77f74bd69d78896887f4f11ba1a5 Mon Sep 17 00:00:00 2001 From: Brett Healey <Bearach.goll@gmail.com> Date: Sat, 6 May 2023 18:29:05 +0000 Subject: [PATCH 281/858] Translated using Weblate (Welsh) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/cy/ --- Emby.Server.Implementations/Localization/Core/cy.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/cy.json b/Emby.Server.Implementations/Localization/Core/cy.json index 331c3d678f..794a8e4ce4 100644 --- a/Emby.Server.Implementations/Localization/Core/cy.json +++ b/Emby.Server.Implementations/Localization/Core/cy.json @@ -28,7 +28,7 @@ "NameSeasonNumber": "Tymor {0}", "MusicVideos": "Fideos Cerddoriaeth", "MixedContent": "Cynnwys amrywiol", - "HomeVideos": "Fideos Cartref", + "HomeVideos": "Genres", "HeaderNextUp": "Nesaf i Fyny", "HeaderFavoriteArtists": "Ffefryn Artistiaid", "HeaderFavoriteAlbums": "Ffefryn Albwmau", @@ -122,5 +122,6 @@ "TaskRefreshChapterImagesDescription": "Creu mân-luniau ar gyfer fideos sydd â phenodau.", "TaskRefreshChapterImages": "Echdynnu Lluniau Pennod", "TaskCleanCacheDescription": "Dileu ffeiliau cache nad oes eu hangen ar y system mwyach.", - "TaskCleanCache": "Gwaghau Ffolder Cache" + "TaskCleanCache": "Gwaghau Ffolder Cache", + "HearingImpaired": "Nam ar y clyw" } From bceca0ae45012fa6fbb13b793559779a4a32910a Mon Sep 17 00:00:00 2001 From: Subham Jena <subhamjena8465@gmail.com> Date: Sat, 6 May 2023 05:43:29 +0000 Subject: [PATCH 282/858] Translated using Weblate (Odia) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/or/ --- Emby.Server.Implementations/Localization/Core/or.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/or.json b/Emby.Server.Implementations/Localization/Core/or.json index 0967ef424b..0e9d81ee87 100644 --- a/Emby.Server.Implementations/Localization/Core/or.json +++ b/Emby.Server.Implementations/Localization/Core/or.json @@ -1 +1,4 @@ -{} +{ + "External": "ବହିଃସ୍ଥ", + "Genres": "ଧରଣ" +} From fbc039b14bb89e57d6637c5e16c6afed59825e2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Weyhm=C3=BCller?= <o.weyhm@gmx.de> Date: Sun, 7 May 2023 21:20:44 +0200 Subject: [PATCH 283/858] Fix scaleFactor limitation to 1 introduced by pull request #9485 --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 70018bb365..920925fc68 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -2148,7 +2148,7 @@ namespace MediaBrowser.Controller.MediaEncoding var outputScaleFactor = GetVideoBitrateScaleFactor(outputVideoCodec); // Don't scale the real bitrate lower than the requested bitrate - var scaleFactor = Math.Min(outputScaleFactor / inputScaleFactor, 1); + var scaleFactor = Math.Max(outputScaleFactor / inputScaleFactor, 1); if (bitrate <= 500000) { From 92a0d26f31743ca0015fcc3e0a4fe094792ac63c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=84=A1=E6=83=85=E5=A4=A9?= <kofzhanganguo@126.com> Date: Mon, 8 May 2023 19:19:12 +0000 Subject: [PATCH 284/858] Translated using Weblate (Chinese (Simplified)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hans/ --- Emby.Server.Implementations/Localization/Core/zh-CN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json index ccfbeef0ce..03265d3fb0 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-CN.json +++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json @@ -14,7 +14,7 @@ "FailedLoginAttemptWithUserName": "从 {0} 尝试登录失败", "Favorites": "我的最爱", "Folders": "文件夹", - "Genres": "风格", + "Genres": "类型", "HeaderAlbumArtists": "专辑艺术家", "HeaderContinueWatching": "继续观看", "HeaderFavoriteAlbums": "收藏的专辑", From b42a6119f56d3201ac402c834faeae73f52cf992 Mon Sep 17 00:00:00 2001 From: SuperDumbTM <eltonlochunkit@gmail.com> Date: Tue, 9 May 2023 05:24:04 +0000 Subject: [PATCH 285/858] Translated using Weblate (Chinese (Traditional, Hong Kong)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant_HK/ --- Emby.Server.Implementations/Localization/Core/zh-HK.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json index 610f503ddb..e8b8c2c5fa 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -15,7 +15,7 @@ "Favorites": "我的最愛", "Folders": "資料夾", "Genres": "風格", - "HeaderAlbumArtists": "專輯藝人", + "HeaderAlbumArtists": "專輯歌手", "HeaderContinueWatching": "繼續觀看", "HeaderFavoriteAlbums": "最愛的專輯", "HeaderFavoriteArtists": "最愛的藝人", From b3b75d5bc232a0e912f4ded8e3588e2c347a51de Mon Sep 17 00:00:00 2001 From: garikaib <garikaib@gmail.com> Date: Wed, 10 May 2023 01:01:16 -0400 Subject: [PATCH 286/858] Added translation using Weblate (Shona) --- Emby.Server.Implementations/Localization/Core/sn.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 Emby.Server.Implementations/Localization/Core/sn.json diff --git a/Emby.Server.Implementations/Localization/Core/sn.json b/Emby.Server.Implementations/Localization/Core/sn.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/sn.json @@ -0,0 +1 @@ +{} From f0bf5c4998ca2f8bedd61a750dc5228e2787f21e Mon Sep 17 00:00:00 2001 From: garikaib <garikaib@gmail.com> Date: Wed, 10 May 2023 05:10:35 +0000 Subject: [PATCH 287/858] Translated using Weblate (Shona) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/sn/ --- .../Localization/Core/sn.json | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/sn.json b/Emby.Server.Implementations/Localization/Core/sn.json index 0967ef424b..74720e7646 100644 --- a/Emby.Server.Implementations/Localization/Core/sn.json +++ b/Emby.Server.Implementations/Localization/Core/sn.json @@ -1 +1,28 @@ -{} +{ + "HeaderAlbumArtists": "Vaimbi vemadambarefu", + "HeaderContinueWatching": "Simudzira kuona", + "HeaderFavoriteSongs": "Nziyo dzaunofarira", + "Albums": "Dambarefu", + "AppDeviceValues": "Apu: {0}, Dhivhaisi: {1}", + "Application": "Purogiramu", + "Artists": "Vaimbi", + "AuthenticationSucceededWithUserName": "apinda", + "Books": "Mabhuku", + "CameraImageUploadedFrom": "Mufananidzo mutsva vabva pakamera {0}", + "Channels": "Machanewo", + "ChapterNameValue": "Chikamu {0}", + "Collections": "Akafanana", + "Default": "Zvakasarudzwa Kare", + "DeviceOfflineWithName": "{0} haasisipo", + "DeviceOnlineWithName": "{0} aripo", + "External": "Zvekunze", + "FailedLoginAttemptWithUserName": "Vatadza kuloga chimboedza kushandisa {0}", + "Favorites": "Zvaunofarira", + "Folders": "Mafoodha", + "Forced": "Zvekumanikidzira", + "Genres": "Mhando", + "HeaderFavoriteAlbums": "Madambarefu aunofarira", + "HeaderFavoriteArtists": "Vaimbi vaunofarira", + "HeaderFavoriteEpisodes": "Maepisodhi aunofarira", + "HeaderFavoriteShows": "Masirisi aunofarira" +} From d5fec4963ee69460a84025c456eb7d928634e765 Mon Sep 17 00:00:00 2001 From: Shadowghost <Shadowghost@users.noreply.github.com> Date: Wed, 10 May 2023 22:05:27 +0200 Subject: [PATCH 288/858] Fix FirstTimeSetupHandler not failing on invalid user if not in setup mode (#9747) --- .../FirstTimeSetupPolicy/FirstTimeSetupHandler.cs | 12 ++++++++++-- Jellyfin.Api/Controllers/SystemController.cs | 12 ++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs index 28ba258503..688a13bc0b 100644 --- a/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs +++ b/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs @@ -38,7 +38,15 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupPolicy return Task.CompletedTask; } - if (requirement.RequireAdmin && !context.User.IsInRole(UserRoles.Administrator)) + var contextUser = context.User; + if (requirement.RequireAdmin && !contextUser.IsInRole(UserRoles.Administrator)) + { + context.Fail(); + return Task.CompletedTask; + } + + var userId = contextUser.GetUserId(); + if (userId.Equals(default)) { context.Fail(); return Task.CompletedTask; @@ -50,7 +58,7 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupPolicy return Task.CompletedTask; } - var user = _userManager.GetUserById(context.User.GetUserId()); + var user = _userManager.GetUserById(userId); if (user is null) { throw new ResourceNotFoundException(); diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index 4ab705f40a..9ed69f4205 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -59,10 +59,12 @@ public class SystemController : BaseJellyfinApiController /// Gets information about the server. /// </summary> /// <response code="200">Information retrieved.</response> + /// <response code="403">User does not have permission to retrieve information.</response> /// <returns>A <see cref="SystemInfo"/> with info about the system.</returns> [HttpGet("Info")] [Authorize(Policy = Policies.FirstTimeSetupOrIgnoreParentalControl)] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] public ActionResult<SystemInfo> GetSystemInfo() { return _appHost.GetSystemInfo(Request); @@ -97,10 +99,12 @@ public class SystemController : BaseJellyfinApiController /// Restarts the application. /// </summary> /// <response code="204">Server restarted.</response> + /// <response code="403">User does not have permission to restart server.</response> /// <returns>No content. Server restarted.</returns> [HttpPost("Restart")] [Authorize(Policy = Policies.LocalAccessOrRequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] public ActionResult RestartApplication() { Task.Run(async () => @@ -115,10 +119,12 @@ public class SystemController : BaseJellyfinApiController /// Shuts down the application. /// </summary> /// <response code="204">Server shut down.</response> + /// <response code="403">User does not have permission to shutdown server.</response> /// <returns>No content. Server shut down.</returns> [HttpPost("Shutdown")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] public ActionResult ShutdownApplication() { Task.Run(async () => @@ -133,10 +139,12 @@ public class SystemController : BaseJellyfinApiController /// Gets a list of available server log files. /// </summary> /// <response code="200">Information retrieved.</response> + /// <response code="403">User does not have permission to get server logs.</response> /// <returns>An array of <see cref="LogFile"/> with the available log files.</returns> [HttpGet("Logs")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] public ActionResult<LogFile[]> GetServerLogs() { IEnumerable<FileSystemMetadata> files; @@ -170,10 +178,12 @@ public class SystemController : BaseJellyfinApiController /// Gets information about the request endpoint. /// </summary> /// <response code="200">Information retrieved.</response> + /// <response code="403">User does not have permission to get endpoint information.</response> /// <returns><see cref="EndPointInfo"/> with information about the endpoint.</returns> [HttpGet("Endpoint")] [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] public ActionResult<EndPointInfo> GetEndpointInfo() { return new EndPointInfo @@ -188,10 +198,12 @@ public class SystemController : BaseJellyfinApiController /// </summary> /// <param name="name">The name of the log file to get.</param> /// <response code="200">Log file retrieved.</response> + /// <response code="403">User does not have permission to get log files.</response> /// <returns>The log file.</returns> [HttpGet("Logs/Log")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesFile(MediaTypeNames.Text.Plain)] public ActionResult GetLogFile([FromQuery, Required] string name) { From bcf92b57220d92242ef6ae82118d469f2401e27f Mon Sep 17 00:00:00 2001 From: Shadowghost <Shadowghost@users.noreply.github.com> Date: Thu, 11 May 2023 01:38:54 +0200 Subject: [PATCH 289/858] Fix MigrateRatingLevels (#9461) --- Jellyfin.Server/Migrations/MigrationRunner.cs | 6 +- .../PreStartupRoutines/MigrateRatingLevels.cs | 86 --------------- .../Routines/MigrateRatingLevels.cs | 103 ++++++++++++++++++ 3 files changed, 106 insertions(+), 89 deletions(-) delete mode 100644 Jellyfin.Server/Migrations/PreStartupRoutines/MigrateRatingLevels.cs create mode 100644 Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs index 866262d22f..abfdcd77d5 100644 --- a/Jellyfin.Server/Migrations/MigrationRunner.cs +++ b/Jellyfin.Server/Migrations/MigrationRunner.cs @@ -22,8 +22,7 @@ namespace Jellyfin.Server.Migrations private static readonly Type[] _preStartupMigrationTypes = { typeof(PreStartupRoutines.CreateNetworkConfiguration), - typeof(PreStartupRoutines.MigrateMusicBrainzTimeout), - typeof(PreStartupRoutines.MigrateRatingLevels) + typeof(PreStartupRoutines.MigrateMusicBrainzTimeout) }; /// <summary> @@ -41,7 +40,8 @@ namespace Jellyfin.Server.Migrations typeof(Routines.MigrateDisplayPreferencesDb), typeof(Routines.RemoveDownloadImagesInAdvance), typeof(Routines.MigrateAuthenticationDb), - typeof(Routines.FixPlaylistOwner) + typeof(Routines.FixPlaylistOwner), + typeof(Routines.MigrateRatingLevels) }; /// <summary> diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateRatingLevels.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateRatingLevels.cs deleted file mode 100644 index 465bbd7fe1..0000000000 --- a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateRatingLevels.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; -using System.Globalization; -using System.IO; - -using Emby.Server.Implementations; -using MediaBrowser.Controller; -using Microsoft.Extensions.Logging; -using SQLitePCL.pretty; - -namespace Jellyfin.Server.Migrations.PreStartupRoutines -{ - /// <summary> - /// Migrate rating levels to new rating level system. - /// </summary> - internal class MigrateRatingLevels : IMigrationRoutine - { - private const string DbFilename = "library.db"; - private readonly ILogger<MigrateRatingLevels> _logger; - private readonly IServerApplicationPaths _applicationPaths; - - public MigrateRatingLevels(ServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory) - { - _applicationPaths = applicationPaths; - _logger = loggerFactory.CreateLogger<MigrateRatingLevels>(); - } - - /// <inheritdoc/> - public Guid Id => Guid.Parse("{67445D54-B895-4B24-9F4C-35CE0690EA07}"); - - /// <inheritdoc/> - public string Name => "MigrateRatingLevels"; - - /// <inheritdoc/> - public bool PerformOnNewInstall => false; - - /// <inheritdoc/> - public void Perform() - { - var dataPath = _applicationPaths.DataPath; - var dbPath = Path.Combine(dataPath, DbFilename); - using (var connection = SQLite3.Open( - dbPath, - ConnectionFlags.ReadWrite, - null)) - { - // Back up the database before deleting any entries - for (int i = 1; ; i++) - { - var bakPath = string.Format(CultureInfo.InvariantCulture, "{0}.bak{1}", dbPath, i); - if (!File.Exists(bakPath)) - { - try - { - File.Copy(dbPath, bakPath); - _logger.LogInformation("Library database backed up to {BackupPath}", bakPath); - break; - } - catch (Exception ex) - { - _logger.LogError(ex, "Cannot make a backup of {Library} at path {BackupPath}", DbFilename, bakPath); - throw; - } - } - } - - // Migrate parental rating levels to new schema - _logger.LogInformation("Migrating parental rating levels."); - connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = NULL WHERE OfficialRating = 'NR'"); - connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = NULL WHERE InheritedParentalRatingValue = ''"); - connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = NULL WHERE InheritedParentalRatingValue = 0"); - connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 1000 WHERE InheritedParentalRatingValue = 100"); - connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 1000 WHERE InheritedParentalRatingValue = 15"); - connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 18 WHERE InheritedParentalRatingValue = 10"); - connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 18 WHERE InheritedParentalRatingValue = 9"); - connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 16 WHERE InheritedParentalRatingValue = 8"); - connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 12 WHERE InheritedParentalRatingValue = 7"); - connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 12 WHERE InheritedParentalRatingValue = 6"); - connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 12 WHERE InheritedParentalRatingValue = 5"); - connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 7 WHERE InheritedParentalRatingValue = 4"); - connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 6 WHERE InheritedParentalRatingValue = 3"); - connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 6 WHERE InheritedParentalRatingValue = 2"); - connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 0 WHERE InheritedParentalRatingValue = 1"); - } - } - } -} diff --git a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs new file mode 100644 index 0000000000..9dee520a50 --- /dev/null +++ b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs @@ -0,0 +1,103 @@ +using System; +using System.Globalization; +using System.IO; + +using Emby.Server.Implementations.Data; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.Globalization; +using Microsoft.Extensions.Logging; +using SQLitePCL.pretty; + +namespace Jellyfin.Server.Migrations.Routines +{ + /// <summary> + /// Migrate rating levels to new rating level system. + /// </summary> + internal class MigrateRatingLevels : IMigrationRoutine + { + private const string DbFilename = "library.db"; + private readonly ILogger<MigrateRatingLevels> _logger; + private readonly IServerApplicationPaths _applicationPaths; + private readonly ILocalizationManager _localizationManager; + private readonly IItemRepository _repository; + + public MigrateRatingLevels( + IServerApplicationPaths applicationPaths, + ILoggerFactory loggerFactory, + ILocalizationManager localizationManager, + IItemRepository repository) + { + _applicationPaths = applicationPaths; + _localizationManager = localizationManager; + _repository = repository; + _logger = loggerFactory.CreateLogger<MigrateRatingLevels>(); + } + + /// <inheritdoc/> + public Guid Id => Guid.Parse("{67445D54-B895-4B24-9F4C-35CE0690EA07}"); + + /// <inheritdoc/> + public string Name => "MigrateRatingLevels"; + + /// <inheritdoc/> + public bool PerformOnNewInstall => false; + + /// <inheritdoc/> + public void Perform() + { + var dbPath = Path.Combine(_applicationPaths.DataPath, DbFilename); + + // Back up the database before modifying any entries + for (int i = 1; ; i++) + { + var bakPath = string.Format(CultureInfo.InvariantCulture, "{0}.bak{1}", dbPath, i); + if (!File.Exists(bakPath)) + { + try + { + File.Copy(dbPath, bakPath); + _logger.LogInformation("Library database backed up to {BackupPath}", bakPath); + break; + } + catch (Exception ex) + { + _logger.LogError(ex, "Cannot make a backup of {Library} at path {BackupPath}", DbFilename, bakPath); + throw; + } + } + } + + // Migrate parental rating strings to new levels + _logger.LogInformation("Recalculating parental rating levels based on rating string."); + using (var connection = SQLite3.Open( + dbPath, + ConnectionFlags.ReadWrite, + null)) + { + var queryResult = connection.Query("SELECT DISTINCT OfficialRating FROM TypedBaseItems"); + foreach (var entry in queryResult) + { + var ratingString = entry[0].ToString(); + if (string.IsNullOrEmpty(ratingString)) + { + connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = NULL WHERE OfficialRating IS NULL OR OfficialRating='';"); + } + else + { + var ratingValue = _localizationManager.GetRatingLevel(ratingString).ToString(); + if (string.IsNullOrEmpty(ratingValue)) + { + ratingValue = "NULL"; + } + + var statement = connection.PrepareStatement("UPDATE TypedBaseItems SET InheritedParentalRatingValue = @Value WHERE OfficialRating = @Rating;"); + statement.TryBind("@Value", ratingValue); + statement.TryBind("@Rating", ratingString); + statement.ExecuteQuery(); + } + } + } + } + } +} From 756ee38d01ae66f1d6abe920d96eabce468a2e2e Mon Sep 17 00:00:00 2001 From: Bond-009 <bond.009@outlook.com> Date: Thu, 11 May 2023 01:39:57 +0200 Subject: [PATCH 290/858] Remove ExtendedFileSystemInfo (#9749) --- .../IO/ExtendedFileSystemInfo.cs | 13 ------ .../IO/ManagedFileSystem.cs | 46 +++++-------------- 2 files changed, 11 insertions(+), 48 deletions(-) delete mode 100644 Emby.Server.Implementations/IO/ExtendedFileSystemInfo.cs diff --git a/Emby.Server.Implementations/IO/ExtendedFileSystemInfo.cs b/Emby.Server.Implementations/IO/ExtendedFileSystemInfo.cs deleted file mode 100644 index 545d73e05f..0000000000 --- a/Emby.Server.Implementations/IO/ExtendedFileSystemInfo.cs +++ /dev/null @@ -1,13 +0,0 @@ -#pragma warning disable CS1591 - -namespace Emby.Server.Implementations.IO -{ - public class ExtendedFileSystemInfo - { - public bool IsHidden { get; set; } - - public bool IsReadOnly { get; set; } - - public bool Exists { get; set; } - } -} diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 55f384ae8f..1fffdfbfa0 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -267,25 +267,6 @@ namespace Emby.Server.Implementations.IO return result; } - private static ExtendedFileSystemInfo GetExtendedFileSystemInfo(string path) - { - var result = new ExtendedFileSystemInfo(); - - var info = new FileInfo(path); - - if (info.Exists) - { - result.Exists = true; - - var attributes = info.Attributes; - - result.IsHidden = (attributes & FileAttributes.Hidden) == FileAttributes.Hidden; - result.IsReadOnly = (attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly; - } - - return result; - } - /// <summary> /// Takes a filename and removes invalid characters. /// </summary> @@ -403,19 +384,18 @@ namespace Emby.Server.Implementations.IO return; } - var info = GetExtendedFileSystemInfo(path); + var info = new FileInfo(path); - if (info.Exists && info.IsHidden != isHidden) + if (info.Exists && + ((info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) != isHidden) { if (isHidden) { - File.SetAttributes(path, File.GetAttributes(path) | FileAttributes.Hidden); + File.SetAttributes(path, info.Attributes | FileAttributes.Hidden); } else { - var attributes = File.GetAttributes(path); - attributes = RemoveAttribute(attributes, FileAttributes.Hidden); - File.SetAttributes(path, attributes); + File.SetAttributes(path, info.Attributes & ~FileAttributes.Hidden); } } } @@ -428,19 +408,20 @@ namespace Emby.Server.Implementations.IO return; } - var info = GetExtendedFileSystemInfo(path); + var info = new FileInfo(path); if (!info.Exists) { return; } - if (info.IsReadOnly == readOnly && info.IsHidden == isHidden) + if (((info.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) == readOnly + && ((info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) == isHidden) { return; } - var attributes = File.GetAttributes(path); + var attributes = info.Attributes; if (readOnly) { @@ -448,7 +429,7 @@ namespace Emby.Server.Implementations.IO } else { - attributes = RemoveAttribute(attributes, FileAttributes.ReadOnly); + attributes &= ~FileAttributes.ReadOnly; } if (isHidden) @@ -457,17 +438,12 @@ namespace Emby.Server.Implementations.IO } else { - attributes = RemoveAttribute(attributes, FileAttributes.Hidden); + attributes &= ~FileAttributes.Hidden; } File.SetAttributes(path, attributes); } - private static FileAttributes RemoveAttribute(FileAttributes attributes, FileAttributes attributesToRemove) - { - return attributes & ~attributesToRemove; - } - /// <summary> /// Swaps the files. /// </summary> From ffac062489ec6bd8823823148220e1785ecc44d9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 10 May 2023 17:43:17 -0600 Subject: [PATCH 291/858] chore(deps): update fedora docker tag to v39 (#9482) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- deployment/Dockerfile.fedora.amd64 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/Dockerfile.fedora.amd64 b/deployment/Dockerfile.fedora.amd64 index 275b379b3d..8d708f9028 100644 --- a/deployment/Dockerfile.fedora.amd64 +++ b/deployment/Dockerfile.fedora.amd64 @@ -1,4 +1,4 @@ -FROM fedora:36 +FROM fedora:39 # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist From 11d7c00de9731b3b875ea5bb3afa02e1dfd7b950 Mon Sep 17 00:00:00 2001 From: SenorSmartyPants <senorsmartypants@gmail.com> Date: Wed, 10 May 2023 18:46:55 -0500 Subject: [PATCH 292/858] Fix local JPG primary image for video being overwritten by screen grabber (#9552) --- .../Manager/ItemImageProvider.cs | 42 +++++++++++++++++-- .../Manager/MetadataService.cs | 19 ++++----- .../Manager/ItemImageProviderTests.cs | 6 +-- 3 files changed, 48 insertions(+), 19 deletions(-) diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs index ba2d2db2f7..dab36625e5 100644 --- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs +++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs @@ -32,6 +32,7 @@ namespace MediaBrowser.Providers.Manager private readonly ILogger _logger; private readonly IProviderManager _providerManager; private readonly IFileSystem _fileSystem; + private static readonly ImageType[] AllImageTypes = Enum.GetValues<ImageType>(); /// <summary> /// Image types that are only one per item. @@ -90,11 +91,12 @@ namespace MediaBrowser.Providers.Manager /// </summary> /// <param name="item">The <see cref="BaseItem"/> to validate images for.</param> /// <param name="providers">The providers to use, must include <see cref="ILocalImageProvider"/>(s) for local scanning.</param> - /// <param name="directoryService">The directory service for <see cref="ILocalImageProvider"/>s to use.</param> + /// <param name="refreshOptions">The refresh options.</param> /// <returns><c>true</c> if changes were made to the item; otherwise <c>false</c>.</returns> - public bool ValidateImages(BaseItem item, IEnumerable<IImageProvider> providers, IDirectoryService directoryService) + public bool ValidateImages(BaseItem item, IEnumerable<IImageProvider> providers, ImageRefreshOptions refreshOptions) { var hasChanges = false; + IDirectoryService directoryService = refreshOptions?.DirectoryService; if (item is not Photo) { @@ -102,7 +104,7 @@ namespace MediaBrowser.Providers.Manager .SelectMany(i => i.GetImages(item, directoryService)) .ToList(); - if (MergeImages(item, images)) + if (MergeImages(item, images, refreshOptions)) { hasChanges = true; } @@ -381,15 +383,36 @@ namespace MediaBrowser.Providers.Manager item.RemoveImages(images); } + /// <summary> + /// Merges a list of images into the provided item, validating existing images and replacing them or adding new images as necessary. + /// </summary> + /// <param name="refreshOptions">The refresh options.</param> + /// <param name="dontReplaceImages">List of imageTypes to remove from ReplaceImages.</param> + public void UpdateReplaceImages(ImageRefreshOptions refreshOptions, ICollection<ImageType> dontReplaceImages) + { + if (refreshOptions is not null) + { + if (refreshOptions.ReplaceAllImages) + { + refreshOptions.ReplaceAllImages = false; + refreshOptions.ReplaceImages = AllImageTypes.ToList(); + } + + refreshOptions.ReplaceImages = refreshOptions.ReplaceImages.Except(dontReplaceImages).ToList(); + } + } + /// <summary> /// Merges a list of images into the provided item, validating existing images and replacing them or adding new images as necessary. /// </summary> /// <param name="item">The <see cref="BaseItem"/> to modify.</param> /// <param name="images">The new images to place in <c>item</c>.</param> + /// <param name="refreshOptions">The refresh options.</param> /// <returns><c>true</c> if changes were made to the item; otherwise <c>false</c>.</returns> - public bool MergeImages(BaseItem item, IReadOnlyList<LocalImageInfo> images) + public bool MergeImages(BaseItem item, IReadOnlyList<LocalImageInfo> images, ImageRefreshOptions refreshOptions) { var changed = item.ValidateImages(); + var foundImageTypes = new List<ImageType>(); for (var i = 0; i < _singularImages.Length; i++) { @@ -399,6 +422,11 @@ namespace MediaBrowser.Providers.Manager if (image is not null) { var currentImage = item.GetImageInfo(type, 0); + // if image file is stored with media, don't replace that later + if (item.ContainingFolderPath is not null && item.ContainingFolderPath.Contains(Path.GetDirectoryName(image.FileInfo.FullName), StringComparison.OrdinalIgnoreCase)) + { + foundImageTypes.Add(type); + } if (currentImage is null || !string.Equals(currentImage.Path, image.FileInfo.FullName, StringComparison.OrdinalIgnoreCase)) { @@ -425,6 +453,12 @@ namespace MediaBrowser.Providers.Manager if (UpdateMultiImages(item, images, ImageType.Backdrop)) { changed = true; + foundImageTypes.Add(ImageType.Backdrop); + } + + if (foundImageTypes.Count > 0) + { + UpdateReplaceImages(refreshOptions, foundImageTypes); } return changed; diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index bcc9b809c2..80f77f7c3a 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -26,8 +26,6 @@ namespace MediaBrowser.Providers.Manager where TItemType : BaseItem, IHasLookupInfo<TIdType>, new() where TIdType : ItemLookupInfo, new() { - private static readonly ImageType[] AllImageTypes = Enum.GetValues<ImageType>(); - protected MetadataService(IServerConfigurationManager serverConfigurationManager, ILogger<MetadataService<TItemType, TIdType>> logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager) { ServerConfigurationManager = serverConfigurationManager; @@ -110,7 +108,7 @@ namespace MediaBrowser.Providers.Manager try { // Always validate images and check for new locally stored ones. - if (ImageProvider.ValidateImages(item, allImageProviders.OfType<ILocalImageProvider>(), refreshOptions.DirectoryService)) + if (ImageProvider.ValidateImages(item, allImageProviders.OfType<ILocalImageProvider>(), refreshOptions)) { updateType |= ItemUpdateType.ImageUpdate; } @@ -674,8 +672,7 @@ namespace MediaBrowser.Providers.Manager } var hasLocalMetadata = false; - var replaceImages = AllImageTypes.ToList(); - var localImagesFound = false; + var foundImageTypes = new List<ImageType>(); foreach (var provider in providers.OfType<ILocalMetadataProvider<TItemType>>()) { @@ -703,9 +700,8 @@ namespace MediaBrowser.Providers.Manager await ProviderManager.SaveImage(item, remoteImage.Url, remoteImage.Type, null, cancellationToken).ConfigureAwait(false); refreshResult.UpdateType |= ItemUpdateType.ImageUpdate; - // remove imagetype that has just been downloaded - replaceImages.Remove(remoteImage.Type); - localImagesFound = true; + // remember imagetype that has just been downloaded + foundImageTypes.Add(remoteImage.Type); } catch (HttpRequestException ex) { @@ -713,13 +709,12 @@ namespace MediaBrowser.Providers.Manager } } - if (localImagesFound) + if (foundImageTypes.Count > 0) { - options.ReplaceAllImages = false; - options.ReplaceImages = replaceImages; + imageService.UpdateReplaceImages(options, foundImageTypes); } - if (imageService.MergeImages(item, localItem.Images)) + if (imageService.MergeImages(item, localItem.Images, options)) { refreshResult.UpdateType |= ItemUpdateType.ImageUpdate; } diff --git a/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs b/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs index 08b343cd89..925e8fa199 100644 --- a/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs +++ b/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs @@ -94,7 +94,7 @@ namespace Jellyfin.Providers.Tests.Manager public void MergeImages_EmptyItemNewImagesEmpty_NoChange() { var itemImageProvider = GetItemImageProvider(null, null); - var changed = itemImageProvider.MergeImages(new Video(), Array.Empty<LocalImageInfo>()); + var changed = itemImageProvider.MergeImages(new Video(), Array.Empty<LocalImageInfo>(), new ImageRefreshOptions(Mock.Of<IDirectoryService>())); Assert.False(changed); } @@ -108,7 +108,7 @@ namespace Jellyfin.Providers.Tests.Manager var images = GetImages(imageType, imageCount, false); var itemImageProvider = GetItemImageProvider(null, null); - var changed = itemImageProvider.MergeImages(item, images); + var changed = itemImageProvider.MergeImages(item, images, new ImageRefreshOptions(Mock.Of<IDirectoryService>())); Assert.True(changed); // adds for types that allow multiple, replaces singular type images @@ -151,7 +151,7 @@ namespace Jellyfin.Providers.Tests.Manager var images = GetImages(imageType, imageCount, true); var itemImageProvider = GetItemImageProvider(null, fileSystem); - var changed = itemImageProvider.MergeImages(item, images); + var changed = itemImageProvider.MergeImages(item, images, new ImageRefreshOptions(Mock.Of<IDirectoryService>())); if (updateTime) { From fdda72139411a89a64a1a1bb7282f273dc720e42 Mon Sep 17 00:00:00 2001 From: Gustavs <tausgus@tuta.io> Date: Thu, 11 May 2023 13:28:25 +0000 Subject: [PATCH 293/858] Translated using Weblate (Latvian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/lv/ --- Emby.Server.Implementations/Localization/Core/lv.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/lv.json b/Emby.Server.Implementations/Localization/Core/lv.json index f753a369ab..f7b24412af 100644 --- a/Emby.Server.Implementations/Localization/Core/lv.json +++ b/Emby.Server.Implementations/Localization/Core/lv.json @@ -84,7 +84,7 @@ "CameraImageUploadedFrom": "Jauns kameras attēls ir ticis augšupielādēts no {0}", "Books": "Grāmatas", "Artists": "Izpildītāji", - "Albums": "Albūmi", + "Albums": "Albumi", "ProviderValue": "Provider: {0}", "HeaderFavoriteSongs": "Dziesmu Favorīti", "HeaderFavoriteShows": "Raidījumu Favorīti", @@ -121,5 +121,7 @@ "TaskOptimizeDatabaseDescription": "Saspiež datubāzi un atbrīvo atmiņu. Uzdevum palaišana pēc bibliotēku skenēšanas vai citām, ar datubāzi saistītām, izmaiņām iespējams uzlabos ātrdarbību.", "TaskOptimizeDatabase": "Optimizēt datubāzi", "External": "Ārējais", - "HearingImpaired": "Ar dzirdes traucējumiem" + "HearingImpaired": "Ar dzirdes traucējumiem", + "TaskKeyframeExtractor": "Atslēgkadru Ekstraktors", + "TaskKeyframeExtractorDescription": "Ekstraktē atslēgkadrus no video failiem lai izveidotu precīzākus HLS atskaņošanas sarakstus. Šis process var būt ilgs." } From a8cdf4434b10044dbb9ba540d6d137906aa67b54 Mon Sep 17 00:00:00 2001 From: Shadowghost <Shadowghost@users.noreply.github.com> Date: Fri, 12 May 2023 15:11:59 +0200 Subject: [PATCH 294/858] Fix access to playlists not created by a user (#9746) --- .../Library/Resolvers/PlaylistResolver.cs | 11 +++++++---- .../Playlists/PlaylistManager.cs | 2 +- Jellyfin.Api/Controllers/PlaylistsController.cs | 16 +++++++++++++--- .../Migrations/Routines/FixPlaylistOwner.cs | 17 ++++++++++++----- MediaBrowser.Controller/Playlists/Playlist.cs | 8 ++++++++ 5 files changed, 41 insertions(+), 13 deletions(-) diff --git a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs index 7a2b3da3a9..5d569009d3 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs @@ -30,7 +30,7 @@ namespace Emby.Server.Implementations.Library.Resolvers { if (args.IsDirectory) { - // It's a boxset if the path is a directory with [playlist] in it's the name + // It's a boxset if the path is a directory with [playlist] in its name var filename = Path.GetFileName(Path.TrimEndingDirectorySeparator(args.Path)); if (string.IsNullOrEmpty(filename)) { @@ -42,7 +42,8 @@ namespace Emby.Server.Implementations.Library.Resolvers return new Playlist { Path = args.Path, - Name = filename.Replace("[playlist]", string.Empty, StringComparison.OrdinalIgnoreCase).Trim() + Name = filename.Replace("[playlist]", string.Empty, StringComparison.OrdinalIgnoreCase).Trim(), + OpenAccess = true }; } @@ -53,7 +54,8 @@ namespace Emby.Server.Implementations.Library.Resolvers return new Playlist { Path = args.Path, - Name = filename + Name = filename, + OpenAccess = true }; } } @@ -70,7 +72,8 @@ namespace Emby.Server.Implementations.Library.Resolvers Path = args.Path, Name = Path.GetFileNameWithoutExtension(args.Path), IsInMixedFolder = true, - PlaylistMediaType = MediaType.Audio + PlaylistMediaType = MediaType.Audio, + OpenAccess = true }; } } diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 6176879b66..adb8ac7327 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -549,7 +549,7 @@ namespace Emby.Server.Implementations.Playlists SavePlaylistFile(playlist); } } - else + else if (!playlist.OpenAccess) { // Remove playlist if not shared _libraryManager.DeleteItem( diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index c6dbea5e22..20995bf1b4 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -64,12 +64,15 @@ public class PlaylistsController : BaseJellyfinApiController /// <param name="userId">The user id.</param> /// <param name="mediaType">The media type.</param> /// <param name="createPlaylistRequest">The create playlist payload.</param> + /// <response code="200">Playlist created.</response> + /// <response code="403">User does not have permission to create playlists.</response> /// <returns> /// A <see cref="Task" /> that represents the asynchronous operation to create a playlist. /// The task result contains an <see cref="OkResult"/> indicating success. /// </returns> [HttpPost] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist( [FromQuery, ParameterObsolete] string? name, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder)), ParameterObsolete] IReadOnlyList<Guid> ids, @@ -102,9 +105,11 @@ public class PlaylistsController : BaseJellyfinApiController /// <param name="ids">Item id, comma delimited.</param> /// <param name="userId">The userId.</param> /// <response code="204">Items added to playlist.</response> + /// <response code="403">User does not have permission to add items to playlist.</response> /// <returns>An <see cref="NoContentResult"/> on success.</returns> [HttpPost("{playlistId}/Items")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task<ActionResult> AddToPlaylist( [FromRoute, Required] Guid playlistId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids, @@ -122,9 +127,11 @@ public class PlaylistsController : BaseJellyfinApiController /// <param name="itemId">The item id.</param> /// <param name="newIndex">The new index.</param> /// <response code="204">Item moved to new index.</response> + /// <response code="403">User does not have permission to move item.</response> /// <returns>An <see cref="NoContentResult"/> on success.</returns> [HttpPost("{playlistId}/Items/{itemId}/Move/{newIndex}")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task<ActionResult> MoveItem( [FromRoute, Required] string playlistId, [FromRoute, Required] string itemId, @@ -140,9 +147,11 @@ public class PlaylistsController : BaseJellyfinApiController /// <param name="playlistId">The playlist id.</param> /// <param name="entryIds">The item ids, comma delimited.</param> /// <response code="204">Items removed.</response> + /// <response code="403">User does not have permission to get playlist.</response> /// <returns>An <see cref="NoContentResult"/> on success.</returns> [HttpDelete("{playlistId}/Items")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task<ActionResult> RemoveFromPlaylist( [FromRoute, Required] string playlistId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] entryIds) @@ -164,9 +173,13 @@ public class PlaylistsController : BaseJellyfinApiController /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> /// <response code="200">Original playlist returned.</response> + /// <response code="403">User does not have permission to get playlist items.</response> /// <response code="404">Playlist not found.</response> /// <returns>The original playlist items.</returns> [HttpGet("{playlistId}/Items")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult<QueryResult<BaseItemDto>> GetPlaylistItems( [FromRoute, Required] Guid playlistId, [FromQuery, Required] Guid userId, @@ -189,9 +202,7 @@ public class PlaylistsController : BaseJellyfinApiController : _userManager.GetUserById(userId); var items = playlist.GetManageableItems().ToArray(); - var count = items.Length; - if (startIndex.HasValue) { items = items.Skip(startIndex.Value).ToArray(); @@ -207,7 +218,6 @@ public class PlaylistsController : BaseJellyfinApiController .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var dtos = _dtoService.GetBaseItemDtos(items.Select(i => i.Item2).ToList(), dtoOptions, user); - for (int index = 0; index < dtos.Count; index++) { dtos[index].PlaylistItemId = items[index].Item1.Id; diff --git a/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs b/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs index 55aadae79a..1dd938b1be 100644 --- a/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs +++ b/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs @@ -53,12 +53,19 @@ internal class FixPlaylistOwner : IMigrationRoutine foreach (var playlist in playlists) { var shares = playlist.Shares; - var firstEditShare = shares.First(x => x.CanEdit); - if (firstEditShare is not null && Guid.TryParse(firstEditShare.UserId, out var guid)) + if (shares.Length > 0) { - playlist.OwnerUserId = guid; - playlist.Shares = shares.Where(x => x != firstEditShare).ToArray(); - + var firstEditShare = shares.First(x => x.CanEdit); + if (firstEditShare is not null && Guid.TryParse(firstEditShare.UserId, out var guid)) + { + playlist.OwnerUserId = guid; + playlist.Shares = shares.Where(x => x != firstEditShare).ToArray(); + _playlistManager.UpdatePlaylistAsync(playlist).GetAwaiter().GetResult(); + } + } + else + { + playlist.OpenAccess = true; _playlistManager.UpdatePlaylistAsync(playlist).GetAwaiter().GetResult(); } } diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs index 344e996ea8..498df5ab06 100644 --- a/MediaBrowser.Controller/Playlists/Playlist.cs +++ b/MediaBrowser.Controller/Playlists/Playlist.cs @@ -34,10 +34,13 @@ namespace MediaBrowser.Controller.Playlists public Playlist() { Shares = Array.Empty<Share>(); + OpenAccess = false; } public Guid OwnerUserId { get; set; } + public bool OpenAccess { get; set; } + public Share[] Shares { get; set; } [JsonIgnore] @@ -233,6 +236,11 @@ namespace MediaBrowser.Controller.Playlists return base.IsVisible(user); } + if (OpenAccess) + { + return true; + } + var userId = user.Id; if (userId.Equals(OwnerUserId)) { From 47290a8c3665f3adb859bda19deb66f438f2e5d0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 12 May 2023 07:12:16 -0600 Subject: [PATCH 295/858] chore(deps): update dependency serilog.settings.configuration to v7 (#9752) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index ee0b9a8dc8..6be5c50003 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -58,7 +58,7 @@ <PackageVersion Include="prometheus-net" Version="8.0.0" /> <PackageVersion Include="Serilog.AspNetCore" Version="6.1.0" /> <PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" /> - <PackageVersion Include="Serilog.Settings.Configuration" Version="3.4.0" /> + <PackageVersion Include="Serilog.Settings.Configuration" Version="7.0.0" /> <PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" /> <PackageVersion Include="Serilog.Sinks.Console" Version="4.1.0" /> <PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" /> From 10fda7a041ed89504a887918f0dd6174bb44543c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 12 May 2023 13:13:26 +0000 Subject: [PATCH 296/858] chore(deps): update dependency serilog.aspnetcore to v7 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 6be5c50003..53177e40df 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -56,7 +56,7 @@ <PackageVersion Include="prometheus-net.AspNetCore" Version="8.0.0" /> <PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" /> <PackageVersion Include="prometheus-net" Version="8.0.0" /> - <PackageVersion Include="Serilog.AspNetCore" Version="6.1.0" /> + <PackageVersion Include="Serilog.AspNetCore" Version="7.0.0" /> <PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" /> <PackageVersion Include="Serilog.Settings.Configuration" Version="7.0.0" /> <PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" /> From 9352a243744d5d6805f80dc57792919f15ca5b90 Mon Sep 17 00:00:00 2001 From: SeaEagle1 <seaeagle1@users.sourceforge.net> Date: Fri, 12 May 2023 17:53:27 +0200 Subject: [PATCH 297/858] Rescue PlayTo function in case of malformed Xml response (port from 10.8 fix) --- Emby.Dlna/PlayTo/DlnaHttpClient.cs | 35 ++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/Emby.Dlna/PlayTo/DlnaHttpClient.cs b/Emby.Dlna/PlayTo/DlnaHttpClient.cs index 75ff542dd8..9930f0ede1 100644 --- a/Emby.Dlna/PlayTo/DlnaHttpClient.cs +++ b/Emby.Dlna/PlayTo/DlnaHttpClient.cs @@ -2,9 +2,11 @@ using System; using System.Globalization; +using System.IO; using System.Net.Http; using System.Net.Mime; using System.Text; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using System.Xml; @@ -54,15 +56,34 @@ namespace Emby.Dlna.PlayTo LoadOptions.None, cancellationToken).ConfigureAwait(false); } - catch (XmlException ex) + catch (XmlException) { - _logger.LogError(ex, "Failed to parse response"); - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Malformed response: {Content}\n", await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false)); - } + // try correcting the Xml response with common errors + var xmlString = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); - return null; + // find and replace unescaped ampersands (&) + Regex regex = new Regex(@"(&(?![a-z]*;))"); + xmlString = regex.Replace(xmlString, @"&"); + + try + { + // retry reading Xml + var xmlReader = new StringReader(xmlString); + return await XDocument.LoadAsync( + xmlReader, + LoadOptions.None, + cancellationToken).ConfigureAwait(false); + } + catch (XmlException ex) + { + _logger.LogError(ex, "Failed to parse response"); + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Malformed response: {Content}\n", xmlString); + } + + return null; + } } } From 126047bfd6fbb3156c07905b8cf58506cde2c664 Mon Sep 17 00:00:00 2001 From: SeaEagle1 <seaeagle1@users.sourceforge.net> Date: Sat, 13 May 2023 00:07:20 +0200 Subject: [PATCH 298/858] Use compile-time generated regex and remove loglevel check --- Emby.Dlna/PlayTo/DlnaHttpClient.cs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/Emby.Dlna/PlayTo/DlnaHttpClient.cs b/Emby.Dlna/PlayTo/DlnaHttpClient.cs index 9930f0ede1..4e9903f26a 100644 --- a/Emby.Dlna/PlayTo/DlnaHttpClient.cs +++ b/Emby.Dlna/PlayTo/DlnaHttpClient.cs @@ -17,7 +17,10 @@ using Microsoft.Extensions.Logging; namespace Emby.Dlna.PlayTo { - public class DlnaHttpClient + /// <summary> + /// Http client for Dlna PlayTo function. + /// </summary> + public partial class DlnaHttpClient { private readonly ILogger _logger; private readonly IHttpClientFactory _httpClientFactory; @@ -62,8 +65,7 @@ namespace Emby.Dlna.PlayTo var xmlString = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); // find and replace unescaped ampersands (&) - Regex regex = new Regex(@"(&(?![a-z]*;))"); - xmlString = regex.Replace(xmlString, @"&"); + xmlString = EscapeAmpersandRegex().Replace(xmlString, "&"); try { @@ -77,10 +79,7 @@ namespace Emby.Dlna.PlayTo catch (XmlException ex) { _logger.LogError(ex, "Failed to parse response"); - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Malformed response: {Content}\n", xmlString); - } + _logger.LogDebug("Malformed response: {Content}\n", xmlString); return null; } @@ -125,5 +124,12 @@ namespace Emby.Dlna.PlayTo // Have to await here instead of returning the Task directly, otherwise request would be disposed too soon return await SendRequestAsync(request, cancellationToken).ConfigureAwait(false); } + + /// <summary> + /// Compile-time generated regular expression for escaping ampersands. + /// </summary> + /// <returns>Compiled regular expression.</returns> + [GeneratedRegex("(&(?![a-z]*;))")] + private static partial Regex EscapeAmpersandRegex(); } } From 74b33da6fcad0481a1bbfd353773de9e2eee5fce Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 14 May 2023 08:59:16 +0000 Subject: [PATCH 299/858] chore(deps): update dependency libse to v3.6.13 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 6be5c50003..ff5f0efdf6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -20,7 +20,7 @@ <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.9.1" /> <PackageVersion Include="FsCheck.Xunit" Version="2.16.5" /> <PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" /> - <PackageVersion Include="libse" Version="3.6.11" /> + <PackageVersion Include="libse" Version="3.6.13" /> <PackageVersion Include="LrcParser" Version="2023.308.0" /> <PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" /> <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.5" /> From ace89e45976a65a365a3d9d7a2ed737a61d584d4 Mon Sep 17 00:00:00 2001 From: herby2212 <12448284+herby2212@users.noreply.github.com> Date: Sun, 14 May 2023 15:05:03 +0200 Subject: [PATCH 300/858] fix formatting and update summary --- Emby.Server.Implementations/Session/SessionManager.cs | 9 +++++---- MediaBrowser.Model/Configuration/ServerConfiguration.cs | 3 ++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 056c3e4b61..14a62626c5 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -404,7 +404,7 @@ namespace Emby.Server.Implementations.Session session.LastPlaybackCheckIn = DateTime.UtcNow; } - if (info.IsPaused && session.LastPausedDate.HasValue == false) + if (info.IsPaused && session.LastPausedDate is null) { session.LastPausedDate = DateTime.UtcNow; } @@ -656,9 +656,10 @@ namespace Emby.Server.Implementations.Session private async void CheckForInactiveSteams(object state) { var pausedSessions = Sessions.Where(i => - (i.NowPlayingItem is not null) && - (i.PlayState.IsPaused == true) && - (i.LastPausedDate is not null)).ToList(); + i.NowPlayingItem is not null + && i.PlayState.IsPaused + && i.LastPausedDate is not null) + .ToList(); if (pausedSessions.Count > 0) { diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index f619f384c6..40dcf50b75 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -159,8 +159,9 @@ namespace MediaBrowser.Model.Configuration /// <summary> /// Gets or sets the threshold in minutes after a inactive session gets closed automatically. + /// If set to 0 the check for inactive sessions gets disabled. /// </summary> - /// <value>The close inactive session threshold in minutes.</value> + /// <value>The close inactive session threshold in minutes. 0 to disable.</value> public int InactiveSessionThreshold { get; set; } = 10; /// <summary> From ec32c56d3ffc4e819485aade6b6c4fdf14cc52dc Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Sun, 14 May 2023 21:45:46 +0200 Subject: [PATCH 301/858] Set removed and added tags recursively --- Jellyfin.Api/Controllers/ItemUpdateController.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs index ece053a9a7..504f2fa1d7 100644 --- a/Jellyfin.Api/Controllers/ItemUpdateController.cs +++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs @@ -251,8 +251,6 @@ public class ItemUpdateController : BaseJellyfinApiController channel.Height = request.Height.Value; } - item.Tags = request.Tags; - if (request.Taglines is not null) { item.Tagline = request.Taglines.FirstOrDefault(); @@ -276,12 +274,19 @@ public class ItemUpdateController : BaseJellyfinApiController item.OfficialRating = request.OfficialRating; item.CustomRating = request.CustomRating; + var currentTags = item.Tags; + var newTags = request.Tags; + var removedTags = currentTags.Except(newTags).ToList(); + var addedTags = newTags.Except(currentTags).ToList(); + item.Tags = newTags; + if (item is Series rseries) { foreach (Season season in rseries.Children) { season.OfficialRating = request.OfficialRating; season.CustomRating = request.CustomRating; + season.Tags = season.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray(); season.OnMetadataChanged(); await season.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); @@ -289,6 +294,7 @@ public class ItemUpdateController : BaseJellyfinApiController { ep.OfficialRating = request.OfficialRating; ep.CustomRating = request.CustomRating; + ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray(); ep.OnMetadataChanged(); await ep.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); } @@ -300,6 +306,7 @@ public class ItemUpdateController : BaseJellyfinApiController { ep.OfficialRating = request.OfficialRating; ep.CustomRating = request.CustomRating; + ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray(); ep.OnMetadataChanged(); await ep.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); } @@ -310,6 +317,7 @@ public class ItemUpdateController : BaseJellyfinApiController { track.OfficialRating = request.OfficialRating; track.CustomRating = request.CustomRating; + track.Tags = track.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray(); track.OnMetadataChanged(); await track.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); } From 603fce59df780626c3269eaa94d95504e823b2f6 Mon Sep 17 00:00:00 2001 From: TelepathicWalrus <48514138+TelepathicWalrus@users.noreply.github.com> Date: Mon, 15 May 2023 12:12:24 +0100 Subject: [PATCH 302/858] Audio normalization (#9222) Co-authored-by: Joe Rogers <1337joe@users.noreply.github.com> Co-authored-by: Bond-009 <bond.009@outlook.com> --- CONTRIBUTORS.md | 1 + .../Data/SqliteItemRepository.cs | 12 +++- Emby.Server.Implementations/Dto/DtoService.cs | 1 + MediaBrowser.Controller/Entities/BaseItem.cs | 7 +++ .../Configuration/LibraryOptions.cs | 2 + MediaBrowser.Model/Dto/BaseItemDto.cs | 6 ++ .../MediaInfo/AudioFileProber.cs | 60 +++++++++++++++++++ .../MediaInfo/ProbeProvider.cs | 2 +- 8 files changed, 88 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index c9430b235f..0b322685d1 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -126,6 +126,7 @@ - [SuperSandro2000](https://github.com/SuperSandro2000) - [tbraeutigam](https://github.com/tbraeutigam) - [teacupx](https://github.com/teacupx) + - [TelepathicWalrus](https://github.com/TelepathicWalrus) - [Terror-Gene](https://github.com/Terror-Gene) - [ThatNerdyPikachu](https://github.com/ThatNerdyPikachu) - [ThibaultNocchi](https://github.com/ThibaultNocchi) diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 22d485d335..c32e7c75ad 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -49,8 +49,8 @@ namespace Emby.Server.Implementations.Data private const string SaveItemCommandText = @"replace into TypedBaseItems - (guid,type,data,Path,StartDate,EndDate,ChannelId,IsMovie,IsSeries,EpisodeTitle,IsRepeat,CommunityRating,CustomRating,IndexNumber,IsLocked,Name,OfficialRating,MediaType,Overview,ParentIndexNumber,PremiereDate,ProductionYear,ParentId,Genres,InheritedParentalRatingValue,SortName,ForcedSortName,RunTimeTicks,Size,DateCreated,DateModified,PreferredMetadataLanguage,PreferredMetadataCountryCode,Width,Height,DateLastRefreshed,DateLastSaved,IsInMixedFolder,LockedFields,Studios,Audio,ExternalServiceId,Tags,IsFolder,UnratedType,TopParentId,TrailerTypes,CriticRating,CleanName,PresentationUniqueKey,OriginalTitle,PrimaryVersionId,DateLastMediaAdded,Album,IsVirtualItem,SeriesName,UserDataKey,SeasonName,SeasonId,SeriesId,ExternalSeriesId,Tagline,ProviderIds,Images,ProductionLocations,ExtraIds,TotalBitrate,ExtraType,Artists,AlbumArtists,ExternalId,SeriesPresentationUniqueKey,ShowId,OwnerId) - values (@guid,@type,@data,@Path,@StartDate,@EndDate,@ChannelId,@IsMovie,@IsSeries,@EpisodeTitle,@IsRepeat,@CommunityRating,@CustomRating,@IndexNumber,@IsLocked,@Name,@OfficialRating,@MediaType,@Overview,@ParentIndexNumber,@PremiereDate,@ProductionYear,@ParentId,@Genres,@InheritedParentalRatingValue,@SortName,@ForcedSortName,@RunTimeTicks,@Size,@DateCreated,@DateModified,@PreferredMetadataLanguage,@PreferredMetadataCountryCode,@Width,@Height,@DateLastRefreshed,@DateLastSaved,@IsInMixedFolder,@LockedFields,@Studios,@Audio,@ExternalServiceId,@Tags,@IsFolder,@UnratedType,@TopParentId,@TrailerTypes,@CriticRating,@CleanName,@PresentationUniqueKey,@OriginalTitle,@PrimaryVersionId,@DateLastMediaAdded,@Album,@IsVirtualItem,@SeriesName,@UserDataKey,@SeasonName,@SeasonId,@SeriesId,@ExternalSeriesId,@Tagline,@ProviderIds,@Images,@ProductionLocations,@ExtraIds,@TotalBitrate,@ExtraType,@Artists,@AlbumArtists,@ExternalId,@SeriesPresentationUniqueKey,@ShowId,@OwnerId)"; + (guid,type,data,Path,StartDate,EndDate,ChannelId,IsMovie,IsSeries,EpisodeTitle,IsRepeat,CommunityRating,CustomRating,IndexNumber,IsLocked,Name,OfficialRating,MediaType,Overview,ParentIndexNumber,PremiereDate,ProductionYear,ParentId,Genres,InheritedParentalRatingValue,SortName,ForcedSortName,RunTimeTicks,Size,DateCreated,DateModified,PreferredMetadataLanguage,PreferredMetadataCountryCode,Width,Height,DateLastRefreshed,DateLastSaved,IsInMixedFolder,LockedFields,Studios,Audio,ExternalServiceId,Tags,IsFolder,UnratedType,TopParentId,TrailerTypes,CriticRating,CleanName,PresentationUniqueKey,OriginalTitle,PrimaryVersionId,DateLastMediaAdded,Album,LUFS,IsVirtualItem,SeriesName,UserDataKey,SeasonName,SeasonId,SeriesId,ExternalSeriesId,Tagline,ProviderIds,Images,ProductionLocations,ExtraIds,TotalBitrate,ExtraType,Artists,AlbumArtists,ExternalId,SeriesPresentationUniqueKey,ShowId,OwnerId) + values (@guid,@type,@data,@Path,@StartDate,@EndDate,@ChannelId,@IsMovie,@IsSeries,@EpisodeTitle,@IsRepeat,@CommunityRating,@CustomRating,@IndexNumber,@IsLocked,@Name,@OfficialRating,@MediaType,@Overview,@ParentIndexNumber,@PremiereDate,@ProductionYear,@ParentId,@Genres,@InheritedParentalRatingValue,@SortName,@ForcedSortName,@RunTimeTicks,@Size,@DateCreated,@DateModified,@PreferredMetadataLanguage,@PreferredMetadataCountryCode,@Width,@Height,@DateLastRefreshed,@DateLastSaved,@IsInMixedFolder,@LockedFields,@Studios,@Audio,@ExternalServiceId,@Tags,@IsFolder,@UnratedType,@TopParentId,@TrailerTypes,@CriticRating,@CleanName,@PresentationUniqueKey,@OriginalTitle,@PrimaryVersionId,@DateLastMediaAdded,@Album,@LUFS,@IsVirtualItem,@SeriesName,@UserDataKey,@SeasonName,@SeasonId,@SeriesId,@ExternalSeriesId,@Tagline,@ProviderIds,@Images,@ProductionLocations,@ExtraIds,@TotalBitrate,@ExtraType,@Artists,@AlbumArtists,@ExternalId,@SeriesPresentationUniqueKey,@ShowId,@OwnerId)"; private readonly IServerConfigurationManager _config; private readonly IServerApplicationHost _appHost; @@ -110,6 +110,7 @@ namespace Emby.Server.Implementations.Data "PrimaryVersionId", "DateLastMediaAdded", "Album", + "LUFS", "CriticRating", "IsVirtualItem", "SeriesName", @@ -489,6 +490,7 @@ namespace Emby.Server.Implementations.Data AddColumn(db, "TypedBaseItems", "PrimaryVersionId", "Text", existingColumnNames); AddColumn(db, "TypedBaseItems", "DateLastMediaAdded", "DATETIME", existingColumnNames); AddColumn(db, "TypedBaseItems", "Album", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "LUFS", "Float", existingColumnNames); AddColumn(db, "TypedBaseItems", "IsVirtualItem", "BIT", existingColumnNames); AddColumn(db, "TypedBaseItems", "SeriesName", "Text", existingColumnNames); AddColumn(db, "TypedBaseItems", "UserDataKey", "Text", existingColumnNames); @@ -906,6 +908,7 @@ namespace Emby.Server.Implementations.Data } saveItemStatement.TryBind("@Album", item.Album); + saveItemStatement.TryBind("@LUFS", item.LUFS); saveItemStatement.TryBind("@IsVirtualItem", item.IsVirtualItem); if (item is IHasSeries hasSeriesName) @@ -1756,6 +1759,11 @@ namespace Emby.Server.Implementations.Data item.Album = album; } + if (reader.TryGetSingle(index++, out var lUFS)) + { + item.LUFS = lUFS; + } + if (reader.TryGetSingle(index++, out var criticRating)) { item.CriticRating = criticRating; diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 8fa2f05662..7a6ed2cb80 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -906,6 +906,7 @@ namespace Emby.Server.Implementations.Dto // Add audio info if (item is Audio audio) { + dto.LUFS = audio.LUFS; dto.Album = audio.Album; if (audio.ExtraType.HasValue) { diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index adc7b2f953..1e868194e6 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -128,6 +128,13 @@ namespace MediaBrowser.Controller.Entities [JsonIgnore] public string Album { get; set; } + /// <summary> + /// Gets or sets the LUFS value. + /// </summary> + /// <value>The LUFS Value.</value> + [JsonIgnore] + public float LUFS { get; set; } + /// <summary> /// Gets or sets the channel identifier. /// </summary> diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs index 81f2f02bc5..df68299465 100644 --- a/MediaBrowser.Model/Configuration/LibraryOptions.cs +++ b/MediaBrowser.Model/Configuration/LibraryOptions.cs @@ -30,6 +30,8 @@ namespace MediaBrowser.Model.Configuration public bool EnableRealtimeMonitor { get; set; } + public bool EnableLUFSScan { get; set; } + public bool EnableChapterImageExtraction { get; set; } public bool ExtractChapterImagesDuringLibraryScan { get; set; } diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs index 2a86fded22..27154c2978 100644 --- a/MediaBrowser.Model/Dto/BaseItemDto.cs +++ b/MediaBrowser.Model/Dto/BaseItemDto.cs @@ -779,6 +779,12 @@ namespace MediaBrowser.Model.Dto /// <value>The timer identifier.</value> public string TimerId { get; set; } + /// <summary> + /// Gets or sets the LUFS value. + /// </summary> + /// <value>The LUFS Value.</value> + public float LUFS { get; set; } + /// <summary> /// Gets or sets the current program. /// </summary> diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index b8578c46f8..e1dcbc9939 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -1,6 +1,9 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; using System.Linq; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Enums; @@ -14,6 +17,7 @@ using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.MediaInfo; +using Microsoft.Extensions.Logging; using TagLib; namespace MediaBrowser.Providers.MediaInfo @@ -23,6 +27,10 @@ namespace MediaBrowser.Providers.MediaInfo /// </summary> public class AudioFileProber { + // Default LUFS value for use with the web interface, at -18db gain will be 1(no db gain). + private const float DefaultLUFSValue = -18; + + private readonly ILogger<AudioFileProber> _logger; private readonly IMediaEncoder _mediaEncoder; private readonly IItemRepository _itemRepo; private readonly ILibraryManager _libraryManager; @@ -31,16 +39,19 @@ namespace MediaBrowser.Providers.MediaInfo /// <summary> /// Initializes a new instance of the <see cref="AudioFileProber"/> class. /// </summary> + /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param> /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param> /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> /// <param name="itemRepo">Instance of the <see cref="IItemRepository"/> interface.</param> /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> public AudioFileProber( + ILogger<AudioFileProber> logger, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder, IItemRepository itemRepo, ILibraryManager libraryManager) { + _logger = logger; _mediaEncoder = mediaEncoder; _itemRepo = itemRepo; _libraryManager = libraryManager; @@ -89,6 +100,54 @@ namespace MediaBrowser.Providers.MediaInfo Fetch(item, result, cancellationToken); } + var libraryOptions = _libraryManager.GetLibraryOptions(item); + + if (libraryOptions.EnableLUFSScan) + { + string output; + using (var process = new Process() + { + StartInfo = new ProcessStartInfo + { + FileName = _mediaEncoder.EncoderPath, + Arguments = $"-hide_banner -i \"{path}\" -af ebur128=framelog=verbose -f null -", + RedirectStandardOutput = false, + RedirectStandardError = true + }, + }) + { + try + { + process.Start(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error starting ffmpeg"); + + throw; + } + + output = await process.StandardError.ReadToEndAsync(cancellationToken).ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); + MatchCollection split = Regex.Matches(output, @"I:\s+(.*?)\s+LUFS"); + + if (split.Count != 0) + { + item.LUFS = float.Parse(split[0].Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat); + } + else + { + item.LUFS = DefaultLUFSValue; + } + } + } + else + { + item.LUFS = DefaultLUFSValue; + } + + _logger.LogDebug("LUFS for {ItemName} is {LUFS}.", item.Name, item.LUFS); + return ItemUpdateType.MetadataImport; } @@ -196,6 +255,7 @@ namespace MediaBrowser.Providers.MediaInfo audio.Album = tags.Album; audio.IndexNumber = Convert.ToInt32(tags.Track); audio.ParentIndexNumber = Convert.ToInt32(tags.Disc); + if (tags.Year != 0) { var year = Convert.ToInt32(tags.Year); diff --git a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs index 2800219552..114a929753 100644 --- a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs @@ -79,7 +79,7 @@ namespace MediaBrowser.Providers.MediaInfo NamingOptions namingOptions) { _logger = loggerFactory.CreateLogger<ProbeProvider>(); - _audioProber = new AudioFileProber(mediaSourceManager, mediaEncoder, itemRepo, libraryManager); + _audioProber = new AudioFileProber(loggerFactory.CreateLogger<AudioFileProber>(), mediaSourceManager, mediaEncoder, itemRepo, libraryManager); _audioResolver = new AudioResolver(loggerFactory.CreateLogger<AudioResolver>(), localization, mediaEncoder, fileSystem, namingOptions); _subtitleResolver = new SubtitleResolver(loggerFactory.CreateLogger<SubtitleResolver>(), localization, mediaEncoder, fileSystem, namingOptions); _videoProber = new FFProbeVideoInfo( From eb52af4e6ab0a81b026063b2dd43dd81a1a08b8e Mon Sep 17 00:00:00 2001 From: Shadowghost <Shadowghost@users.noreply.github.com> Date: Mon, 15 May 2023 14:45:33 +0200 Subject: [PATCH 303/858] Fix playlists library and migration (#9770) --- .../Library/UserViewManager.cs | 20 +++++++++++---- .../Playlists/PlaylistManager.cs | 25 +++---------------- .../Playlists/PlaylistsFolder.cs | 6 ----- Jellyfin.Api/Controllers/ItemsController.cs | 7 ++++-- .../Controllers/PlaylistsController.cs | 10 -------- .../Migrations/Routines/FixPlaylistOwner.cs | 6 +++-- .../Playlists/IPlaylistManager.cs | 7 +++--- 7 files changed, 30 insertions(+), 51 deletions(-) diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs index 17f1d1905f..2c3dc18574 100644 --- a/Emby.Server.Implementations/Library/UserViewManager.cs +++ b/Emby.Server.Implementations/Library/UserViewManager.cs @@ -46,10 +46,9 @@ namespace Emby.Server.Implementations.Library public Folder[] GetUserViews(UserViewQuery query) { var user = _userManager.GetUserById(query.UserId); - if (user is null) { - throw new ArgumentException("User Id specified in the query does not exist.", nameof(query)); + throw new ArgumentException("User id specified in the query does not exist.", nameof(query)); } var folders = _libraryManager.GetUserRootFolder() @@ -58,7 +57,6 @@ namespace Emby.Server.Implementations.Library .ToList(); var groupedFolders = new List<ICollectionFolder>(); - var list = new List<Folder>(); foreach (var folder in folders) @@ -66,6 +64,20 @@ namespace Emby.Server.Implementations.Library var collectionFolder = folder as ICollectionFolder; var folderViewType = collectionFolder?.CollectionType; + // Playlist library requires special handling because the folder only refrences user playlists + if (string.Equals(folderViewType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase)) + { + var items = folder.GetItemList(new InternalItemsQuery(user) + { + ParentId = folder.ParentId + }); + + if (!items.Any(item => item.IsVisible(user))) + { + continue; + } + } + if (UserView.IsUserSpecific(folder)) { list.Add(_libraryManager.GetNamedView(user, folder.Name, folder.Id, folderViewType, null)); @@ -132,14 +144,12 @@ namespace Emby.Server.Implementations.Library } var sorted = _libraryManager.Sort(list, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).ToList(); - var orders = user.GetPreferenceValues<Guid>(PreferenceKind.OrderedViews); return list .OrderBy(i => { var index = Array.IndexOf(orders, i.Id); - if (index == -1 && i is UserView view && !view.DisplayParentId.Equals(default)) diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index adb8ac7327..702f8d45bc 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -67,9 +67,8 @@ namespace Emby.Server.Implementations.Playlists public async Task<PlaylistCreationResult> CreatePlaylist(PlaylistCreationRequest options) { var name = options.Name; - var folderName = _fileSystem.GetValidFilename(name); - var parentFolder = GetPlaylistsFolder(Guid.Empty); + var parentFolder = GetPlaylistsFolder(options.UserId); if (parentFolder is null) { throw new ArgumentException(nameof(parentFolder)); @@ -80,7 +79,6 @@ namespace Emby.Server.Implementations.Playlists foreach (var itemId in options.ItemIdList) { var item = _libraryManager.GetItemById(itemId); - if (item is null) { throw new ArgumentException("No item exists with the supplied Id"); @@ -121,7 +119,6 @@ namespace Emby.Server.Implementations.Playlists } var user = _userManager.GetUserById(options.UserId); - var path = Path.Combine(parentFolder.Path, folderName); path = GetTargetPath(path); @@ -130,7 +127,6 @@ namespace Emby.Server.Implementations.Playlists try { Directory.CreateDirectory(path); - var playlist = new Playlist { Name = name, @@ -140,7 +136,6 @@ namespace Emby.Server.Implementations.Playlists }; playlist.SetMediaType(options.MediaType); - parentFolder.AddChild(playlist); await playlist.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { ForceSave = true }, CancellationToken.None) @@ -326,7 +321,8 @@ namespace Emby.Server.Implementations.Playlists } } - private void SavePlaylistFile(Playlist item) + /// <inheritdoc /> + public void SavePlaylistFile(Playlist item) { // this is probably best done as a metadata provider // saving a file over itself will require some work to prevent this from happening when not needed @@ -564,20 +560,5 @@ namespace Emby.Server.Implementations.Playlists } } } - - /// <inheritdoc /> - public async Task UpdatePlaylistAsync(Playlist playlist) - { - var currentPlaylist = (Playlist)_libraryManager.GetItemById(playlist.Id); - currentPlaylist.OwnerUserId = playlist.OwnerUserId; - currentPlaylist.Shares = playlist.Shares; - - await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); - - if (currentPlaylist.IsFile) - { - SavePlaylistFile(currentPlaylist); - } - } } } diff --git a/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs b/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs index e2f2e436f2..5492097152 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs @@ -27,11 +27,6 @@ namespace Emby.Server.Implementations.Playlists [JsonIgnore] public override string CollectionType => MediaBrowser.Model.Entities.CollectionType.Playlists; - public override bool IsVisible(User user) - { - return base.IsVisible(user) && GetChildren(user, true).Any(); - } - protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user) { return base.GetEligibleChildrenForRecursiveChildren(user).OfType<Playlist>(); @@ -47,7 +42,6 @@ namespace Emby.Server.Implementations.Playlists query.Recursive = true; query.IncludeItemTypes = new[] { BaseItemKind.Playlist }; - query.Parent = null; return LibraryManager.GetItemsResult(query); } diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 377526729a..d4116116bd 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -503,6 +503,7 @@ public class ItemsController : BaseJellyfinApiController } } + query.Parent = null; result = folder.GetItems(query); } else @@ -511,10 +512,12 @@ public class ItemsController : BaseJellyfinApiController result = new QueryResult<BaseItem>(itemsArray); } + // result might include items not accessible by the user, DtoService will remove them + var accessibleItems = _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user); return new QueryResult<BaseItemDto>( startIndex, - result.TotalRecordCount, - _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user)); + accessibleItems.Count, + accessibleItems); } /// <summary> diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index 20995bf1b4..8d2a738d4a 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -65,14 +65,12 @@ public class PlaylistsController : BaseJellyfinApiController /// <param name="mediaType">The media type.</param> /// <param name="createPlaylistRequest">The create playlist payload.</param> /// <response code="200">Playlist created.</response> - /// <response code="403">User does not have permission to create playlists.</response> /// <returns> /// A <see cref="Task" /> that represents the asynchronous operation to create a playlist. /// The task result contains an <see cref="OkResult"/> indicating success. /// </returns> [HttpPost] [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist( [FromQuery, ParameterObsolete] string? name, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder)), ParameterObsolete] IReadOnlyList<Guid> ids, @@ -105,11 +103,9 @@ public class PlaylistsController : BaseJellyfinApiController /// <param name="ids">Item id, comma delimited.</param> /// <param name="userId">The userId.</param> /// <response code="204">Items added to playlist.</response> - /// <response code="403">User does not have permission to add items to playlist.</response> /// <returns>An <see cref="NoContentResult"/> on success.</returns> [HttpPost("{playlistId}/Items")] [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task<ActionResult> AddToPlaylist( [FromRoute, Required] Guid playlistId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids, @@ -127,11 +123,9 @@ public class PlaylistsController : BaseJellyfinApiController /// <param name="itemId">The item id.</param> /// <param name="newIndex">The new index.</param> /// <response code="204">Item moved to new index.</response> - /// <response code="403">User does not have permission to move item.</response> /// <returns>An <see cref="NoContentResult"/> on success.</returns> [HttpPost("{playlistId}/Items/{itemId}/Move/{newIndex}")] [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task<ActionResult> MoveItem( [FromRoute, Required] string playlistId, [FromRoute, Required] string itemId, @@ -147,11 +141,9 @@ public class PlaylistsController : BaseJellyfinApiController /// <param name="playlistId">The playlist id.</param> /// <param name="entryIds">The item ids, comma delimited.</param> /// <response code="204">Items removed.</response> - /// <response code="403">User does not have permission to get playlist.</response> /// <returns>An <see cref="NoContentResult"/> on success.</returns> [HttpDelete("{playlistId}/Items")] [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task<ActionResult> RemoveFromPlaylist( [FromRoute, Required] string playlistId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] entryIds) @@ -173,12 +165,10 @@ public class PlaylistsController : BaseJellyfinApiController /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> /// <response code="200">Original playlist returned.</response> - /// <response code="403">User does not have permission to get playlist items.</response> /// <response code="404">Playlist not found.</response> /// <returns>The original playlist items.</returns> [HttpGet("{playlistId}/Items")] [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult<QueryResult<BaseItemDto>> GetPlaylistItems( [FromRoute, Required] Guid playlistId, diff --git a/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs b/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs index 1dd938b1be..cf31820034 100644 --- a/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs +++ b/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Threading; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; @@ -60,13 +61,14 @@ internal class FixPlaylistOwner : IMigrationRoutine { playlist.OwnerUserId = guid; playlist.Shares = shares.Where(x => x != firstEditShare).ToArray(); - _playlistManager.UpdatePlaylistAsync(playlist).GetAwaiter().GetResult(); + playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult(); + _playlistManager.SavePlaylistFile(playlist); } } else { playlist.OpenAccess = true; - _playlistManager.UpdatePlaylistAsync(playlist).GetAwaiter().GetResult(); + playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult(); } } } diff --git a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs index d889436629..d1a51c2cf6 100644 --- a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs +++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs @@ -66,10 +66,9 @@ namespace MediaBrowser.Controller.Playlists Task RemovePlaylistsAsync(Guid userId); /// <summary> - /// Updates a playlist. + /// Saves a playlist. /// </summary> - /// <param name="playlist">The updated playlist.</param> - /// <returns>Task.</returns> - Task UpdatePlaylistAsync(Playlist playlist); + /// <param name="item">The playlist.</param> + void SavePlaylistFile(Playlist item); } } From c809c36b30fe9fa4be34f15089a5855c343a0f4e Mon Sep 17 00:00:00 2001 From: Cody Robibero <cody@robibe.ro> Date: Mon, 15 May 2023 06:55:28 -0600 Subject: [PATCH 304/858] Fix readonlyspan usage --- .../LiveTv/Listings/SchedulesDirect.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index ca3e45707b..7645c6c52d 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -462,10 +462,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings } StringBuilder str = new StringBuilder("[", 1 + (programIds.Count * 13)); - foreach (ReadOnlySpan<char> i in programIds) + foreach (var i in programIds) { str.Append('"') - .Append(i.Slice(0, 10)) + .Append(i[..10]) .Append("\","); } From 155f3856c05645ac70f98029d66e5d465e3798fd Mon Sep 17 00:00:00 2001 From: Bill Thornton <billt2006@gmail.com> Date: Mon, 15 May 2023 15:28:33 -0400 Subject: [PATCH 305/858] Use default files to remove index.html from url --- Emby.Server.Implementations/ConfigurationOptions.cs | 2 +- Jellyfin.Api/Middleware/BaseUrlRedirectionMiddleware.cs | 2 -- Jellyfin.Server/Startup.cs | 8 +++++++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs index f0a4c8ffbd..630265dac9 100644 --- a/Emby.Server.Implementations/ConfigurationOptions.cs +++ b/Emby.Server.Implementations/ConfigurationOptions.cs @@ -14,7 +14,7 @@ namespace Emby.Server.Implementations public static Dictionary<string, string?> DefaultConfiguration => new Dictionary<string, string?> { { HostWebClientKey, bool.TrueString }, - { DefaultRedirectKey, "web/index.html" }, + { DefaultRedirectKey, "web/" }, { FfmpegProbeSizeKey, "1G" }, { FfmpegAnalyzeDurationKey, "200M" }, { PlaylistsAllowDuplicatesKey, bool.FalseString }, diff --git a/Jellyfin.Api/Middleware/BaseUrlRedirectionMiddleware.cs b/Jellyfin.Api/Middleware/BaseUrlRedirectionMiddleware.cs index 7bcc328aa1..2241c68e7a 100644 --- a/Jellyfin.Api/Middleware/BaseUrlRedirectionMiddleware.cs +++ b/Jellyfin.Api/Middleware/BaseUrlRedirectionMiddleware.cs @@ -48,8 +48,6 @@ public class BaseUrlRedirectionMiddleware if (string.IsNullOrEmpty(localPath) || string.Equals(localPath, baseUrlPrefix, StringComparison.OrdinalIgnoreCase) || string.Equals(localPath, baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase) - || string.Equals(localPath, baseUrlPrefix + "/web", StringComparison.OrdinalIgnoreCase) - || string.Equals(localPath, baseUrlPrefix + "/web/", StringComparison.OrdinalIgnoreCase) || !localPath.StartsWith(baseUrlPrefix, StringComparison.OrdinalIgnoreCase) ) { diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 57efd5820b..6394800f75 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -182,7 +182,7 @@ namespace Jellyfin.Server // This must be injected before any path related middleware. mainApp.UsePathTrim(); - mainApp.UseStaticFiles(); + if (appConfig.HostWebClient()) { var extensionProvider = new FileExtensionContentTypeProvider(); @@ -190,6 +190,11 @@ namespace Jellyfin.Server // subtitles octopus requires .data, .mem files. extensionProvider.Mappings.Add(".data", MediaTypeNames.Application.Octet); extensionProvider.Mappings.Add(".mem", MediaTypeNames.Application.Octet); + mainApp.UseDefaultFiles(new DefaultFilesOptions + { + FileProvider = new PhysicalFileProvider(_serverConfigurationManager.ApplicationPaths.WebPath), + RequestPath = "/web" + }); mainApp.UseStaticFiles(new StaticFileOptions { FileProvider = new PhysicalFileProvider(_serverConfigurationManager.ApplicationPaths.WebPath), @@ -200,6 +205,7 @@ namespace Jellyfin.Server mainApp.UseRobotsRedirection(); } + mainApp.UseStaticFiles(); mainApp.UseAuthentication(); mainApp.UseJellyfinApiSwagger(_serverConfigurationManager); mainApp.UseQueryStringDecoding(); From 61ca06662eae7f4e40ac798ddb4dc13c3a0f00ce Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 16 May 2023 16:42:11 +0000 Subject: [PATCH 306/858] chore(deps): update dependency microsoft.net.test.sdk to v17.6.0 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 6be5c50003..b112c6532f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -45,7 +45,7 @@ <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Logging" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Options" Version="7.0.1" /> - <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.5.0" /> + <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.6.0" /> <PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" /> <PackageVersion Include="MimeTypes" Version="2.4.0" /> <PackageVersion Include="Mono.Nat" Version="3.0.4" /> From 55b10d298c516c1f7ae297990f6a3f2816ca8920 Mon Sep 17 00:00:00 2001 From: Abhinivesh Vijayan <vabhinivesh@gmail.com> Date: Tue, 16 May 2023 17:39:06 +0000 Subject: [PATCH 307/858] Translated using Weblate (Malayalam) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ml/ --- Emby.Server.Implementations/Localization/Core/ml.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/ml.json b/Emby.Server.Implementations/Localization/Core/ml.json index acc7746c12..0620fbcdb0 100644 --- a/Emby.Server.Implementations/Localization/Core/ml.json +++ b/Emby.Server.Implementations/Localization/Core/ml.json @@ -119,5 +119,7 @@ "Genres": "വിഭാഗങ്ങൾ", "Channels": "ചാനലുകൾ", "TaskOptimizeDatabaseDescription": "ഡാറ്റാബേസ് ചുരുക്കുകയും സ്വതന്ത്ര ഇടം വെട്ടിച്ചുരുക്കുകയും ചെയ്യുന്നു. ലൈബ്രറി സ്‌കാൻ ചെയ്‌തതിനുശേഷം അല്ലെങ്കിൽ ഡാറ്റാബേസ് പരിഷ്‌ക്കരണങ്ങളെ സൂചിപ്പിക്കുന്ന മറ്റ് മാറ്റങ്ങൾ ചെയ്‌തതിന് ശേഷം ഈ ടാസ്‌ക് പ്രവർത്തിപ്പിക്കുന്നത് പ്രകടനം മെച്ചപ്പെടുത്തും.", - "TaskOptimizeDatabase": "ഡാറ്റാബേസ് ഒപ്റ്റിമൈസ് ചെയ്യുക" + "TaskOptimizeDatabase": "ഡാറ്റാബേസ് ഒപ്റ്റിമൈസ് ചെയ്യുക", + "HearingImpaired": "കേൾവി തകരാറുകൾ", + "External": "പുറമേയുള്ള" } From 3aa03a46402ea4293f35058909b81c4e914e17f3 Mon Sep 17 00:00:00 2001 From: Anvesh <anveshsai8@gmail.com> Date: Wed, 17 May 2023 07:34:58 +0000 Subject: [PATCH 308/858] Translated using Weblate (Telugu) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/te/ --- .../Localization/Core/te.json | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/te.json b/Emby.Server.Implementations/Localization/Core/te.json index a9a8ceae0e..24168b6112 100644 --- a/Emby.Server.Implementations/Localization/Core/te.json +++ b/Emby.Server.Implementations/Localization/Core/te.json @@ -19,5 +19,24 @@ "Channels": "ఛానెల్‌లు", "Books": "పుస్తకాలు", "Artists": "కళాకారులు", - "Albums": "ఆల్బమ్‌లు" + "Albums": "ఆల్బమ్‌లు", + "HearingImpaired": "వినికిడి లోపం", + "HomeVideos": "హోమ్ వీడియోలు", + "AppDeviceValues": "అప్లికేషన్ : {0}, పరికరం: {1}", + "Application": "అప్లికేషన్", + "AuthenticationSucceededWithUserName": "విజయవంతంగా ఆమోదించబడింది", + "CameraImageUploadedFrom": "{0} నుండి కొత్త కెమెరా చిత్రం అప్‌లోడ్ చేయబడింది", + "ChapterNameValue": "అధ్యాయం", + "DeviceOfflineWithName": "{0} డిస్‌కనెక్ట్ చేయబడింది", + "DeviceOnlineWithName": "{0} కనెక్ట్ చేయబడింది", + "External": "బాహ్య", + "FailedLoginAttemptWithUserName": "{0} నుండి విఫలమైన లాగిన్ ప్రయత్నం", + "HeaderFavoriteAlbums": "ఇష్టమైన ఆల్బమ్‌లు", + "HeaderFavoriteArtists": "ఇష్టమైన కళాకారులు", + "HeaderFavoriteEpisodes": "ఇష్టమైన ఎపిసోడ్‌లు", + "HeaderFavoriteShows": "ఇష్టమైన ప్రదర్శనలు", + "HeaderFavoriteSongs": "ఇష్టమైన పాటలు", + "HeaderLiveTV": "ప్రత్యక్ష TV", + "HeaderNextUp": "తదుపరి", + "HeaderRecordingGroups": "రికార్డింగ్ గుంపులు" } From 02a0c0abc1ce9e57feb0c6088a4781009f4af47d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 18 May 2023 16:32:47 -0600 Subject: [PATCH 309/858] chore(deps): update dependency playlistsnet to v1.3.2 (#9786) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index fa6bead8b4..a0bc053fd3 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -52,7 +52,7 @@ <PackageVersion Include="Moq" Version="4.18.4" /> <PackageVersion Include="NEbml" Version="0.11.0" /> <PackageVersion Include="Newtonsoft.Json" Version="13.0.3" /> - <PackageVersion Include="PlaylistsNET" Version="1.3.1" /> + <PackageVersion Include="PlaylistsNET" Version="1.3.2" /> <PackageVersion Include="prometheus-net.AspNetCore" Version="8.0.0" /> <PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" /> <PackageVersion Include="prometheus-net" Version="8.0.0" /> From c81508869b06e676bd0cfbb324e3e97804ac51e3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 19 May 2023 21:54:38 +0000 Subject: [PATCH 310/858] chore(deps): update dependency sqlitepclraw.bundle_e_sqlite3 to v2.1.5 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index a0bc053fd3..40ad567b35 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -70,7 +70,7 @@ <PackageVersion Include="SkiaSharp" Version="2.88.3" /> <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> <PackageVersion Include="SQLitePCL.pretty.netstandard" Version="3.1.0" /> - <PackageVersion Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.4" /> + <PackageVersion Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.5" /> <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.435" /> <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.4.0" /> <PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" /> From 6ddc449a89ac615deb8c6d736e9cac7af4e02b9c Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Tue, 2 Aug 2022 17:46:38 +0200 Subject: [PATCH 311/858] Implement NFO named season parsing --- .../Library/Resolvers/TV/SeasonResolver.cs | 26 +++++-- MediaBrowser.Controller/Entities/TV/Series.cs | 4 + .../Manager/MetadataService.cs | 1 + .../TV/SeriesMetadataService.cs | 78 ++++++++++++++----- .../Parsers/SeasonNfoParser.cs | 12 +++ .../Parsers/SeriesNfoParser.cs | 15 ++++ 6 files changed, 107 insertions(+), 29 deletions(-) diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs index 62a524d2eb..e9538a5c97 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs @@ -81,14 +81,24 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV if (season.IndexNumber.HasValue) { var seasonNumber = season.IndexNumber.Value; - - season.Name = seasonNumber == 0 ? - args.LibraryOptions.SeasonZeroDisplayName : - string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("NameSeasonNumber"), - seasonNumber, - args.LibraryOptions.PreferredMetadataLanguage); + if (string.IsNullOrEmpty(season.Name)) + { + var seasonNames = series.SeasonNames; + if (seasonNames.TryGetValue(seasonNumber, out var seasonName)) + { + season.Name = seasonName; + } + else + { + season.Name = seasonNumber == 0 ? + args.LibraryOptions.SeasonZeroDisplayName : + string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("NameSeasonNumber"), + seasonNumber, + args.LibraryOptions.PreferredMetadataLanguage); + } + } } return season; diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index e7a8a773ec..a49c1609d9 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -28,12 +28,16 @@ namespace MediaBrowser.Controller.Entities.TV public Series() { AirDays = Array.Empty<DayOfWeek>(); + SeasonNames = new Dictionary<int, string>(); } public DayOfWeek[] AirDays { get; set; } public string AirTime { get; set; } + [JsonIgnore] + public Dictionary<int, string> SeasonNames { get; set; } + [JsonIgnore] public override bool SupportsAddingToPlaylist => true; diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index 80f77f7c3a..834ef29f51 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -12,6 +12,7 @@ using Jellyfin.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs index 97f9383971..b99f1cbdbf 100644 --- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs +++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs @@ -41,7 +41,7 @@ namespace MediaBrowser.Providers.TV RemoveObsoleteEpisodes(item); RemoveObsoleteSeasons(item); - await FillInMissingSeasonsAsync(item, cancellationToken).ConfigureAwait(false); + await UpdateAndCreateSeasonsAsync(item, cancellationToken).ConfigureAwait(false); } /// <inheritdoc /> @@ -67,6 +67,24 @@ namespace MediaBrowser.Providers.TV var sourceItem = source.Item; var targetItem = target.Item; + var sourceSeasonNames = sourceItem.SeasonNames; + var targetSeasonNames = targetItem.SeasonNames; + + if (replaceData || targetSeasonNames.Count == 0) + { + targetItem.SeasonNames = sourceSeasonNames; + } + else if (targetSeasonNames.Count != sourceSeasonNames.Count || !sourceSeasonNames.Keys.All(targetSeasonNames.ContainsKey)) + { + foreach (var season in sourceSeasonNames) + { + var seasonNumber = season.Key; + if (!targetSeasonNames.ContainsKey(seasonNumber)) + { + targetItem.SeasonNames[seasonNumber] = season.Value; + } + } + } if (replaceData || string.IsNullOrEmpty(targetItem.AirTime)) { @@ -86,7 +104,7 @@ namespace MediaBrowser.Providers.TV private void RemoveObsoleteSeasons(Series series) { - // TODO Legacy. It's not really "physical" seasons as any virtual seasons are always converted to non-virtual in FillInMissingSeasonsAsync. + // TODO Legacy. It's not really "physical" seasons as any virtual seasons are always converted to non-virtual in UpdateAndCreateSeasonsAsync. var physicalSeasonNumbers = new HashSet<int>(); var virtualSeasons = new List<Season>(); foreach (var existingSeason in series.Children.OfType<Season>()) @@ -177,36 +195,43 @@ namespace MediaBrowser.Providers.TV } /// <summary> - /// Creates seasons for all episodes that aren't in a season folder. + /// Creates seasons for all episodes if they don't exist. /// If no season number can be determined, a dummy season will be created. + /// Updates seasons names. /// </summary> /// <param name="series">The series.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>The async task.</returns> - private async Task FillInMissingSeasonsAsync(Series series, CancellationToken cancellationToken) + private async Task UpdateAndCreateSeasonsAsync(Series series, CancellationToken cancellationToken) { + var seasonNames = series.SeasonNames; var seriesChildren = series.GetRecursiveChildren(i => i is Episode || i is Season); - var episodesInSeriesFolder = seriesChildren + var seasons = seriesChildren.OfType<Season>().ToList(); + var uniqueSeasonNumbers = seriesChildren .OfType<Episode>() - .Where(i => !i.IsInSeasonFolder); - - List<Season> seasons = seriesChildren.OfType<Season>().ToList(); + .Select(e => e.ParentIndexNumber >= 0 ? e.ParentIndexNumber : null) + .Distinct(); // Loop through the unique season numbers - foreach (var episode in episodesInSeriesFolder) + foreach (var seasonNumber in uniqueSeasonNumbers) { // Null season numbers will have a 'dummy' season created because seasons are always required. - var seasonNumber = episode.ParentIndexNumber >= 0 ? episode.ParentIndexNumber : null; var existingSeason = seasons.FirstOrDefault(i => i.IndexNumber == seasonNumber); + string? seasonName = null; + + if (seasonNumber.HasValue && seasonNames.ContainsKey(seasonNumber.Value)) + { + seasonName = seasonNames[seasonNumber.Value]; + } if (existingSeason is null) { - var season = await CreateSeasonAsync(series, seasonNumber, cancellationToken).ConfigureAwait(false); - seasons.Add(season); + var season = await CreateSeasonAsync(series, seasonName, seasonNumber, cancellationToken).ConfigureAwait(false); + series.AddChild(season); } - else if (existingSeason.IsVirtualItem) + else { - existingSeason.IsVirtualItem = false; + existingSeason.Name = GetValidSeasonNameForSeries(series, seasonName, seasonNumber); await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false); } } @@ -216,21 +241,17 @@ namespace MediaBrowser.Providers.TV /// Creates a new season, adds it to the database by linking it to the [series] and refreshes the metadata. /// </summary> /// <param name="series">The series.</param> + /// <param name="seasonName">The season name.</param> /// <param name="seasonNumber">The season number.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>The newly created season.</returns> private async Task<Season> CreateSeasonAsync( Series series, + string? seasonName, int? seasonNumber, CancellationToken cancellationToken) { - string seasonName = seasonNumber switch - { - null => _localizationManager.GetLocalizedString("NameSeasonUnknown"), - 0 => LibraryManager.GetLibraryOptions(series).SeasonZeroDisplayName, - _ => string.Format(CultureInfo.InvariantCulture, _localizationManager.GetLocalizedString("NameSeasonNumber"), seasonNumber.Value) - }; - + seasonName = GetValidSeasonNameForSeries(series, seasonName, seasonNumber); Logger.LogInformation("Creating Season {SeasonName} entry for {SeriesName}", seasonName, series.Name); var season = new Season @@ -251,5 +272,20 @@ namespace MediaBrowser.Providers.TV return season; } + + private string GetValidSeasonNameForSeries(Series series, string? seasonName, int? seasonNumber) + { + if (string.IsNullOrEmpty(seasonName)) + { + seasonName = seasonNumber switch + { + null => _localizationManager.GetLocalizedString("NameSeasonUnknown"), + 0 => LibraryManager.GetLibraryOptions(series).SeasonZeroDisplayName, + _ => string.Format(CultureInfo.InvariantCulture, _localizationManager.GetLocalizedString("NameSeasonNumber"), seasonNumber.Value) + }; + } + + return seasonName; + } } } diff --git a/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs index 2f5fd40e2f..51d5f932bc 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs @@ -55,6 +55,18 @@ namespace MediaBrowser.XbmcMetadata.Parsers break; } + case "seasonname": + { + var name = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(name)) + { + item.Name = name; + } + + break; + } + default: base.FetchDataFromXmlNode(reader, itemResult); break; diff --git a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs index 3011d65a6d..f22b861eba 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Globalization; using System.Xml; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Entities.TV; @@ -110,6 +112,19 @@ namespace MediaBrowser.XbmcMetadata.Parsers break; } + case "namedseason": + { + var parsed = int.TryParse(reader.GetAttribute("number"), NumberStyles.Integer, CultureInfo.InvariantCulture, out var seasonNumber); + var name = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(name) && parsed) + { + item.SeasonNames[seasonNumber] = name; + } + + break; + } + default: base.FetchDataFromXmlNode(reader, itemResult); break; From a496da24e3bebe66e5ed6d61a7cd1d51311d4c48 Mon Sep 17 00:00:00 2001 From: Shadowghost <Shadowghost@users.noreply.github.com> Date: Sun, 21 May 2023 13:52:43 +0200 Subject: [PATCH 312/858] Apply suggestions from code review Co-authored-by: Bond-009 <bond.009@outlook.com> --- MediaBrowser.Providers/TV/SeriesMetadataService.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs index b99f1cbdbf..85e2481147 100644 --- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs +++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs @@ -79,10 +79,7 @@ namespace MediaBrowser.Providers.TV foreach (var season in sourceSeasonNames) { var seasonNumber = season.Key; - if (!targetSeasonNames.ContainsKey(seasonNumber)) - { - targetItem.SeasonNames[seasonNumber] = season.Value; - } + targetSeasonNames.TryAdd(seasonNumber, season.Value); } } @@ -219,9 +216,9 @@ namespace MediaBrowser.Providers.TV var existingSeason = seasons.FirstOrDefault(i => i.IndexNumber == seasonNumber); string? seasonName = null; - if (seasonNumber.HasValue && seasonNames.ContainsKey(seasonNumber.Value)) + if (seasonNumber.HasValue && seasonNames.TryGetValue(seasonNumber.Value, out var tmp)) { - seasonName = seasonNames[seasonNumber.Value]; + seasonName = tmp; } if (existingSeason is null) From 144cb60b90b6efe8ee449f016930a76e893b26be Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 21 May 2023 15:14:12 +0000 Subject: [PATCH 313/858] chore(deps): update dependency coverlet.collector to v6 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 40ad567b35..60eb3656fb 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -13,7 +13,7 @@ <PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.2.0" /> <PackageVersion Include="BlurHashSharp" Version="1.2.0" /> <PackageVersion Include="CommandLineParser" Version="2.9.1" /> - <PackageVersion Include="coverlet.collector" Version="3.2.0" /> + <PackageVersion Include="coverlet.collector" Version="6.0.0" /> <PackageVersion Include="Diacritics" Version="3.3.18" /> <PackageVersion Include="DiscUtils.Udf" Version="0.16.13" /> <PackageVersion Include="DotNet.Glob" Version="3.1.3" /> From bf90f502ae62c8aa0c63dca7907106b843cc6639 Mon Sep 17 00:00:00 2001 From: PakuDrag <iceclout@tutanota.com> Date: Sat, 20 May 2023 18:17:01 +0000 Subject: [PATCH 314/858] Translated using Weblate (Turkish) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/tr/ --- Emby.Server.Implementations/Localization/Core/tr.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json index b802db9822..9a140f8712 100644 --- a/Emby.Server.Implementations/Localization/Core/tr.json +++ b/Emby.Server.Implementations/Localization/Core/tr.json @@ -123,5 +123,6 @@ "TaskOptimizeDatabase": "Veritabanını optimize et", "TaskKeyframeExtractorDescription": "Daha hassas HLS çalma listeleri oluşturmak için video dosyalarından kareleri çıkarır. Bu görev uzun bir süre çalışabilir.", "TaskKeyframeExtractor": "Kare Ayırt Edici", - "External": "Harici" + "External": "Harici", + "HearingImpaired": "Duyma engelli" } From 45e99d2523e0da8c642731eeb371d6c373c5856f Mon Sep 17 00:00:00 2001 From: Shimul Roy <stenasaha@gmail.com> Date: Sat, 20 May 2023 09:40:38 +0000 Subject: [PATCH 315/858] Translated using Weblate (Bengali) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/bn/ --- Emby.Server.Implementations/Localization/Core/bn.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/bn.json b/Emby.Server.Implementations/Localization/Core/bn.json index 355fb3b21e..005926231d 100644 --- a/Emby.Server.Implementations/Localization/Core/bn.json +++ b/Emby.Server.Implementations/Localization/Core/bn.json @@ -117,7 +117,7 @@ "Forced": "জোরকরে", "TaskCleanActivityLogDescription": "নির্ধারিত সময়ের আগের কাজের হিসাব মুছে দিন খালি করুন.", "TaskCleanActivityLog": "কাজের ফাইল খালি করুন", - "Default": "প্রাথমিক", + "Default": "ডিফল্ট", "HearingImpaired": "দুর্বল শ্রবণক্ষমতাধরদের জন্য", "TaskOptimizeDatabaseDescription": "তথ্যভাণ্ডার সুবিন্যস্ত করে ও অব্যবহৃত জায়গা ছেড়ে দেয়। লাইব্রেরী স্ক্যান অথবা যেকোনো তথ্যভাণ্ডার পরিবর্তনের পর এই প্রক্রিয়া চালালে তথ্যভাণ্ডারের তথ্য প্রদান দ্রুততর হতে পারে।", "External": "বাহ্যিক", From b37e9209df94dcad757d0b9ad0a7a7076c3cf743 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Mon, 22 May 2023 10:39:48 +0200 Subject: [PATCH 316/858] Apply review suggestion --- .../LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index a86c329d62..1795e85a3c 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -661,7 +661,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun // Need a way to set the Receive timeout on the socket otherwise this might never timeout? try { - await udpClient.SendToAsync(discBytes, new IPEndPoint(IPAddress.Parse("255.255.255.255"), 65001), cancellationToken).ConfigureAwait(false); + await udpClient.SendToAsync(discBytes, new IPEndPoint(IPAddress.Broadcast, 65001), cancellationToken).ConfigureAwait(false); var receiveBuffer = new byte[8192]; while (!cancellationToken.IsCancellationRequested) From b44c9eb88eb2431295756c90b59f6751c35c6647 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Tue, 23 May 2023 16:23:25 +0200 Subject: [PATCH 317/858] Check for Imdb id for series --- .../Library/Resolvers/TV/SeriesResolver.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs index 8f69175d07..d4f275bed4 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs @@ -184,6 +184,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV { var justName = Path.GetFileName(path.AsSpan()); + var imdbId = justName.GetAttributeValue("imdbid"); + if (!string.IsNullOrEmpty(imdbId)) + { + item.SetProviderId(MetadataProvider.Imdb, imdbId); + } + var tvdbId = justName.GetAttributeValue("tvdbid"); if (!string.IsNullOrEmpty(tvdbId)) { From 7211571e2061ade1f2b17c738fdf95069f4690a4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 24 May 2023 16:11:47 +0000 Subject: [PATCH 318/858] chore(deps): update dependency lrcparser to v2023.524.0 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 60eb3656fb..bb5b72d210 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -21,7 +21,7 @@ <PackageVersion Include="FsCheck.Xunit" Version="2.16.5" /> <PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" /> <PackageVersion Include="libse" Version="3.6.13" /> - <PackageVersion Include="LrcParser" Version="2023.308.0" /> + <PackageVersion Include="LrcParser" Version="2023.524.0" /> <PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" /> <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.5" /> <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.5" /> From 9839c0e97a845fd2f5a4bfa3d9c1a7980a8fc579 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 May 2023 14:18:46 +0000 Subject: [PATCH 319/858] chore(deps): update ci dependencies --- .github/workflows/codeql-analysis.yml | 8 ++++---- .github/workflows/openapi.yml | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 8a806f9419..51fb446eeb 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -22,16 +22,16 @@ jobs: - name: Checkout repository uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 - name: Setup .NET - uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # v3.0.3 + uses: actions/setup-dotnet@aa983c550dfda0d1722b6ac6aed55724ffacc6d3 # v3.1.0 with: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@29b1f65c5e92e24fe6b6647da1eaabe529cec70f # v2.3.3 + uses: github/codeql-action/init@f0e3dfb30302f8a0881bb509b044e0de4f6ef589 # v2.3.4 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@29b1f65c5e92e24fe6b6647da1eaabe529cec70f # v2.3.3 + uses: github/codeql-action/autobuild@f0e3dfb30302f8a0881bb509b044e0de4f6ef589 # v2.3.4 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@29b1f65c5e92e24fe6b6647da1eaabe529cec70f # v2.3.3 + uses: github/codeql-action/analyze@f0e3dfb30302f8a0881bb509b044e0de4f6ef589 # v2.3.4 diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index e970bc7062..c2387f2ef6 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -19,7 +19,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} - name: Setup .NET - uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # v3.0.3 + uses: actions/setup-dotnet@aa983c550dfda0d1722b6ac6aed55724ffacc6d3 # v3.1.0 with: dotnet-version: '7.0.x' - name: Generate openapi.json @@ -51,7 +51,7 @@ jobs: ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/${{ github.head_ref }}) git checkout --progress --force $ANCESTOR_REF - name: Setup .NET - uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # v3.0.3 + uses: actions/setup-dotnet@aa983c550dfda0d1722b6ac6aed55724ffacc6d3 # v3.1.0 with: dotnet-version: '7.0.x' - name: Generate openapi.json From a381cd3c7652e4c802e697e367370f4dba3987f6 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Thu, 25 May 2023 17:10:53 +0200 Subject: [PATCH 320/858] Apply review suggestions --- MediaBrowser.Common/Net/NetworkExtensions.cs | 52 +++++++++++--------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/MediaBrowser.Common/Net/NetworkExtensions.cs b/MediaBrowser.Common/Net/NetworkExtensions.cs index 227f0483f4..8a14bf48db 100644 --- a/MediaBrowser.Common/Net/NetworkExtensions.cs +++ b/MediaBrowser.Common/Net/NetworkExtensions.cs @@ -93,31 +93,36 @@ namespace MediaBrowser.Common.Net ArgumentNullException.ThrowIfNull(mask); byte cidrnet = 0; - if (!mask.Equals(IPAddress.Any)) + if (mask.Equals(IPAddress.Any)) { - // GetAddressBytes - Span<byte> bytes = stackalloc byte[mask.AddressFamily == AddressFamily.InterNetwork ? 4 : 16]; - mask.TryWriteBytes(bytes, out _); + return cidrnet; + } - var zeroed = false; - for (var i = 0; i < bytes.Length; i++) + // GetAddressBytes + Span<byte> bytes = stackalloc byte[mask.AddressFamily == AddressFamily.InterNetwork ? 4 : 16]; + if (!mask.TryWriteBytes(bytes, out var bytesWritten)) + { + Console.WriteLine("Unable to write address bytes, only {Bytes} bytes written.", bytesWritten); + } + + var zeroed = false; + for (var i = 0; i < bytes.Length; i++) + { + for (int v = bytes[i]; (v & 0xFF) != 0; v <<= 1) { - for (int v = bytes[i]; (v & 0xFF) != 0; v <<= 1) + if (zeroed) { - if (zeroed) - { - // Invalid netmask. - return (byte)~cidrnet; - } + // Invalid netmask. + return (byte)~cidrnet; + } - if ((v & 0x80) == 0) - { - zeroed = true; - } - else - { - cidrnet++; - } + if ((v & 0x80) == 0) + { + zeroed = true; + } + else + { + cidrnet++; } } } @@ -273,10 +278,9 @@ namespace MediaBrowser.Common.Net } var hosts = new List<string>(); - var splitSpan = host.Split(':'); - while (splitSpan.MoveNext()) + foreach (var splitSpan in host.Split(':')) { - hosts.Add(splitSpan.Current.ToString()); + hosts.Add(splitSpan.ToString()); } if (hosts.Count <= 2) @@ -316,7 +320,7 @@ namespace MediaBrowser.Common.Net } else if (hosts.Count <= 9) // 8 octets + port { - splitSpan = host.Split('/'); + var splitSpan = host.Split('/'); if (splitSpan.MoveNext() && IPAddress.TryParse(splitSpan.Current, out var address)) { addresses = new[] { address }; From 71e867d10fb507941f2b88d2550f13101e12a312 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 26 May 2023 00:11:24 +0000 Subject: [PATCH 321/858] chore(deps): update github/codeql-action action to v2.3.5 --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 51fb446eeb..9a929a8b04 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@f0e3dfb30302f8a0881bb509b044e0de4f6ef589 # v2.3.4 + uses: github/codeql-action/init@0225834cc549ee0ca93cb085b92954821a145866 # v2.3.5 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@f0e3dfb30302f8a0881bb509b044e0de4f6ef589 # v2.3.4 + uses: github/codeql-action/autobuild@0225834cc549ee0ca93cb085b92954821a145866 # v2.3.5 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@f0e3dfb30302f8a0881bb509b044e0de4f6ef589 # v2.3.4 + uses: github/codeql-action/analyze@0225834cc549ee0ca93cb085b92954821a145866 # v2.3.5 From 81746666ded02f844605fb26bc1c0ac2dd650bb0 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Fri, 26 May 2023 10:59:49 +0200 Subject: [PATCH 322/858] Fix TotalRecordCount calculation --- Emby.Server.Implementations/Library/LibraryManager.cs | 9 ++++++++- Emby.Server.Implementations/Playlists/PlaylistsFolder.cs | 2 +- Jellyfin.Api/Controllers/ItemsController.cs | 6 ++---- MediaBrowser.Controller/Entities/Folder.cs | 4 ++-- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 75a1a5a4d8..ea45bf0ba0 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -1264,7 +1264,14 @@ namespace Emby.Server.Implementations.Library AddUserToQuery(query, query.User, allowExternalContent); } - return _itemRepository.GetItemList(query); + var itemList = _itemRepository.GetItemList(query); + var user = query.User; + if (user is not null) + { + return itemList.Where(i => i.IsVisible(user)).ToList(); + } + + return itemList; } public List<BaseItem> GetItemList(InternalItemsQuery query) diff --git a/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs b/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs index 5492097152..d67caa52dc 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs @@ -42,7 +42,7 @@ namespace Emby.Server.Implementations.Playlists query.Recursive = true; query.IncludeItemTypes = new[] { BaseItemKind.Playlist }; - return LibraryManager.GetItemsResult(query); + return QueryWithPostFiltering2(query); } public override string GetClientTypeName() diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index d4116116bd..7650b861f4 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -512,12 +512,10 @@ public class ItemsController : BaseJellyfinApiController result = new QueryResult<BaseItem>(itemsArray); } - // result might include items not accessible by the user, DtoService will remove them - var accessibleItems = _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user); return new QueryResult<BaseItemDto>( startIndex, - accessibleItems.Count, - accessibleItems); + result.TotalRecordCount, + _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user)); } /// <summary> diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index bccb4107ff..84952295c4 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -730,7 +730,7 @@ namespace MediaBrowser.Controller.Entities return LibraryManager.GetItemsResult(query); } - private QueryResult<BaseItem> QueryWithPostFiltering2(InternalItemsQuery query) + protected QueryResult<BaseItem> QueryWithPostFiltering2(InternalItemsQuery query) { var startIndex = query.StartIndex; var limit = query.Limit; @@ -1272,7 +1272,7 @@ namespace MediaBrowser.Controller.Entities { ArgumentNullException.ThrowIfNull(user); - return GetChildren(user, includeLinkedChildren, null); + return GetChildren(user, includeLinkedChildren, new InternalItemsQuery(user)); } public virtual List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) From 716bcc6410c91edd755ea294f5908b7f383fc326 Mon Sep 17 00:00:00 2001 From: cvium <clausvium@gmail.com> Date: Fri, 26 May 2023 19:40:40 +0200 Subject: [PATCH 323/858] chore: deprecate EasyPassword as it isn't very secure --- Jellyfin.Api/Controllers/UserController.cs | 26 ++------------ Jellyfin.Data/Entities/User.cs | 10 ------ .../Migrations/JellyfinDbModelSnapshot.cs | 34 ++++++++---------- .../Users/DefaultPasswordResetProvider.cs | 2 -- .../Users/UserManager.cs | 36 ------------------- .../Migrations/Routines/MigrateUserDb.cs | 1 - .../Library/IUserManager.cs | 16 --------- MediaBrowser.Model/Dto/UserDto.cs | 1 + 8 files changed, 18 insertions(+), 108 deletions(-) diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index e495288670..f7202a34c6 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -323,36 +323,16 @@ public class UserController : BaseJellyfinApiController /// <response code="404">User not found.</response> /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="ForbidResult"/> or a <see cref="NotFoundResult"/> on failure.</returns> [HttpPost("{userId}/EasyPassword")] + [Obsolete("Use Quick Connect instead")] [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task<ActionResult> UpdateUserEasyPassword( + public ActionResult UpdateUserEasyPassword( [FromRoute, Required] Guid userId, [FromBody, Required] UpdateUserEasyPassword request) { - if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true)) - { - return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the easy password."); - } - - var user = _userManager.GetUserById(userId); - - if (user is null) - { - return NotFound("User not found"); - } - - if (request.ResetPassword) - { - await _userManager.ResetEasyPassword(user).ConfigureAwait(false); - } - else - { - await _userManager.ChangeEasyPassword(user, request.NewPw ?? string.Empty, request.NewPassword ?? string.Empty).ConfigureAwait(false); - } - - return NoContent(); + return BadRequest("Deprecated"); } /// <summary> diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs index 606e1b5427..58ddaaf83a 100644 --- a/Jellyfin.Data/Entities/User.cs +++ b/Jellyfin.Data/Entities/User.cs @@ -91,16 +91,6 @@ namespace Jellyfin.Data.Entities [StringLength(65535)] public string? Password { get; set; } - /// <summary> - /// Gets or sets the user's easy password, or <c>null</c> if none is set. - /// </summary> - /// <remarks> - /// Max length = 65535. - /// </remarks> - [MaxLength(65535)] - [StringLength(65535)] - public string? EasyPassword { get; set; } - /// <summary> /// Gets or sets a value indicating whether the user must update their password. /// </summary> diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs index dd5f7f0121..d23508096f 100644 --- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs +++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs @@ -15,9 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder - .HasDefaultSchema("jellyfin") - .HasAnnotation("ProductVersion", "6.0.9"); + modelBuilder.HasAnnotation("ProductVersion", "7.0.5"); modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => { @@ -41,7 +39,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("UserId"); - b.ToTable("AccessSchedules", "jellyfin"); + b.ToTable("AccessSchedules"); }); modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => @@ -89,7 +87,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("DateCreated"); - b.ToTable("ActivityLogs", "jellyfin"); + b.ToTable("ActivityLogs"); }); modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b => @@ -121,7 +119,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("UserId", "ItemId", "Client", "Key") .IsUnique(); - b.ToTable("CustomItemDisplayPreferences", "jellyfin"); + b.ToTable("CustomItemDisplayPreferences"); }); modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => @@ -178,7 +176,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("UserId", "ItemId", "Client") .IsUnique(); - b.ToTable("DisplayPreferences", "jellyfin"); + b.ToTable("DisplayPreferences"); }); modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => @@ -200,7 +198,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("DisplayPreferencesId"); - b.ToTable("HomeSection", "jellyfin"); + b.ToTable("HomeSection"); }); modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => @@ -225,7 +223,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("UserId") .IsUnique(); - b.ToTable("ImageInfos", "jellyfin"); + b.ToTable("ImageInfos"); }); modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => @@ -269,7 +267,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("UserId"); - b.ToTable("ItemDisplayPreferences", "jellyfin"); + b.ToTable("ItemDisplayPreferences"); }); modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => @@ -300,7 +298,7 @@ namespace Jellyfin.Server.Implementations.Migrations .IsUnique() .HasFilter("[UserId] IS NOT NULL"); - b.ToTable("Permissions", "jellyfin"); + b.ToTable("Permissions"); }); modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => @@ -333,7 +331,7 @@ namespace Jellyfin.Server.Implementations.Migrations .IsUnique() .HasFilter("[UserId] IS NOT NULL"); - b.ToTable("Preferences", "jellyfin"); + b.ToTable("Preferences"); }); modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b => @@ -362,7 +360,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("AccessToken") .IsUnique(); - b.ToTable("ApiKeys", "jellyfin"); + b.ToTable("ApiKeys"); }); modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => @@ -420,7 +418,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("UserId", "DeviceId"); - b.ToTable("Devices", "jellyfin"); + b.ToTable("Devices"); }); modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b => @@ -441,7 +439,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("DeviceId") .IsUnique(); - b.ToTable("DeviceOptions", "jellyfin"); + b.ToTable("DeviceOptions"); }); modelBuilder.Entity("Jellyfin.Data.Entities.User", b => @@ -465,10 +463,6 @@ namespace Jellyfin.Server.Implementations.Migrations b.Property<bool>("DisplayMissingEpisodes") .HasColumnType("INTEGER"); - b.Property<string>("EasyPassword") - .HasMaxLength(65535) - .HasColumnType("TEXT"); - b.Property<bool>("EnableAutoLogin") .HasColumnType("INTEGER"); @@ -554,7 +548,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("Username") .IsUnique(); - b.ToTable("Users", "jellyfin"); + b.ToTable("Users"); }); modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => diff --git a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs index 9601954671..cefbd0624d 100644 --- a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs +++ b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs @@ -114,8 +114,6 @@ namespace Jellyfin.Server.Implementations.Users await JsonSerializer.SerializeAsync(fileStream, spr).ConfigureAwait(false); } - user.EasyPassword = pin; - return new ForgotPasswordResult { Action = ForgotPasswordAction.PinCode, diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index c4756433e0..fa23fe148b 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -268,12 +268,6 @@ namespace Jellyfin.Server.Implementations.Users return ChangePassword(user, string.Empty); } - /// <inheritdoc/> - public Task ResetEasyPassword(User user) - { - return ChangeEasyPassword(user, string.Empty, null); - } - /// <inheritdoc/> public async Task ChangePassword(User user, string newPassword) { @@ -285,25 +279,6 @@ namespace Jellyfin.Server.Implementations.Users await _eventManager.PublishAsync(new UserPasswordChangedEventArgs(user)).ConfigureAwait(false); } - /// <inheritdoc/> - public async Task ChangeEasyPassword(User user, string newPassword, string? newPasswordSha1) - { - if (newPassword is not null) - { - newPasswordSha1 = _cryptoProvider.CreatePasswordHash(newPassword).ToString(); - } - - if (string.IsNullOrWhiteSpace(newPasswordSha1)) - { - throw new ArgumentNullException(nameof(newPasswordSha1)); - } - - user.EasyPassword = newPasswordSha1; - await UpdateUserAsync(user).ConfigureAwait(false); - - await _eventManager.PublishAsync(new UserPasswordChangedEventArgs(user)).ConfigureAwait(false); - } - /// <inheritdoc/> public UserDto GetUserDto(User user, string? remoteEndPoint = null) { @@ -315,7 +290,6 @@ namespace Jellyfin.Server.Implementations.Users ServerId = _appHost.SystemId, HasPassword = hasPassword, HasConfiguredPassword = hasPassword, - HasConfiguredEasyPassword = !string.IsNullOrEmpty(user.EasyPassword), EnableAutoLogin = user.EnableAutoLogin, LastLoginDate = user.LastLoginDate, LastActivityDate = user.LastActivityDate, @@ -832,16 +806,6 @@ namespace Jellyfin.Server.Implementations.Users } } - if (!success - && _networkManager.IsInLocalNetwork(remoteEndPoint) - && user?.EnableLocalPassword == true - && !string.IsNullOrEmpty(user.EasyPassword)) - { - // Check easy password - var passwordHash = PasswordHash.Parse(user.EasyPassword); - success = _cryptoProvider.Verify(passwordHash, password); - } - return (authenticationProvider, username, success); } diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs index 9bf1e6b808..0186500a12 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs @@ -127,7 +127,6 @@ namespace Jellyfin.Server.Migrations.Routines RememberSubtitleSelections = config.RememberSubtitleSelections, SubtitleLanguagePreference = config.SubtitleLanguagePreference, Password = mockup.Password, - EasyPassword = mockup.EasyPassword, LastLoginDate = mockup.LastLoginDate, LastActivityDate = mockup.LastActivityDate }; diff --git a/MediaBrowser.Controller/Library/IUserManager.cs b/MediaBrowser.Controller/Library/IUserManager.cs index 37b4afcf32..6d6a532dba 100644 --- a/MediaBrowser.Controller/Library/IUserManager.cs +++ b/MediaBrowser.Controller/Library/IUserManager.cs @@ -96,13 +96,6 @@ namespace MediaBrowser.Controller.Library /// <returns>Task.</returns> Task ResetPassword(User user); - /// <summary> - /// Resets the easy password. - /// </summary> - /// <param name="user">The user.</param> - /// <returns>Task.</returns> - Task ResetEasyPassword(User user); - /// <summary> /// Changes the password. /// </summary> @@ -111,15 +104,6 @@ namespace MediaBrowser.Controller.Library /// <returns>Awaitable task.</returns> Task ChangePassword(User user, string newPassword); - /// <summary> - /// Changes the easy password. - /// </summary> - /// <param name="user">The user.</param> - /// <param name="newPassword">New password to use.</param> - /// <param name="newPasswordSha1">Hash of new password.</param> - /// <returns>Task.</returns> - Task ChangeEasyPassword(User user, string newPassword, string newPasswordSha1); - /// <summary> /// Gets the user dto. /// </summary> diff --git a/MediaBrowser.Model/Dto/UserDto.cs b/MediaBrowser.Model/Dto/UserDto.cs index 256d7b10f1..05019741e0 100644 --- a/MediaBrowser.Model/Dto/UserDto.cs +++ b/MediaBrowser.Model/Dto/UserDto.cs @@ -66,6 +66,7 @@ namespace MediaBrowser.Model.Dto /// Gets or sets a value indicating whether this instance has configured easy password. /// </summary> /// <value><c>true</c> if this instance has configured easy password; otherwise, <c>false</c>.</value> + [Obsolete("Easy Password has been replaced with Quick Connect")] public bool HasConfiguredEasyPassword { get; set; } /// <summary> From b33f46560decc7f8e94f74714a14ab917cf7f00f Mon Sep 17 00:00:00 2001 From: cvium <clausvium@gmail.com> Date: Fri, 26 May 2023 19:45:40 +0200 Subject: [PATCH 324/858] use 403 instead to avoid compat issues with swagger spec --- Jellyfin.Api/Controllers/UserController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index f7202a34c6..530bd96031 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -332,7 +332,7 @@ public class UserController : BaseJellyfinApiController [FromRoute, Required] Guid userId, [FromBody, Required] UpdateUserEasyPassword request) { - return BadRequest("Deprecated"); + return Forbid(); } /// <summary> From 3bdef7207c39d077c8548006ae4a545e2fa27dae Mon Sep 17 00:00:00 2001 From: cvium <clausvium@gmail.com> Date: Fri, 26 May 2023 19:45:53 +0200 Subject: [PATCH 325/858] chore: add db migrations --- ...30526173516_RemoveEasyPassword.Designer.cs | 650 ++++++++++++++++++ .../20230526173516_RemoveEasyPassword.cs | 164 +++++ 2 files changed, 814 insertions(+) create mode 100644 Jellyfin.Server.Implementations/Migrations/20230526173516_RemoveEasyPassword.Designer.cs create mode 100644 Jellyfin.Server.Implementations/Migrations/20230526173516_RemoveEasyPassword.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20230526173516_RemoveEasyPassword.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20230526173516_RemoveEasyPassword.Designer.cs new file mode 100644 index 0000000000..00ccd9f0ff --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20230526173516_RemoveEasyPassword.Designer.cs @@ -0,0 +1,650 @@ +// <auto-generated /> +using System; +using Jellyfin.Server.Implementations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + [DbContext(typeof(JellyfinDbContext))] + [Migration("20230526173516_RemoveEasyPassword")] + partial class RemoveEasyPassword + { + /// <inheritdoc /> + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.5"); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<int>("DayOfWeek") + .HasColumnType("INTEGER"); + + b.Property<double>("EndHour") + .HasColumnType("REAL"); + + b.Property<double>("StartHour") + .HasColumnType("REAL"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccessSchedules"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<DateTime>("DateCreated") + .HasColumnType("TEXT"); + + b.Property<string>("ItemId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property<int>("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property<string>("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property<string>("Overview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property<uint>("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property<string>("ShortOverview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property<string>("Type") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DateCreated"); + + b.ToTable("ActivityLogs"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<string>("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<Guid>("ItemId") + .HasColumnType("TEXT"); + + b.Property<string>("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.Property<string>("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client", "Key") + .IsUnique(); + + b.ToTable("CustomItemDisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<int>("ChromecastVersion") + .HasColumnType("INTEGER"); + + b.Property<string>("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<string>("DashboardTheme") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<bool>("EnableNextVideoInfoOverlay") + .HasColumnType("INTEGER"); + + b.Property<int?>("IndexBy") + .HasColumnType("INTEGER"); + + b.Property<Guid>("ItemId") + .HasColumnType("TEXT"); + + b.Property<int>("ScrollDirection") + .HasColumnType("INTEGER"); + + b.Property<bool>("ShowBackdrop") + .HasColumnType("INTEGER"); + + b.Property<bool>("ShowSidebar") + .HasColumnType("INTEGER"); + + b.Property<int>("SkipBackwardLength") + .HasColumnType("INTEGER"); + + b.Property<int>("SkipForwardLength") + .HasColumnType("INTEGER"); + + b.Property<string>("TvHome") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client") + .IsUnique(); + + b.ToTable("DisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<int>("DisplayPreferencesId") + .HasColumnType("INTEGER"); + + b.Property<int>("Order") + .HasColumnType("INTEGER"); + + b.Property<int>("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DisplayPreferencesId"); + + b.ToTable("HomeSection"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<DateTime>("LastModified") + .HasColumnType("TEXT"); + + b.Property<string>("Path") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property<Guid?>("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("ImageInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<string>("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<int?>("IndexBy") + .HasColumnType("INTEGER"); + + b.Property<Guid>("ItemId") + .HasColumnType("TEXT"); + + b.Property<bool>("RememberIndexing") + .HasColumnType("INTEGER"); + + b.Property<bool>("RememberSorting") + .HasColumnType("INTEGER"); + + b.Property<string>("SortBy") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property<int>("SortOrder") + .HasColumnType("INTEGER"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.Property<int>("ViewType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemDisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<int>("Kind") + .HasColumnType("INTEGER"); + + b.Property<Guid?>("Permission_Permissions_Guid") + .HasColumnType("TEXT"); + + b.Property<uint>("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property<Guid?>("UserId") + .HasColumnType("TEXT"); + + b.Property<bool>("Value") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<int>("Kind") + .HasColumnType("INTEGER"); + + b.Property<Guid?>("Preference_Preferences_Guid") + .HasColumnType("TEXT"); + + b.Property<uint>("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property<Guid?>("UserId") + .HasColumnType("TEXT"); + + b.Property<string>("Value") + .IsRequired() + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Preferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<string>("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property<DateTime>("DateCreated") + .HasColumnType("TEXT"); + + b.Property<DateTime>("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property<string>("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccessToken") + .IsUnique(); + + b.ToTable("ApiKeys"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<string>("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property<string>("AppName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property<string>("AppVersion") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<DateTime>("DateCreated") + .HasColumnType("TEXT"); + + b.Property<DateTime>("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property<DateTime>("DateModified") + .HasColumnType("TEXT"); + + b.Property<string>("DeviceId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property<string>("DeviceName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property<bool>("IsActive") + .HasColumnType("INTEGER"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("AccessToken", "DateLastActivity"); + + b.HasIndex("DeviceId", "DateLastActivity"); + + b.HasIndex("UserId", "DeviceId"); + + b.ToTable("Devices"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<string>("CustomName") + .HasColumnType("TEXT"); + + b.Property<string>("DeviceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.ToTable("DeviceOptions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property<string>("AudioLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property<string>("AuthenticationProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property<bool>("DisplayCollectionsView") + .HasColumnType("INTEGER"); + + b.Property<bool>("DisplayMissingEpisodes") + .HasColumnType("INTEGER"); + + b.Property<bool>("EnableAutoLogin") + .HasColumnType("INTEGER"); + + b.Property<bool>("EnableLocalPassword") + .HasColumnType("INTEGER"); + + b.Property<bool>("EnableNextEpisodeAutoPlay") + .HasColumnType("INTEGER"); + + b.Property<bool>("EnableUserPreferenceAccess") + .HasColumnType("INTEGER"); + + b.Property<bool>("HidePlayedInLatest") + .HasColumnType("INTEGER"); + + b.Property<long>("InternalId") + .HasColumnType("INTEGER"); + + b.Property<int>("InvalidLoginAttemptCount") + .HasColumnType("INTEGER"); + + b.Property<DateTime?>("LastActivityDate") + .HasColumnType("TEXT"); + + b.Property<DateTime?>("LastLoginDate") + .HasColumnType("TEXT"); + + b.Property<int?>("LoginAttemptsBeforeLockout") + .HasColumnType("INTEGER"); + + b.Property<int>("MaxActiveSessions") + .HasColumnType("INTEGER"); + + b.Property<int?>("MaxParentalAgeRating") + .HasColumnType("INTEGER"); + + b.Property<bool>("MustUpdatePassword") + .HasColumnType("INTEGER"); + + b.Property<string>("Password") + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.Property<string>("PasswordResetProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property<bool>("PlayDefaultAudioTrack") + .HasColumnType("INTEGER"); + + b.Property<bool>("RememberAudioSelections") + .HasColumnType("INTEGER"); + + b.Property<bool>("RememberSubtitleSelections") + .HasColumnType("INTEGER"); + + b.Property<int?>("RemoteClientBitrateLimit") + .HasColumnType("INTEGER"); + + b.Property<uint>("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property<string>("SubtitleLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property<int>("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property<int>("SyncPlayAccess") + .HasColumnType("INTEGER"); + + b.Property<string>("Username") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("AccessSchedules") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("DisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null) + .WithMany("HomeSections") + .HasForeignKey("DisplayPreferencesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithOne("ProfileImage") + .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("ItemDisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Permissions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Preferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => + { + b.HasOne("Jellyfin.Data.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Navigation("HomeSections"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Navigation("AccessSchedules"); + + b.Navigation("DisplayPreferences"); + + b.Navigation("ItemDisplayPreferences"); + + b.Navigation("Permissions"); + + b.Navigation("Preferences"); + + b.Navigation("ProfileImage"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/20230526173516_RemoveEasyPassword.cs b/Jellyfin.Server.Implementations/Migrations/20230526173516_RemoveEasyPassword.cs new file mode 100644 index 0000000000..9496ff3c0d --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20230526173516_RemoveEasyPassword.cs @@ -0,0 +1,164 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + /// <inheritdoc /> + public partial class RemoveEasyPassword : Migration + { + /// <inheritdoc /> + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "EasyPassword", + schema: "jellyfin", + table: "Users"); + + migrationBuilder.RenameTable( + name: "Users", + schema: "jellyfin", + newName: "Users"); + + migrationBuilder.RenameTable( + name: "Preferences", + schema: "jellyfin", + newName: "Preferences"); + + migrationBuilder.RenameTable( + name: "Permissions", + schema: "jellyfin", + newName: "Permissions"); + + migrationBuilder.RenameTable( + name: "ItemDisplayPreferences", + schema: "jellyfin", + newName: "ItemDisplayPreferences"); + + migrationBuilder.RenameTable( + name: "ImageInfos", + schema: "jellyfin", + newName: "ImageInfos"); + + migrationBuilder.RenameTable( + name: "HomeSection", + schema: "jellyfin", + newName: "HomeSection"); + + migrationBuilder.RenameTable( + name: "DisplayPreferences", + schema: "jellyfin", + newName: "DisplayPreferences"); + + migrationBuilder.RenameTable( + name: "Devices", + schema: "jellyfin", + newName: "Devices"); + + migrationBuilder.RenameTable( + name: "DeviceOptions", + schema: "jellyfin", + newName: "DeviceOptions"); + + migrationBuilder.RenameTable( + name: "CustomItemDisplayPreferences", + schema: "jellyfin", + newName: "CustomItemDisplayPreferences"); + + migrationBuilder.RenameTable( + name: "ApiKeys", + schema: "jellyfin", + newName: "ApiKeys"); + + migrationBuilder.RenameTable( + name: "ActivityLogs", + schema: "jellyfin", + newName: "ActivityLogs"); + + migrationBuilder.RenameTable( + name: "AccessSchedules", + schema: "jellyfin", + newName: "AccessSchedules"); + } + + /// <inheritdoc /> + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "jellyfin"); + + migrationBuilder.RenameTable( + name: "Users", + newName: "Users", + newSchema: "jellyfin"); + + migrationBuilder.RenameTable( + name: "Preferences", + newName: "Preferences", + newSchema: "jellyfin"); + + migrationBuilder.RenameTable( + name: "Permissions", + newName: "Permissions", + newSchema: "jellyfin"); + + migrationBuilder.RenameTable( + name: "ItemDisplayPreferences", + newName: "ItemDisplayPreferences", + newSchema: "jellyfin"); + + migrationBuilder.RenameTable( + name: "ImageInfos", + newName: "ImageInfos", + newSchema: "jellyfin"); + + migrationBuilder.RenameTable( + name: "HomeSection", + newName: "HomeSection", + newSchema: "jellyfin"); + + migrationBuilder.RenameTable( + name: "DisplayPreferences", + newName: "DisplayPreferences", + newSchema: "jellyfin"); + + migrationBuilder.RenameTable( + name: "Devices", + newName: "Devices", + newSchema: "jellyfin"); + + migrationBuilder.RenameTable( + name: "DeviceOptions", + newName: "DeviceOptions", + newSchema: "jellyfin"); + + migrationBuilder.RenameTable( + name: "CustomItemDisplayPreferences", + newName: "CustomItemDisplayPreferences", + newSchema: "jellyfin"); + + migrationBuilder.RenameTable( + name: "ApiKeys", + newName: "ApiKeys", + newSchema: "jellyfin"); + + migrationBuilder.RenameTable( + name: "ActivityLogs", + newName: "ActivityLogs", + newSchema: "jellyfin"); + + migrationBuilder.RenameTable( + name: "AccessSchedules", + newName: "AccessSchedules", + newSchema: "jellyfin"); + + migrationBuilder.AddColumn<string>( + name: "EasyPassword", + schema: "jellyfin", + table: "Users", + type: "TEXT", + maxLength: 65535, + nullable: true); + } + } +} From 57d8452e2a093c733d25d662e72f43a4c4c55eea Mon Sep 17 00:00:00 2001 From: cvium <clausvium@gmail.com> Date: Fri, 26 May 2023 19:52:27 +0200 Subject: [PATCH 326/858] refactor: admin users must have a non-empty password --- Jellyfin.Server.Implementations/Users/UserManager.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index c4756433e0..04c3d8a70d 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -278,6 +278,10 @@ namespace Jellyfin.Server.Implementations.Users public async Task ChangePassword(User user, string newPassword) { ArgumentNullException.ThrowIfNull(user); + if (user.HasPermission(PermissionKind.IsAdministrator) && string.IsNullOrWhiteSpace(newPassword)) + { + throw new ArgumentException("Admin user passwords must not be empty", nameof(newPassword)); + } await GetAuthenticationProvider(user).ChangePassword(user, newPassword).ConfigureAwait(false); await UpdateUserAsync(user).ConfigureAwait(false); From 29ef02af9a1c2f2ff51b5cd34a9074943cdc1d1f Mon Sep 17 00:00:00 2001 From: cvium <clausvium@gmail.com> Date: Fri, 26 May 2023 21:50:51 +0200 Subject: [PATCH 327/858] do not allow empty admin password during wizard --- Jellyfin.Api/Controllers/StartupController.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index aab390d1f9..1098733b2c 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -131,6 +131,10 @@ public class StartupController : BaseJellyfinApiController public async Task<ActionResult> UpdateStartupUser([FromBody] StartupUserDto startupUserDto) { var user = _userManager.Users.First(); + if (string.IsNullOrWhiteSpace(startupUserDto.Password)) + { + return BadRequest("Password must not be empty"); + } if (startupUserDto.Name is not null) { From 3ccac32f7e70cf5ee31e7ae02a471ba9b582b0c6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 26 May 2023 21:28:05 +0000 Subject: [PATCH 328/858] chore(deps): update dependency playlistsnet to v1.4.0 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index bb5b72d210..82cc8c41e9 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -52,7 +52,7 @@ <PackageVersion Include="Moq" Version="4.18.4" /> <PackageVersion Include="NEbml" Version="0.11.0" /> <PackageVersion Include="Newtonsoft.Json" Version="13.0.3" /> - <PackageVersion Include="PlaylistsNET" Version="1.3.2" /> + <PackageVersion Include="PlaylistsNET" Version="1.4.0" /> <PackageVersion Include="prometheus-net.AspNetCore" Version="8.0.0" /> <PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" /> <PackageVersion Include="prometheus-net" Version="8.0.0" /> From f7b418465fcbfd299e8ca0b61e242edcff5edc61 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 27 May 2023 08:49:15 +0000 Subject: [PATCH 329/858] chore(deps): update dependency efcoresecondlevelcacheinterceptor to v3.9.2 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index bb5b72d210..011821dd43 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -17,7 +17,7 @@ <PackageVersion Include="Diacritics" Version="3.3.18" /> <PackageVersion Include="DiscUtils.Udf" Version="0.16.13" /> <PackageVersion Include="DotNet.Glob" Version="3.1.3" /> - <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.9.1" /> + <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.9.2" /> <PackageVersion Include="FsCheck.Xunit" Version="2.16.5" /> <PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" /> <PackageVersion Include="libse" Version="3.6.13" /> From 1bf78409361996621981392e7f27dfc377ce49cc Mon Sep 17 00:00:00 2001 From: pranelio <pramanauskas@icloud.com> Date: Fri, 26 May 2023 13:36:16 +0000 Subject: [PATCH 330/858] Translated using Weblate (Lithuanian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/lt/ --- Emby.Server.Implementations/Localization/Core/lt-LT.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/lt-LT.json b/Emby.Server.Implementations/Localization/Core/lt-LT.json index e1c937b6cd..ce8d8fc322 100644 --- a/Emby.Server.Implementations/Localization/Core/lt-LT.json +++ b/Emby.Server.Implementations/Localization/Core/lt-LT.json @@ -20,9 +20,9 @@ "HeaderFavoriteAlbums": "Mėgstami Albumai", "HeaderFavoriteArtists": "Mėgstami Atlikėjai", "HeaderFavoriteEpisodes": "Mėgstamiausios serijos", - "HeaderFavoriteShows": "Mėgstamiausi serialai", - "HeaderFavoriteSongs": "Mėgstamos dainos", - "HeaderLiveTV": "TV gyvai", + "HeaderFavoriteShows": "Mėgstamiausios TV Laidos", + "HeaderFavoriteSongs": "Mėgstamos Dainos", + "HeaderLiveTV": "Tiesioginė TV", "HeaderNextUp": "Toliau eilėje", "HeaderRecordingGroups": "Įrašų grupės", "HomeVideos": "Namų vaizdo įrašai", From 58473aa343ba32a2a302789c122c291572625182 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 29 May 2023 13:42:21 +0000 Subject: [PATCH 331/858] chore(deps): update actions/setup-dotnet action to v3.2.0 --- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/openapi.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 9a929a8b04..9ee292f798 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -22,7 +22,7 @@ jobs: - name: Checkout repository uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 - name: Setup .NET - uses: actions/setup-dotnet@aa983c550dfda0d1722b6ac6aed55724ffacc6d3 # v3.1.0 + uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0 with: dotnet-version: '7.0.x' diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index c2387f2ef6..539da7aefe 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -19,7 +19,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} - name: Setup .NET - uses: actions/setup-dotnet@aa983c550dfda0d1722b6ac6aed55724ffacc6d3 # v3.1.0 + uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0 with: dotnet-version: '7.0.x' - name: Generate openapi.json @@ -51,7 +51,7 @@ jobs: ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/${{ github.head_ref }}) git checkout --progress --force $ANCESTOR_REF - name: Setup .NET - uses: actions/setup-dotnet@aa983c550dfda0d1722b6ac6aed55724ffacc6d3 # v3.1.0 + uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0 with: dotnet-version: '7.0.x' - name: Generate openapi.json From b57ada9bb06ab938902259db1cb20ff580112813 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 1 Jun 2023 19:51:48 +0000 Subject: [PATCH 332/858] chore(deps): update dependency microsoft.net.test.sdk to v17.6.1 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index cd26f74a94..6f81b5fd47 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -45,7 +45,7 @@ <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Logging" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Options" Version="7.0.1" /> - <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.6.0" /> + <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.6.1" /> <PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" /> <PackageVersion Include="MimeTypes" Version="2.4.0" /> <PackageVersion Include="Mono.Nat" Version="3.0.4" /> From e0159c5d9204db636e0f7f4b99f998b8d90e812e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 1 Jun 2023 19:51:54 +0000 Subject: [PATCH 333/858] chore(deps): update github/codeql-action action to v2.3.6 --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 9ee292f798..d03d44a1ca 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@0225834cc549ee0ca93cb085b92954821a145866 # v2.3.5 + uses: github/codeql-action/init@83f0fe6c4988d98a455712a27f0255212bba9bd4 # v2.3.6 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@0225834cc549ee0ca93cb085b92954821a145866 # v2.3.5 + uses: github/codeql-action/autobuild@83f0fe6c4988d98a455712a27f0255212bba9bd4 # v2.3.6 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@0225834cc549ee0ca93cb085b92954821a145866 # v2.3.5 + uses: github/codeql-action/analyze@83f0fe6c4988d98a455712a27f0255212bba9bd4 # v2.3.6 From 093c2e03562589174aeba10b21606d7b927a9f8b Mon Sep 17 00:00:00 2001 From: Bucky Patel <ghousshumail+weblatejellyfintranslate@gmail.com> Date: Thu, 1 Jun 2023 06:46:24 +0000 Subject: [PATCH 334/858] Translated using Weblate (Hindi) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hi/ --- .../Localization/Core/hi.json | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/hi.json b/Emby.Server.Implementations/Localization/Core/hi.json index 4bbb1dd352..47d3eeac5d 100644 --- a/Emby.Server.Implementations/Localization/Core/hi.json +++ b/Emby.Server.Implementations/Localization/Core/hi.json @@ -104,5 +104,24 @@ "TaskCleanActivityLog": "क्रियाकलाप लॉग साफ करें", "TasksChannelsCategory": "इंटरनेट प्रणाली", "TasksApplicationCategory": "अनुप्रयोग", - "TaskRefreshPeople": "लोगोकी जानकारी ताज़ी करें" + "TaskRefreshPeople": "लोगोकी जानकारी ताज़ी करें", + "TaskKeyframeExtractor": "कीफ़्रेम एक्सट्रैक्टर", + "TaskCleanActivityLogDescription": "कॉन्फ़िगर की गई आयु से पुरानी गतिविधि लॉग प्रविष्टियां हटाता है।", + "TaskRefreshChapterImagesDescription": "अध्याय वाले वीडियो के लिए थंबनेल बनाता है।", + "TaskRefreshLibraryDescription": "नई फ़ाइलों के लिए आपकी मीडिया लाइब्रेरी को स्कैन करता है और मेटाडेटा को ताज़ा करता है।", + "TaskCleanLogs": "स्वच्छ लॉग निर्देशिका", + "TaskUpdatePluginsDescription": "प्लगइन्स के लिए अपडेट डाउनलोड और इंस्टॉल करें जो स्वचालित रूप से अपडेट करने के लिए कॉन्फ़िगर किए गए हैं।", + "TaskCleanTranscode": "स्वच्छ ट्रांसकोड निर्देशिका", + "TaskCleanTranscodeDescription": "एक दिन से अधिक पुरानी ट्रांसकोड फ़ाइलें हटाता है.", + "TaskRefreshChannelsDescription": "इंटरनेट चैनल की जानकारी को ताज़ा करता है।", + "TaskOptimizeDatabaseDescription": "डेटाबेस को कॉम्पैक्ट करता है और मुक्त स्थान को छोटा करता है। लाइब्रेरी को स्कैन करने के बाद इस कार्य को चलाने या अन्य परिवर्तन करने से जो डेटाबेस संशोधनों को लागू करते हैं, प्रदर्शन में सुधार कर सकते हैं।", + "TaskRefreshChannels": "इंटरनेट चैनल की जानकारी को ताज़ा करता है", + "TaskRefreshChapterImages": "अध्याय छवियाँ निकालें", + "TaskCleanLogsDescription": "{0} दिन से अधिक पुरानी लॉग फ़ाइलें हटाता है।", + "TaskCleanCacheDescription": "उन कैश फ़ाइलों को हटाता है जिनकी अब सिस्टम को आवश्यकता नहीं है।", + "TaskUpdatePlugins": "अद्यतन प्लगइन्स", + "TaskRefreshPeopleDescription": "आपकी मीडिया लाइब्रेरी में अभिनेताओं और निर्देशकों के लिए मेटाडेटा अपडेट करता है।", + "TaskCleanCache": "स्वच्छ कैश निर्देशिका", + "TaskDownloadMissingSubtitlesDescription": "मेटाडेटा कॉन्फ़िगरेशन के आधार पर लापता उपशीर्षक के लिए इंटरनेट खोजता है।", + "TaskKeyframeExtractorDescription": "अधिक सटीक एचएलएस प्लेलिस्ट बनाने के लिए वीडियो फ़ाइलों से मुख्य-फ़्रेम निकालता है। यह कार्य लंबे समय तक चल सकता है।" } From 29368a1566afcbfa861e3cf431156ee0f73f1f2b Mon Sep 17 00:00:00 2001 From: Mark Lopez <m@silvenga.com> Date: Mon, 5 Jun 2023 11:46:13 -0500 Subject: [PATCH 335/858] Source SQLite cache_size from an Environment Variable (#9666) --- Emby.Server.Implementations/ConfigurationOptions.cs | 5 +++-- .../Data/SqliteItemRepository.cs | 10 ++++++++-- .../Extensions/ConfigurationExtensions.cs | 13 +++++++++++++ 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs index 630265dac9..f0c2676279 100644 --- a/Emby.Server.Implementations/ConfigurationOptions.cs +++ b/Emby.Server.Implementations/ConfigurationOptions.cs @@ -11,14 +11,15 @@ namespace Emby.Server.Implementations /// <summary> /// Gets a new copy of the default configuration options. /// </summary> - public static Dictionary<string, string?> DefaultConfiguration => new Dictionary<string, string?> + public static Dictionary<string, string?> DefaultConfiguration => new() { { HostWebClientKey, bool.TrueString }, { DefaultRedirectKey, "web/" }, { FfmpegProbeSizeKey, "1G" }, { FfmpegAnalyzeDurationKey, "200M" }, { PlaylistsAllowDuplicatesKey, bool.FalseString }, - { BindToUnixSocketKey, bool.FalseString } + { BindToUnixSocketKey, bool.FalseString }, + { SqliteCacheSizeKey, "20000" } }; } } diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index c32e7c75ad..ca8f605a02 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -25,6 +25,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Persistence; @@ -34,6 +35,7 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Querying; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using SQLitePCL.pretty; @@ -319,13 +321,15 @@ namespace Emby.Server.Implementations.Data /// <param name="logger">Instance of the <see cref="ILogger{SqliteItemRepository}"/> interface.</param> /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param> /// <param name="imageProcessor">Instance of the <see cref="IImageProcessor"/> interface.</param> + /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param> /// <exception cref="ArgumentNullException">config is null.</exception> public SqliteItemRepository( IServerConfigurationManager config, IServerApplicationHost appHost, ILogger<SqliteItemRepository> logger, ILocalizationManager localization, - IImageProcessor imageProcessor) + IImageProcessor imageProcessor, + IConfiguration configuration) : base(logger) { _config = config; @@ -337,11 +341,13 @@ namespace Emby.Server.Implementations.Data _jsonOptions = JsonDefaults.Options; DbFilePath = Path.Combine(_config.ApplicationPaths.DataPath, "library.db"); + + CacheSize = configuration.GetSqliteCacheSize(); ReadConnectionsCount = Environment.ProcessorCount * 2; } /// <inheritdoc /> - protected override int? CacheSize => 20000; + protected override int? CacheSize { get; } /// <inheritdoc /> protected override TempStoreMode TempStore => TempStoreMode.Memory; diff --git a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs index 3b5e8ece71..6c58064ce9 100644 --- a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs +++ b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs @@ -59,6 +59,11 @@ namespace MediaBrowser.Controller.Extensions /// </summary> public const string UnixSocketPermissionsKey = "kestrel:socketPermissions"; + /// <summary> + /// The cache size of the SQL database, see cache_size. + /// </summary> + public const string SqliteCacheSizeKey = "sqlite:cacheSize"; + /// <summary> /// Gets a value indicating whether the application should host static web content from the <see cref="IConfiguration"/>. /// </summary> @@ -115,5 +120,13 @@ namespace MediaBrowser.Controller.Extensions /// <returns>The unix socket permissions.</returns> public static string? GetUnixSocketPermissions(this IConfiguration configuration) => configuration[UnixSocketPermissionsKey]; + + /// <summary> + /// Gets the cache_size from the <see cref="IConfiguration" />. + /// </summary> + /// <param name="configuration">The configuration to read the setting from.</param> + /// <returns>The sqlite cache size.</returns> + public static int? GetSqliteCacheSize(this IConfiguration configuration) + => configuration.GetValue<int?>(SqliteCacheSizeKey); } } From cb788dbd73c8b003b22a2f376289b98cf212f0a4 Mon Sep 17 00:00:00 2001 From: Cody Robibero <cody@robibe.ro> Date: Mon, 5 Jun 2023 16:47:50 -0600 Subject: [PATCH 336/858] Mock configuration to get SqliteCacheSizeKey during test --- .../Data/SqliteItemRepositoryTests.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs index 7d92e7b261..0d2b488bc7 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs @@ -6,6 +6,7 @@ using Emby.Server.Implementations.Data; using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Entities; +using Microsoft.Extensions.Configuration; using Moq; using Xunit; @@ -27,8 +28,18 @@ namespace Jellyfin.Server.Implementations.Tests.Data appHost.Setup(x => x.ReverseVirtualPath(It.IsAny<string>())) .Returns((string x) => x.Replace(MetaDataPath, VirtualMetaDataPath, StringComparison.Ordinal)); + var configSection = new Mock<IConfigurationSection>(); + configSection + .SetupGet(x => x[It.Is<string>(s => s == MediaBrowser.Controller.Extensions.ConfigurationExtensions.SqliteCacheSizeKey)]) + .Returns("0"); + var config = new Mock<IConfiguration>(); + config + .Setup(x => x.GetSection(It.Is<string>(s => s == MediaBrowser.Controller.Extensions.ConfigurationExtensions.SqliteCacheSizeKey))) + .Returns(configSection.Object); + _fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true }); _fixture.Inject(appHost); + _fixture.Inject(config); _sqliteItemRepository = _fixture.Create<SqliteItemRepository>(); } From d23578c46f2c76e5b0cb0f2bda0b9132033f6f1b Mon Sep 17 00:00:00 2001 From: Niels van Velzen <nielsvanvelzen@users.noreply.github.com> Date: Tue, 6 Jun 2023 17:51:26 +0200 Subject: [PATCH 337/858] Make LUFS property nullable in BaseItemDto This fixes a regression from #9222 where the LUFS field in the OpenAPI spec was not nullable. This will cause issues with the Kotlin SDK. --- MediaBrowser.Model/Dto/BaseItemDto.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs index 27154c2978..8fab1ca6d6 100644 --- a/MediaBrowser.Model/Dto/BaseItemDto.cs +++ b/MediaBrowser.Model/Dto/BaseItemDto.cs @@ -783,7 +783,7 @@ namespace MediaBrowser.Model.Dto /// Gets or sets the LUFS value. /// </summary> /// <value>The LUFS Value.</value> - public float LUFS { get; set; } + public float? LUFS { get; set; } /// <summary> /// Gets or sets the current program. From 6e72ea3135717c7b74258dc1284c24db933897b5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 6 Jun 2023 18:09:02 +0000 Subject: [PATCH 338/858] chore(deps): update dependency microsoft.net.test.sdk to v17.6.2 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 6f81b5fd47..64827256ce 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -45,7 +45,7 @@ <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Logging" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Options" Version="7.0.1" /> - <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.6.1" /> + <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.6.2" /> <PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" /> <PackageVersion Include="MimeTypes" Version="2.4.0" /> <PackageVersion Include="Mono.Nat" Version="3.0.4" /> From 40ca923a71e8edb6687d69feffd24497dec27845 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 8 Jun 2023 09:11:25 +0000 Subject: [PATCH 339/858] chore(deps): update peter-evans/create-or-update-comment action to v3.0.2 --- .github/workflows/commands.yml | 10 +++++----- .github/workflows/openapi.yml | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index ae06c4141b..bad6f45edf 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Notify as seen - uses: peter-evans/create-or-update-comment@ca08ebd5dc95aa0cd97021e9708fcd6b87138c9b # v3.0.1 + uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2 with: token: ${{ secrets.JF_BOT_TOKEN }} comment-id: ${{ github.event.comment.id }} @@ -43,7 +43,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Notify as seen - uses: peter-evans/create-or-update-comment@ca08ebd5dc95aa0cd97021e9708fcd6b87138c9b # v3.0.1 + uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2 if: ${{ github.event.comment != null }} with: token: ${{ secrets.JF_BOT_TOKEN }} @@ -58,7 +58,7 @@ jobs: - name: Notify as running id: comment_running - uses: peter-evans/create-or-update-comment@ca08ebd5dc95aa0cd97021e9708fcd6b87138c9b # v3.0.1 + uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2 if: ${{ github.event.comment != null }} with: token: ${{ secrets.JF_BOT_TOKEN }} @@ -93,7 +93,7 @@ jobs: exit ${retcode} - name: Notify with result success - uses: peter-evans/create-or-update-comment@ca08ebd5dc95aa0cd97021e9708fcd6b87138c9b # v3.0.1 + uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2 if: ${{ github.event.comment != null && success() }} with: token: ${{ secrets.JF_BOT_TOKEN }} @@ -108,7 +108,7 @@ jobs: reactions: hooray - name: Notify with result failure - uses: peter-evans/create-or-update-comment@ca08ebd5dc95aa0cd97021e9708fcd6b87138c9b # v3.0.1 + uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2 if: ${{ github.event.comment != null && failure() }} with: token: ${{ secrets.JF_BOT_TOKEN }} diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index 539da7aefe..e8d080e9ad 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -110,7 +110,7 @@ jobs: direction: last body-includes: openapi-diff-workflow-comment - name: Reply or edit difference comment (changed) - uses: peter-evans/create-or-update-comment@ca08ebd5dc95aa0cd97021e9708fcd6b87138c9b # v3.0.1 + uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2 if: ${{ steps.read-diff.outputs.body != '' }} with: issue-number: ${{ github.event.pull_request.number }} @@ -125,7 +125,7 @@ jobs: </details> - name: Edit difference comment (unchanged) - uses: peter-evans/create-or-update-comment@ca08ebd5dc95aa0cd97021e9708fcd6b87138c9b # v3.0.1 + uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2 if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }} with: issue-number: ${{ github.event.pull_request.number }} From 1330bd0b52346de0b505e663b1f8cc4bc550295a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 10 Jun 2023 06:55:15 -0600 Subject: [PATCH 340/858] chore(deps): update actions/checkout action to v3.5.3 (#9876) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/commands.yml | 4 ++-- .github/workflows/openapi.yml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index d03d44a1ca..9f1be02327 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - name: Setup .NET uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0 with: diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index bad6f45edf..178959afc9 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -24,7 +24,7 @@ jobs: reactions: '+1' - name: Checkout the latest code - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 with: token: ${{ secrets.JF_BOT_TOKEN }} fetch-depth: 0 @@ -51,7 +51,7 @@ jobs: reactions: eyes - name: Checkout the latest code - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 with: token: ${{ secrets.JF_BOT_TOKEN }} fetch-depth: 0 diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index e8d080e9ad..ad1cedd52e 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -14,7 +14,7 @@ jobs: permissions: read-all steps: - name: Checkout repository - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 with: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} @@ -39,7 +39,7 @@ jobs: permissions: read-all steps: - name: Checkout repository - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 with: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} From a5005e3d01fe98364fd3449e8778bc3370ad0e65 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 10 Jun 2023 07:01:34 -0600 Subject: [PATCH 341/858] chore(deps): update dependency serilog.sinks.graylog to v3 (#9867) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 64827256ce..49081399e1 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -62,7 +62,7 @@ <PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" /> <PackageVersion Include="Serilog.Sinks.Console" Version="4.1.0" /> <PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" /> - <PackageVersion Include="Serilog.Sinks.Graylog" Version="2.3.0" /> + <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.0.0" /> <PackageVersion Include="SerilogAnalyzer" Version="0.15.0" /> <PackageVersion Include="SharpFuzz" Version="2.0.2" /> <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.3" /> From 898bb955641e9ddfe5347d4406960f363b100eac Mon Sep 17 00:00:00 2001 From: Bond-009 <bond.009@outlook.com> Date: Sat, 10 Jun 2023 15:01:47 +0200 Subject: [PATCH 342/858] Fix InvalidOpEx while trying to read HttpResponseContent 2x (#9861) --- Emby.Dlna/PlayTo/DlnaHttpClient.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Emby.Dlna/PlayTo/DlnaHttpClient.cs b/Emby.Dlna/PlayTo/DlnaHttpClient.cs index 4e9903f26a..8b983e9e3d 100644 --- a/Emby.Dlna/PlayTo/DlnaHttpClient.cs +++ b/Emby.Dlna/PlayTo/DlnaHttpClient.cs @@ -49,20 +49,24 @@ namespace Emby.Dlna.PlayTo private async Task<XDocument?> SendRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken) { - using var response = await _httpClientFactory.CreateClient(NamedClient.Dlna).SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var client = _httpClientFactory.CreateClient(NamedClient.Dlna); + using var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false); response.EnsureSuccessStatusCode(); - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + await using MemoryStream ms = new MemoryStream(); + await response.Content.CopyToAsync(ms, cancellationToken).ConfigureAwait(false); try { return await XDocument.LoadAsync( - stream, + ms, LoadOptions.None, cancellationToken).ConfigureAwait(false); } catch (XmlException) { // try correcting the Xml response with common errors - var xmlString = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + ms.Position = 0; + using StreamReader sr = new StreamReader(ms); + var xmlString = await sr.ReadToEndAsync(cancellationToken).ConfigureAwait(false); // find and replace unescaped ampersands (&) xmlString = EscapeAmpersandRegex().Replace(xmlString, "&"); @@ -70,7 +74,7 @@ namespace Emby.Dlna.PlayTo try { // retry reading Xml - var xmlReader = new StringReader(xmlString); + using var xmlReader = new StringReader(xmlString); return await XDocument.LoadAsync( xmlReader, LoadOptions.None, From 198b9aa5300236d2eb02310702b2333085d0b796 Mon Sep 17 00:00:00 2001 From: Cody Robibero <cody@robibe.ro> Date: Sat, 10 Jun 2023 07:04:23 -0600 Subject: [PATCH 343/858] Update MediaBrowser.Providers/TV/SeriesMetadataService.cs Co-authored-by: Bond-009 <bond.009@outlook.com> --- MediaBrowser.Providers/TV/SeriesMetadataService.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs index 85e2481147..9016e5de0c 100644 --- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs +++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs @@ -76,10 +76,9 @@ namespace MediaBrowser.Providers.TV } else if (targetSeasonNames.Count != sourceSeasonNames.Count || !sourceSeasonNames.Keys.All(targetSeasonNames.ContainsKey)) { - foreach (var season in sourceSeasonNames) + foreach (var (number, name) in sourceSeasonNames) { - var seasonNumber = season.Key; - targetSeasonNames.TryAdd(seasonNumber, season.Value); + targetSeasonNames.TryAdd(number, name); } } From 9a0dfc00f1e36092476f62984ba66886032f4f89 Mon Sep 17 00:00:00 2001 From: Cody Robibero <cody@robibe.ro> Date: Sat, 10 Jun 2023 07:28:21 -0600 Subject: [PATCH 344/858] Add all websocket messages to generated openapi spec (#9682) * Add all websocket messages to generated openapi spec * Use oneOf * JsonIgnore ServerId * Oops * Add discriminators * Add WebSocketMessage container for Inbound and Outbound messages --- .../Filters/AdditionalModelFilter.cs | 148 ++++++++++++++++-- .../Net/WebSocketMessage.cs | 28 ++++ .../Net/WebSocketMessageOfT.cs | 33 ++++ .../IInboundWebSocketMessage.cs | 10 ++ .../IOutboundWebSocketMessage.cs | 10 ++ .../Inbound/ActivityLogEntryStartMessage.cs | 25 +++ .../Inbound/ActivityLogEntryStopMessage.cs | 25 +++ .../Inbound/ScheduledTasksInfoStartMessage.cs | 25 +++ .../Inbound/ScheduledTasksInfoStopMessage.cs | 25 +++ .../Inbound/SessionsStartMessage.cs | 24 +++ .../Inbound/SessionsStopMessage.cs | 24 +++ .../InboundWebSocketMessage.cs | 9 ++ .../Outbound/ActivityLogEntryMessage.cs | 25 +++ .../Outbound/ForceKeepAliveMessage.cs | 23 +++ .../Outbound/GeneralCommandMessage.cs | 23 +++ .../Outbound/LibraryChangedMessage.cs | 24 +++ .../WebSocketMessages/Outbound/PlayMessage.cs | 23 +++ .../Outbound/PlaystateMessage.cs | 23 +++ .../PluginInstallationCancelledMessage.cs | 24 +++ .../PluginInstallationCompletedMessage.cs | 24 +++ .../PluginInstallationFailedMessage.cs | 24 +++ .../Outbound/PluginInstallingMessage.cs | 24 +++ .../Outbound/PluginUninstalledMessage.cs | 24 +++ .../Outbound/RefreshProgressMessage.cs | 24 +++ .../Outbound/RestartRequiredMessage.cs | 14 ++ .../Outbound/ScheduledTaskEndedMessage.cs | 24 +++ .../Outbound/ScheduledTasksInfoMessage.cs | 25 +++ .../Outbound/SeriesTimerCancelledMessage.cs | 24 +++ .../Outbound/SeriesTimerCreatedMessage.cs | 24 +++ .../Outbound/ServerRestartingMessage.cs | 14 ++ .../Outbound/ServerShuttingDownMessage.cs | 14 ++ .../Outbound/SessionsMessage.cs | 24 +++ .../Outbound/SyncPlayCommandMessage.cs | 24 +++ .../SyncPlayGroupUpdateCommandMessage.cs | 24 +++ ...layGroupUpdateCommandOfGroupInfoMessage.cs | 25 +++ ...pUpdateCommandOfGroupStateUpdateMessage.cs | 25 +++ ...upUpdateCommandOfPlayQueueUpdateMessage.cs | 25 +++ ...ncPlayGroupUpdateCommandOfStringMessage.cs | 25 +++ .../Outbound/TimerCancelledMessage.cs | 24 +++ .../Outbound/TimerCreatedMessage.cs | 24 +++ .../Outbound/UserDataChangedMessage.cs | 23 +++ .../Outbound/UserDeletedMessage.cs | 24 +++ .../Outbound/UserUpdatedMessage.cs | 24 +++ .../OutboundWebSocketMessage.cs | 9 ++ .../Shared/KeepAliveMessage.cs | 23 +++ .../SyncPlay/Queue/PlayQueueManager.cs | 24 +-- MediaBrowser.Model/Net/WebSocketMessage.cs | 31 ---- MediaBrowser.Model/SyncPlay/GroupUpdate.cs | 56 +++---- MediaBrowser.Model/SyncPlay/GroupUpdateOfT.cs | 31 ++++ .../SyncPlay/PlayQueueUpdate.cs | 4 +- .../{QueueItem.cs => SyncPlayQueueItem.cs} | 6 +- 51 files changed, 1193 insertions(+), 92 deletions(-) create mode 100644 MediaBrowser.Controller/Net/WebSocketMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessageOfT.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/IInboundWebSocketMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/IOutboundWebSocketMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ActivityLogEntryStartMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ActivityLogEntryStopMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ScheduledTasksInfoStartMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ScheduledTasksInfoStopMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Inbound/SessionsStartMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Inbound/SessionsStopMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/InboundWebSocketMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ActivityLogEntryMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ForceKeepAliveMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Outbound/GeneralCommandMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Outbound/LibraryChangedMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PlayMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PlaystateMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationCancelledMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationCompletedMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationFailedMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallingMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginUninstalledMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Outbound/RefreshProgressMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Outbound/RestartRequiredMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ScheduledTaskEndedMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ScheduledTasksInfoMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SeriesTimerCancelledMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SeriesTimerCreatedMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ServerRestartingMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ServerShuttingDownMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayCommandMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupInfoMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfStringMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Outbound/TimerCancelledMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Outbound/TimerCreatedMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserDataChangedMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserDeletedMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserUpdatedMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/OutboundWebSocketMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Shared/KeepAliveMessage.cs delete mode 100644 MediaBrowser.Model/Net/WebSocketMessage.cs create mode 100644 MediaBrowser.Model/SyncPlay/GroupUpdateOfT.cs rename MediaBrowser.Model/SyncPlay/{QueueItem.cs => SyncPlayQueueItem.cs} (80%) diff --git a/Jellyfin.Server/Filters/AdditionalModelFilter.cs b/Jellyfin.Server/Filters/AdditionalModelFilter.cs index 645696e319..bf38f741cd 100644 --- a/Jellyfin.Server/Filters/AdditionalModelFilter.cs +++ b/Jellyfin.Server/Filters/AdditionalModelFilter.cs @@ -1,12 +1,16 @@ using System; +using System.Collections.Generic; +using System.ComponentModel; using System.Linq; +using System.Reflection; using Jellyfin.Extensions; using Jellyfin.Server.Migrations; using MediaBrowser.Common.Plugins; using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Net.WebSocketMessages; +using MediaBrowser.Controller.Net.WebSocketMessages.Outbound; using MediaBrowser.Model.ApiClient; -using MediaBrowser.Model.Entities; using MediaBrowser.Model.Session; using MediaBrowser.Model.SyncPlay; using Microsoft.OpenApi.Any; @@ -36,17 +40,141 @@ namespace Jellyfin.Server.Filters /// <inheritdoc /> public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) { - context.SchemaGenerator.GenerateSchema(typeof(LibraryUpdateInfo), context.SchemaRepository); context.SchemaGenerator.GenerateSchema(typeof(IPlugin), context.SchemaRepository); - context.SchemaGenerator.GenerateSchema(typeof(PlayRequest), context.SchemaRepository); - context.SchemaGenerator.GenerateSchema(typeof(PlaystateRequest), context.SchemaRepository); - context.SchemaGenerator.GenerateSchema(typeof(TimerEventInfo), context.SchemaRepository); - context.SchemaGenerator.GenerateSchema(typeof(SendCommand), context.SchemaRepository); - context.SchemaGenerator.GenerateSchema(typeof(GeneralCommandType), context.SchemaRepository); - context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate<object>), context.SchemaRepository); + var webSocketTypes = typeof(WebSocketMessage).Assembly.GetTypes() + .Where(t => t.IsSubclassOf(typeof(WebSocketMessage)) + && !t.IsGenericType + && t != typeof(WebSocketMessageInfo)) + .ToList(); + + var inboundWebSocketSchemas = new List<OpenApiSchema>(); + var inboundWebSocketDiscriminators = new Dictionary<string, string>(); + foreach (var type in webSocketTypes.Where(t => typeof(IInboundWebSocketMessage).IsAssignableFrom(t))) + { + var messageType = (SessionMessageType?)type.GetProperty(nameof(WebSocketMessage.MessageType))?.GetCustomAttribute<DefaultValueAttribute>()?.Value; + if (messageType is null) + { + continue; + } + + var schema = context.SchemaGenerator.GenerateSchema(type, context.SchemaRepository); + inboundWebSocketSchemas.Add(schema); + inboundWebSocketDiscriminators[messageType.ToString()!] = schema.Reference.ReferenceV3; + } + + var inboundWebSocketMessageSchema = new OpenApiSchema + { + Type = "object", + Description = "Represents the list of possible inbound websocket types", + Reference = new OpenApiReference + { + Id = nameof(InboundWebSocketMessage), + Type = ReferenceType.Schema + }, + OneOf = inboundWebSocketSchemas, + Discriminator = new OpenApiDiscriminator + { + PropertyName = nameof(WebSocketMessage.MessageType), + Mapping = inboundWebSocketDiscriminators + } + }; + + context.SchemaRepository.AddDefinition(nameof(InboundWebSocketMessage), inboundWebSocketMessageSchema); + + var outboundWebSocketSchemas = new List<OpenApiSchema>(); + var outboundWebSocketDiscriminators = new Dictionary<string, string>(); + foreach (var type in webSocketTypes.Where(t => typeof(IOutboundWebSocketMessage).IsAssignableFrom(t))) + { + var messageType = (SessionMessageType?)type.GetProperty(nameof(WebSocketMessage.MessageType))?.GetCustomAttribute<DefaultValueAttribute>()?.Value; + if (messageType is null) + { + continue; + } + + // Additional discriminator needed for GroupUpdate models... + if (messageType == SessionMessageType.SyncPlayGroupUpdate && type != typeof(SyncPlayGroupUpdateCommandMessage)) + { + continue; + } + + var schema = context.SchemaGenerator.GenerateSchema(type, context.SchemaRepository); + outboundWebSocketSchemas.Add(schema); + outboundWebSocketDiscriminators.Add(messageType.ToString()!, schema.Reference.ReferenceV3); + } + + var outboundWebSocketMessageSchema = new OpenApiSchema + { + Type = "object", + Description = "Represents the list of possible outbound websocket types", + Reference = new OpenApiReference + { + Id = nameof(OutboundWebSocketMessage), + Type = ReferenceType.Schema + }, + OneOf = outboundWebSocketSchemas, + Discriminator = new OpenApiDiscriminator + { + PropertyName = nameof(WebSocketMessage.MessageType), + Mapping = outboundWebSocketDiscriminators + } + }; + + context.SchemaRepository.AddDefinition(nameof(OutboundWebSocketMessage), outboundWebSocketMessageSchema); + context.SchemaRepository.AddDefinition( + nameof(WebSocketMessage), + new OpenApiSchema + { + Type = "object", + Description = "Represents the possible websocket types", + Reference = new OpenApiReference + { + Id = nameof(WebSocketMessage), + Type = ReferenceType.Schema + }, + OneOf = new[] + { + inboundWebSocketMessageSchema, + outboundWebSocketMessageSchema + } + }); + + // Manually generate sync play GroupUpdate messages. + if (!context.SchemaRepository.Schemas.TryGetValue(nameof(GroupUpdate), out var groupUpdateSchema)) + { + groupUpdateSchema = context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate), context.SchemaRepository); + } + + var groupUpdateOfGroupInfoSchema = context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate<GroupInfoDto>), context.SchemaRepository); + var groupUpdateOfGroupStateSchema = context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate<GroupStateUpdate>), context.SchemaRepository); + var groupUpdateOfStringSchema = context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate<string>), context.SchemaRepository); + var groupUpdateOfPlayQueueSchema = context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate<PlayQueueUpdate>), context.SchemaRepository); + + groupUpdateSchema.OneOf = new List<OpenApiSchema> + { + groupUpdateOfGroupInfoSchema, + groupUpdateOfGroupStateSchema, + groupUpdateOfStringSchema, + groupUpdateOfPlayQueueSchema + }; + + groupUpdateSchema.Discriminator = new OpenApiDiscriminator + { + PropertyName = nameof(GroupUpdate.Type), + Mapping = new Dictionary<string, string> + { + { GroupUpdateType.UserJoined.ToString(), groupUpdateOfStringSchema.Reference.ReferenceV3 }, + { GroupUpdateType.UserLeft.ToString(), groupUpdateOfStringSchema.Reference.ReferenceV3 }, + { GroupUpdateType.GroupJoined.ToString(), groupUpdateOfGroupInfoSchema.Reference.ReferenceV3 }, + { GroupUpdateType.GroupLeft.ToString(), groupUpdateOfStringSchema.Reference.ReferenceV3 }, + { GroupUpdateType.StateUpdate.ToString(), groupUpdateOfGroupStateSchema.Reference.ReferenceV3 }, + { GroupUpdateType.PlayQueue.ToString(), groupUpdateOfPlayQueueSchema.Reference.ReferenceV3 }, + { GroupUpdateType.NotInGroup.ToString(), groupUpdateOfStringSchema.Reference.ReferenceV3 }, + { GroupUpdateType.GroupDoesNotExist.ToString(), groupUpdateOfStringSchema.Reference.ReferenceV3 }, + { GroupUpdateType.LibraryAccessDenied.ToString(), groupUpdateOfStringSchema.Reference.ReferenceV3 } + } + }; - context.SchemaGenerator.GenerateSchema(typeof(SessionMessageType), context.SchemaRepository); context.SchemaGenerator.GenerateSchema(typeof(ServerDiscoveryInfo), context.SchemaRepository); foreach (var configuration in _serverConfigurationManager.GetConfigurationStores()) diff --git a/MediaBrowser.Controller/Net/WebSocketMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessage.cs new file mode 100644 index 0000000000..c02bcd70b6 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessage.cs @@ -0,0 +1,28 @@ +using System; +using System.Text.Json.Serialization; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net; + +/// <summary> +/// Websocket message without data. +/// </summary> +public abstract class WebSocketMessage +{ + /// <summary> + /// Gets or sets the type of the message. + /// TODO make this abstract and get only. + /// </summary> + public virtual SessionMessageType MessageType { get; set; } + + /// <summary> + /// Gets or sets the message id. + /// </summary> + public Guid MessageId { get; set; } + + /// <summary> + /// Gets or sets the server id. + /// </summary> + [JsonIgnore] + public string? ServerId { get; set; } +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessageOfT.cs b/MediaBrowser.Controller/Net/WebSocketMessageOfT.cs new file mode 100644 index 0000000000..7c35c8010d --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessageOfT.cs @@ -0,0 +1,33 @@ +#pragma warning disable SA1649 // File name must equal class name. + +namespace MediaBrowser.Controller.Net; + +/// <summary> +/// Class WebSocketMessage. +/// </summary> +/// <typeparam name="T">The type of the data.</typeparam> +// TODO make this abstract, remove empty ctor. +public class WebSocketMessage<T> : WebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="WebSocketMessage{T}"/> class. + /// </summary> + public WebSocketMessage() + { + } + + /// <summary> + /// Initializes a new instance of the <see cref="WebSocketMessage{T}"/> class. + /// </summary> + /// <param name="data">The data to send.</param> + protected WebSocketMessage(T data) + { + Data = data; + } + + /// <summary> + /// Gets or sets the data. + /// </summary> + // TODO make this set only. + public T? Data { get; set; } +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/IInboundWebSocketMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/IInboundWebSocketMessage.cs new file mode 100644 index 0000000000..c3cf9955ad --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/IInboundWebSocketMessage.cs @@ -0,0 +1,10 @@ +#pragma warning disable CA1040 + +namespace MediaBrowser.Controller.Net.WebSocketMessages; + +/// <summary> +/// Interface representing that the websocket message is inbound. +/// </summary> +public interface IInboundWebSocketMessage +{ +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/IOutboundWebSocketMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/IOutboundWebSocketMessage.cs new file mode 100644 index 0000000000..c74a254a68 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/IOutboundWebSocketMessage.cs @@ -0,0 +1,10 @@ +#pragma warning disable CA1040 + +namespace MediaBrowser.Controller.Net.WebSocketMessages; + +/// <summary> +/// Interface representing that the websocket message is outbound. +/// </summary> +public interface IOutboundWebSocketMessage +{ +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ActivityLogEntryStartMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ActivityLogEntryStartMessage.cs new file mode 100644 index 0000000000..b9f71b9225 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ActivityLogEntryStartMessage.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.ComponentModel; +using MediaBrowser.Model.Activity; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound; + +/// <summary> +/// Activity log entry start message. +/// </summary> +public class ActivityLogEntryStartMessage : WebSocketMessage<IReadOnlyCollection<ActivityLogEntry>>, IInboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="ActivityLogEntryStartMessage"/> class. + /// </summary> + /// <param name="data">Collection of activity log entries.</param> + public ActivityLogEntryStartMessage(IReadOnlyCollection<ActivityLogEntry> data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.ActivityLogEntryStart)] + public override SessionMessageType MessageType => SessionMessageType.ActivityLogEntryStart; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ActivityLogEntryStopMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ActivityLogEntryStopMessage.cs new file mode 100644 index 0000000000..eac129b20a --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ActivityLogEntryStopMessage.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.ComponentModel; +using MediaBrowser.Model.Activity; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound; + +/// <summary> +/// Activity log entry stop message. +/// </summary> +public class ActivityLogEntryStopMessage : WebSocketMessage<IReadOnlyCollection<ActivityLogEntry>>, IInboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="ActivityLogEntryStopMessage"/> class. + /// </summary> + /// <param name="data">Collection of activity log entries.</param> + public ActivityLogEntryStopMessage(IReadOnlyCollection<ActivityLogEntry> data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.ActivityLogEntryStop)] + public override SessionMessageType MessageType => SessionMessageType.ActivityLogEntryStop; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ScheduledTasksInfoStartMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ScheduledTasksInfoStartMessage.cs new file mode 100644 index 0000000000..dd2a7145e3 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ScheduledTasksInfoStartMessage.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.ComponentModel; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.Tasks; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound; + +/// <summary> +/// Scheduled tasks info start message. +/// </summary> +public class ScheduledTasksInfoStartMessage : WebSocketMessage<IReadOnlyCollection<TaskInfo>>, IInboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="ScheduledTasksInfoStartMessage"/> class. + /// </summary> + /// <param name="data">Collection of task info.</param> + public ScheduledTasksInfoStartMessage(IReadOnlyCollection<TaskInfo> data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.ScheduledTasksInfoStart)] + public override SessionMessageType MessageType => SessionMessageType.ScheduledTasksInfoStart; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ScheduledTasksInfoStopMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ScheduledTasksInfoStopMessage.cs new file mode 100644 index 0000000000..84e1f01667 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ScheduledTasksInfoStopMessage.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.ComponentModel; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.Tasks; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound; + +/// <summary> +/// Scheduled tasks info stop message. +/// </summary> +public class ScheduledTasksInfoStopMessage : WebSocketMessage<IReadOnlyCollection<TaskInfo>>, IInboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="ScheduledTasksInfoStopMessage"/> class. + /// </summary> + /// <param name="data">Collection of task info.</param> + public ScheduledTasksInfoStopMessage(IReadOnlyCollection<TaskInfo> data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.ScheduledTasksInfoStop)] + public override SessionMessageType MessageType => SessionMessageType.ScheduledTasksInfoStop; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/SessionsStartMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/SessionsStartMessage.cs new file mode 100644 index 0000000000..e35a5dc3ad --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/SessionsStartMessage.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound; + +/// <summary> +/// Sessions start message. +/// </summary> +public class SessionsStartMessage : WebSocketMessage<SessionInfo>, IInboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="SessionsStartMessage"/> class. + /// </summary> + /// <param name="data">Session info.</param> + public SessionsStartMessage(SessionInfo data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.SessionsStart)] + public override SessionMessageType MessageType => SessionMessageType.SessionsStart; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/SessionsStopMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/SessionsStopMessage.cs new file mode 100644 index 0000000000..7e3582d640 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/SessionsStopMessage.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound; + +/// <summary> +/// Sessions stop message. +/// </summary> +public class SessionsStopMessage : WebSocketMessage<SessionInfo>, IInboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="SessionsStopMessage"/> class. + /// </summary> + /// <param name="data">Session info.</param> + public SessionsStopMessage(SessionInfo data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.SessionsStop)] + public override SessionMessageType MessageType => SessionMessageType.SessionsStop; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/InboundWebSocketMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/InboundWebSocketMessage.cs new file mode 100644 index 0000000000..20ca888e11 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/InboundWebSocketMessage.cs @@ -0,0 +1,9 @@ +namespace MediaBrowser.Controller.Net.WebSocketMessages; + +/// <summary> +/// Class representing the list of outbound websocket message types. +/// Only used in openapi generation. +/// </summary> +public class InboundWebSocketMessage : WebSocketMessage +{ +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ActivityLogEntryMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ActivityLogEntryMessage.cs new file mode 100644 index 0000000000..5650ee4bbe --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ActivityLogEntryMessage.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.ComponentModel; +using MediaBrowser.Model.Activity; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Activity log created message. +/// </summary> +public class ActivityLogEntryMessage : WebSocketMessage<IReadOnlyList<ActivityLogEntry>>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="ActivityLogEntryMessage"/> class. + /// </summary> + /// <param name="data">List of activity log entries.</param> + public ActivityLogEntryMessage(IReadOnlyList<ActivityLogEntry> data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.ActivityLogEntry)] + public override SessionMessageType MessageType => SessionMessageType.ActivityLogEntry; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ForceKeepAliveMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ForceKeepAliveMessage.cs new file mode 100644 index 0000000000..94ade5e817 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ForceKeepAliveMessage.cs @@ -0,0 +1,23 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Force keep alive websocket messages. +/// </summary> +public class ForceKeepAliveMessage : WebSocketMessage<int>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="ForceKeepAliveMessage"/> class. + /// </summary> + /// <param name="data">The timeout in seconds.</param> + public ForceKeepAliveMessage(int data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.ForceKeepAlive)] + public override SessionMessageType MessageType => SessionMessageType.ForceKeepAlive; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/GeneralCommandMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/GeneralCommandMessage.cs new file mode 100644 index 0000000000..6c71e73f9d --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/GeneralCommandMessage.cs @@ -0,0 +1,23 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// General command websocket message. +/// </summary> +public class GeneralCommandMessage : WebSocketMessage<GeneralCommand>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="GeneralCommandMessage"/> class. + /// </summary> + /// <param name="data">The general command.</param> + public GeneralCommandMessage(GeneralCommand data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.GeneralCommand)] + public override SessionMessageType MessageType => SessionMessageType.GeneralCommand; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/LibraryChangedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/LibraryChangedMessage.cs new file mode 100644 index 0000000000..6432ae8efb --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/LibraryChangedMessage.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Library changed message. +/// </summary> +public class LibraryChangedMessage : WebSocketMessage<LibraryUpdateInfo>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="LibraryChangedMessage"/> class. + /// </summary> + /// <param name="data">The library update info.</param> + public LibraryChangedMessage(LibraryUpdateInfo data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.LibraryChanged)] + public override SessionMessageType MessageType => SessionMessageType.LibraryChanged; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PlayMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PlayMessage.cs new file mode 100644 index 0000000000..7f943bda10 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PlayMessage.cs @@ -0,0 +1,23 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Play command websocket message. +/// </summary> +public class PlayMessage : WebSocketMessage<PlayRequest>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="PlayMessage"/> class. + /// </summary> + /// <param name="data">The play request.</param> + public PlayMessage(PlayRequest data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.Play)] + public override SessionMessageType MessageType => SessionMessageType.Play; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PlaystateMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PlaystateMessage.cs new file mode 100644 index 0000000000..804ccb37d6 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PlaystateMessage.cs @@ -0,0 +1,23 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Playstate message. +/// </summary> +public class PlaystateMessage : WebSocketMessage<PlaystateRequest>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="PlaystateMessage"/> class. + /// </summary> + /// <param name="data">Playstate request data.</param> + public PlaystateMessage(PlaystateRequest data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.Playstate)] + public override SessionMessageType MessageType => SessionMessageType.Playstate; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationCancelledMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationCancelledMessage.cs new file mode 100644 index 0000000000..3d7dc5c937 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationCancelledMessage.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.Updates; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Plugin installation cancelled message. +/// </summary> +public class PluginInstallationCancelledMessage : WebSocketMessage<InstallationInfo>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="PluginInstallationCancelledMessage"/> class. + /// </summary> + /// <param name="data">Installation info.</param> + public PluginInstallationCancelledMessage(InstallationInfo data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.PackageInstallationCancelled)] + public override SessionMessageType MessageType => SessionMessageType.PackageInstallationCancelled; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationCompletedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationCompletedMessage.cs new file mode 100644 index 0000000000..81268007fd --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationCompletedMessage.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.Updates; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Plugin installation completed message. +/// </summary> +public class PluginInstallationCompletedMessage : WebSocketMessage<InstallationInfo>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="PluginInstallationCompletedMessage"/> class. + /// </summary> + /// <param name="data">Installation info.</param> + public PluginInstallationCompletedMessage(InstallationInfo data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.PackageInstallationCompleted)] + public override SessionMessageType MessageType => SessionMessageType.PackageInstallationCompleted; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationFailedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationFailedMessage.cs new file mode 100644 index 0000000000..9177f12938 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationFailedMessage.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.Updates; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Plugin installation failed message. +/// </summary> +public class PluginInstallationFailedMessage : WebSocketMessage<InstallationInfo>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="PluginInstallationFailedMessage"/> class. + /// </summary> + /// <param name="data">Installation info.</param> + public PluginInstallationFailedMessage(InstallationInfo data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.PackageInstallationFailed)] + public override SessionMessageType MessageType => SessionMessageType.PackageInstallationFailed; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallingMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallingMessage.cs new file mode 100644 index 0000000000..e371440a0f --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallingMessage.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.Updates; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Package installing message. +/// </summary> +public class PluginInstallingMessage : WebSocketMessage<InstallationInfo>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="PluginInstallingMessage"/> class. + /// </summary> + /// <param name="data">Installation info.</param> + public PluginInstallingMessage(InstallationInfo data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.PackageInstalling)] + public override SessionMessageType MessageType => SessionMessageType.PackageInstalling; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginUninstalledMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginUninstalledMessage.cs new file mode 100644 index 0000000000..b2994fc956 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginUninstalledMessage.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; +using MediaBrowser.Model.Plugins; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Plugin uninstalled message. +/// </summary> +public class PluginUninstalledMessage : WebSocketMessage<PluginInfo>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="PluginUninstalledMessage"/> class. + /// </summary> + /// <param name="data">Plugin info.</param> + public PluginUninstalledMessage(PluginInfo data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.PackageUninstalled)] + public override SessionMessageType MessageType => SessionMessageType.PackageUninstalled; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/RefreshProgressMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/RefreshProgressMessage.cs new file mode 100644 index 0000000000..42dbc30295 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/RefreshProgressMessage.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.ComponentModel; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Refresh progress message. +/// </summary> +public class RefreshProgressMessage : WebSocketMessage<Dictionary<string, string>>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="RefreshProgressMessage"/> class. + /// </summary> + /// <param name="data">Refresh progress data.</param> + public RefreshProgressMessage(Dictionary<string, string> data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.RefreshProgress)] + public override SessionMessageType MessageType => SessionMessageType.RefreshProgress; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/RestartRequiredMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/RestartRequiredMessage.cs new file mode 100644 index 0000000000..3f3d9e4c84 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/RestartRequiredMessage.cs @@ -0,0 +1,14 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Restart required. +/// </summary> +public class RestartRequiredMessage : WebSocketMessage, IOutboundWebSocketMessage +{ + /// <inheritdoc /> + [DefaultValue(SessionMessageType.RestartRequired)] + public override SessionMessageType MessageType => SessionMessageType.RestartRequired; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ScheduledTaskEndedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ScheduledTaskEndedMessage.cs new file mode 100644 index 0000000000..d69662b004 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ScheduledTaskEndedMessage.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.Tasks; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Scheduled task ended message. +/// </summary> +public class ScheduledTaskEndedMessage : WebSocketMessage<TaskResult>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="ScheduledTaskEndedMessage"/> class. + /// </summary> + /// <param name="data">Task result.</param> + public ScheduledTaskEndedMessage(TaskResult data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.ScheduledTaskEnded)] + public override SessionMessageType MessageType => SessionMessageType.ScheduledTaskEnded; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ScheduledTasksInfoMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ScheduledTasksInfoMessage.cs new file mode 100644 index 0000000000..41a05b0de2 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ScheduledTasksInfoMessage.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.ComponentModel; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.Tasks; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Scheduled tasks info message. +/// </summary> +public class ScheduledTasksInfoMessage : WebSocketMessage<IReadOnlyList<TaskInfo>>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="ScheduledTasksInfoMessage"/> class. + /// </summary> + /// <param name="data">List of task infos.</param> + public ScheduledTasksInfoMessage(IReadOnlyList<TaskInfo> data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.ScheduledTasksInfo)] + public override SessionMessageType MessageType => SessionMessageType.ScheduledTasksInfo; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SeriesTimerCancelledMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SeriesTimerCancelledMessage.cs new file mode 100644 index 0000000000..d4950b8b67 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SeriesTimerCancelledMessage.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Series timer cancelled message. +/// </summary> +public class SeriesTimerCancelledMessage : WebSocketMessage<TimerEventInfo>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="SeriesTimerCancelledMessage"/> class. + /// </summary> + /// <param name="data">The timer event info.</param> + public SeriesTimerCancelledMessage(TimerEventInfo data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.SeriesTimerCancelled)] + public override SessionMessageType MessageType => SessionMessageType.SeriesTimerCancelled; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SeriesTimerCreatedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SeriesTimerCreatedMessage.cs new file mode 100644 index 0000000000..091c10be6d --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SeriesTimerCreatedMessage.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Series timer created message. +/// </summary> +public class SeriesTimerCreatedMessage : WebSocketMessage<TimerEventInfo>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="SeriesTimerCreatedMessage"/> class. + /// </summary> + /// <param name="data">timer event info.</param> + public SeriesTimerCreatedMessage(TimerEventInfo data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.SeriesTimerCreated)] + public override SessionMessageType MessageType => SessionMessageType.SeriesTimerCreated; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ServerRestartingMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ServerRestartingMessage.cs new file mode 100644 index 0000000000..a465d8b008 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ServerRestartingMessage.cs @@ -0,0 +1,14 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Server restarting down message. +/// </summary> +public class ServerRestartingMessage : WebSocketMessage, IOutboundWebSocketMessage +{ + /// <inheritdoc /> + [DefaultValue(SessionMessageType.ServerRestarting)] + public override SessionMessageType MessageType => SessionMessageType.ServerRestarting; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ServerShuttingDownMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ServerShuttingDownMessage.cs new file mode 100644 index 0000000000..0b998a5239 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ServerShuttingDownMessage.cs @@ -0,0 +1,14 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Server shutting down message. +/// </summary> +public class ServerShuttingDownMessage : WebSocketMessage, IOutboundWebSocketMessage +{ + /// <inheritdoc /> + [DefaultValue(SessionMessageType.ServerShuttingDown)] + public override SessionMessageType MessageType => SessionMessageType.ServerShuttingDown; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs new file mode 100644 index 0000000000..4c91e0bca2 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Sessions message. +/// </summary> +public class SessionsMessage : WebSocketMessage<SessionInfo>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="SessionsMessage"/> class. + /// </summary> + /// <param name="data">Session info.</param> + public SessionsMessage(SessionInfo data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.Sessions)] + public override SessionMessageType MessageType => SessionMessageType.Sessions; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayCommandMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayCommandMessage.cs new file mode 100644 index 0000000000..17a0fc66e5 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayCommandMessage.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Sync play command. +/// </summary> +public class SyncPlayCommandMessage : WebSocketMessage<SendCommand>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="SyncPlayCommandMessage"/> class. + /// </summary> + /// <param name="data">The send command.</param> + public SyncPlayCommandMessage(SendCommand data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.SyncPlayCommand)] + public override SessionMessageType MessageType => SessionMessageType.SyncPlayCommand; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandMessage.cs new file mode 100644 index 0000000000..d145d0e01a --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandMessage.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Untyped sync play command. +/// </summary> +public class SyncPlayGroupUpdateCommandMessage : WebSocketMessage<GroupUpdate>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandMessage"/> class. + /// </summary> + /// <param name="data">The send command.</param> + public SyncPlayGroupUpdateCommandMessage(GroupUpdate data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.SyncPlayGroupUpdate)] + public override SessionMessageType MessageType => SessionMessageType.SyncPlayGroupUpdate; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupInfoMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupInfoMessage.cs new file mode 100644 index 0000000000..668392c66b --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupInfoMessage.cs @@ -0,0 +1,25 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Sync play group update command with group info. +/// GroupUpdateTypes: GroupJoined. +/// </summary> +public class SyncPlayGroupUpdateCommandOfGroupInfoMessage : WebSocketMessage<GroupUpdate<GroupInfoDto>>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandOfGroupInfoMessage"/> class. + /// </summary> + /// <param name="data">The group info.</param> + public SyncPlayGroupUpdateCommandOfGroupInfoMessage(GroupUpdate<GroupInfoDto> data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.SyncPlayGroupUpdate)] + public override SessionMessageType MessageType => SessionMessageType.SyncPlayGroupUpdate; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage.cs new file mode 100644 index 0000000000..ec8c3344f4 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage.cs @@ -0,0 +1,25 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Sync play group update command with group state update. +/// GroupUpdateTypes: StateUpdate. +/// </summary> +public class SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage : WebSocketMessage<GroupUpdate<GroupStateUpdate>>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage"/> class. + /// </summary> + /// <param name="data">The group info.</param> + public SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage(GroupUpdate<GroupStateUpdate> data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.SyncPlayGroupUpdate)] + public override SessionMessageType MessageType => SessionMessageType.SyncPlayGroupUpdate; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage.cs new file mode 100644 index 0000000000..465363f143 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage.cs @@ -0,0 +1,25 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Sync play group update command with play queue update. +/// GroupUpdateTypes: PlayQueue. +/// </summary> +public class SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage : WebSocketMessage<GroupUpdate<PlayQueueUpdate>>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage"/> class. + /// </summary> + /// <param name="data">The play queue update.</param> + public SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage(GroupUpdate<PlayQueueUpdate> data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.SyncPlayGroupUpdate)] + public override SessionMessageType MessageType => SessionMessageType.SyncPlayGroupUpdate; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfStringMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfStringMessage.cs new file mode 100644 index 0000000000..b87e9bf715 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfStringMessage.cs @@ -0,0 +1,25 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Sync play group update command with string. +/// GroupUpdateTypes: GroupDoesNotExist (error), LibraryAccessDenied (error), NotInGroup (error), GroupLeft (groupId), UserJoined (username), UserLeft (username). +/// </summary> +public class SyncPlayGroupUpdateCommandOfStringMessage : WebSocketMessage<GroupUpdate<string>>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandOfStringMessage"/> class. + /// </summary> + /// <param name="data">The send command.</param> + public SyncPlayGroupUpdateCommandOfStringMessage(GroupUpdate<string> data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.SyncPlayGroupUpdate)] + public override SessionMessageType MessageType => SessionMessageType.SyncPlayGroupUpdate; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/TimerCancelledMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/TimerCancelledMessage.cs new file mode 100644 index 0000000000..0e70549ef8 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/TimerCancelledMessage.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Timer cancelled message. +/// </summary> +public class TimerCancelledMessage : WebSocketMessage<TimerEventInfo>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="TimerCancelledMessage"/> class. + /// </summary> + /// <param name="data">Timer event info.</param> + public TimerCancelledMessage(TimerEventInfo data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.TimerCancelled)] + public override SessionMessageType MessageType => SessionMessageType.TimerCancelled; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/TimerCreatedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/TimerCreatedMessage.cs new file mode 100644 index 0000000000..295b3081ce --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/TimerCreatedMessage.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Timer created message. +/// </summary> +public class TimerCreatedMessage : WebSocketMessage<TimerEventInfo>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="TimerCreatedMessage"/> class. + /// </summary> + /// <param name="data">Timer event info.</param> + public TimerCreatedMessage(TimerEventInfo data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.TimerCreated)] + public override SessionMessageType MessageType => SessionMessageType.TimerCreated; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserDataChangedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserDataChangedMessage.cs new file mode 100644 index 0000000000..b60769540d --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserDataChangedMessage.cs @@ -0,0 +1,23 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// User data changed message. +/// </summary> +public class UserDataChangedMessage : WebSocketMessage<UserDataChangeInfo>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="UserDataChangedMessage"/> class. + /// </summary> + /// <param name="data">The data change info.</param> + public UserDataChangedMessage(UserDataChangeInfo data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.UserDataChanged)] + public override SessionMessageType MessageType => SessionMessageType.UserDataChanged; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserDeletedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserDeletedMessage.cs new file mode 100644 index 0000000000..6d527be7f2 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserDeletedMessage.cs @@ -0,0 +1,24 @@ +using System; +using System.ComponentModel; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// User deleted message. +/// </summary> +public class UserDeletedMessage : WebSocketMessage<Guid>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="UserDeletedMessage"/> class. + /// </summary> + /// <param name="data">The user id.</param> + public UserDeletedMessage(Guid data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.UserDeleted)] + public override SessionMessageType MessageType => SessionMessageType.UserDeleted; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserUpdatedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserUpdatedMessage.cs new file mode 100644 index 0000000000..99e9a1f911 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserUpdatedMessage.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// User updated message. +/// </summary> +public class UserUpdatedMessage : WebSocketMessage<UserDto>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="UserUpdatedMessage"/> class. + /// </summary> + /// <param name="data">The user dto.</param> + public UserUpdatedMessage(UserDto data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.UserUpdated)] + public override SessionMessageType MessageType => SessionMessageType.UserUpdated; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/OutboundWebSocketMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/OutboundWebSocketMessage.cs new file mode 100644 index 0000000000..dba3c8392b --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/OutboundWebSocketMessage.cs @@ -0,0 +1,9 @@ +namespace MediaBrowser.Controller.Net.WebSocketMessages; + +/// <summary> +/// Class representing the list of outbound websocket message types. +/// Only used in openapi generation. +/// </summary> +public class OutboundWebSocketMessage : WebSocketMessage +{ +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Shared/KeepAliveMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Shared/KeepAliveMessage.cs new file mode 100644 index 0000000000..7f636212ca --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Shared/KeepAliveMessage.cs @@ -0,0 +1,23 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Shared; + +/// <summary> +/// Keep alive websocket messages. +/// </summary> +public class KeepAliveMessage : WebSocketMessage<int>, IInboundWebSocketMessage, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="KeepAliveMessage"/> class. + /// </summary> + /// <param name="data">The seconds to keep alive for.</param> + public KeepAliveMessage(int data) + : base(data) + { + } + + /// <inheritdoc /> + [DefaultValue(SessionMessageType.KeepAlive)] + public override SessionMessageType MessageType => SessionMessageType.KeepAlive; +} diff --git a/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs b/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs index bdebbbfd49..c0a168192e 100644 --- a/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs +++ b/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs @@ -23,13 +23,13 @@ namespace MediaBrowser.Controller.SyncPlay.Queue /// The sorted playlist. /// </summary> /// <value>The sorted playlist, or play queue of the group.</value> - private List<QueueItem> _sortedPlaylist = new List<QueueItem>(); + private List<SyncPlayQueueItem> _sortedPlaylist = new List<SyncPlayQueueItem>(); /// <summary> /// The shuffled playlist. /// </summary> /// <value>The shuffled playlist, or play queue of the group.</value> - private List<QueueItem> _shuffledPlaylist = new List<QueueItem>(); + private List<SyncPlayQueueItem> _shuffledPlaylist = new List<SyncPlayQueueItem>(); /// <summary> /// Initializes a new instance of the <see cref="PlayQueueManager" /> class. @@ -76,7 +76,7 @@ namespace MediaBrowser.Controller.SyncPlay.Queue /// Gets the current playlist considering the shuffle mode. /// </summary> /// <returns>The playlist.</returns> - public IReadOnlyList<QueueItem> GetPlaylist() + public IReadOnlyList<SyncPlayQueueItem> GetPlaylist() { return GetPlaylistInternal(); } @@ -93,7 +93,7 @@ namespace MediaBrowser.Controller.SyncPlay.Queue _sortedPlaylist = CreateQueueItemsFromArray(items); if (ShuffleMode.Equals(GroupShuffleMode.Shuffle)) { - _shuffledPlaylist = new List<QueueItem>(_sortedPlaylist); + _shuffledPlaylist = new List<SyncPlayQueueItem>(_sortedPlaylist); _shuffledPlaylist.Shuffle(); } @@ -125,14 +125,14 @@ namespace MediaBrowser.Controller.SyncPlay.Queue { if (PlayingItemIndex == NoPlayingItemIndex) { - _shuffledPlaylist = new List<QueueItem>(_sortedPlaylist); + _shuffledPlaylist = new List<SyncPlayQueueItem>(_sortedPlaylist); _shuffledPlaylist.Shuffle(); } else if (ShuffleMode.Equals(GroupShuffleMode.Sorted)) { // First time shuffle. var playingItem = _sortedPlaylist[PlayingItemIndex]; - _shuffledPlaylist = new List<QueueItem>(_sortedPlaylist); + _shuffledPlaylist = new List<SyncPlayQueueItem>(_sortedPlaylist); _shuffledPlaylist.RemoveAt(PlayingItemIndex); _shuffledPlaylist.Shuffle(); _shuffledPlaylist.Insert(0, playingItem); @@ -407,7 +407,7 @@ namespace MediaBrowser.Controller.SyncPlay.Queue /// Gets the next item in the playlist considering repeat mode and shuffle mode. /// </summary> /// <returns>The next item in the playlist.</returns> - public QueueItem GetNextItemPlaylistId() + public SyncPlayQueueItem GetNextItemPlaylistId() { int newIndex; var playlist = GetPlaylistInternal(); @@ -502,12 +502,12 @@ namespace MediaBrowser.Controller.SyncPlay.Queue /// Creates a list from the array of items. Each item is given an unique playlist identifier. /// </summary> /// <returns>The list of queue items.</returns> - private List<QueueItem> CreateQueueItemsFromArray(IReadOnlyList<Guid> items) + private List<SyncPlayQueueItem> CreateQueueItemsFromArray(IReadOnlyList<Guid> items) { - var list = new List<QueueItem>(); + var list = new List<SyncPlayQueueItem>(); foreach (var item in items) { - var queueItem = new QueueItem(item); + var queueItem = new SyncPlayQueueItem(item); list.Add(queueItem); } @@ -518,7 +518,7 @@ namespace MediaBrowser.Controller.SyncPlay.Queue /// Gets the current playlist considering the shuffle mode. /// </summary> /// <returns>The playlist.</returns> - private List<QueueItem> GetPlaylistInternal() + private List<SyncPlayQueueItem> GetPlaylistInternal() { if (ShuffleMode.Equals(GroupShuffleMode.Shuffle)) { @@ -532,7 +532,7 @@ namespace MediaBrowser.Controller.SyncPlay.Queue /// Gets the current playing item, depending on the shuffle mode. /// </summary> /// <returns>The playing item.</returns> - private QueueItem GetPlayingItem() + private SyncPlayQueueItem GetPlayingItem() { if (PlayingItemIndex == NoPlayingItemIndex) { diff --git a/MediaBrowser.Model/Net/WebSocketMessage.cs b/MediaBrowser.Model/Net/WebSocketMessage.cs deleted file mode 100644 index b00158cb33..0000000000 --- a/MediaBrowser.Model/Net/WebSocketMessage.cs +++ /dev/null @@ -1,31 +0,0 @@ -#nullable disable -#pragma warning disable CS1591 - -using System; -using MediaBrowser.Model.Session; - -namespace MediaBrowser.Model.Net -{ - /// <summary> - /// Class WebSocketMessage. - /// </summary> - /// <typeparam name="T">The type of the data.</typeparam> - public class WebSocketMessage<T> - { - /// <summary> - /// Gets or sets the type of the message. - /// </summary> - /// <value>The type of the message.</value> - public SessionMessageType MessageType { get; set; } - - public Guid MessageId { get; set; } - - public string ServerId { get; set; } - - /// <summary> - /// Gets or sets the data. - /// </summary> - /// <value>The data.</value> - public T Data { get; set; } - } -} diff --git a/MediaBrowser.Model/SyncPlay/GroupUpdate.cs b/MediaBrowser.Model/SyncPlay/GroupUpdate.cs index 6f159d653c..ec67d7ea87 100644 --- a/MediaBrowser.Model/SyncPlay/GroupUpdate.cs +++ b/MediaBrowser.Model/SyncPlay/GroupUpdate.cs @@ -1,42 +1,30 @@ using System; -namespace MediaBrowser.Model.SyncPlay +namespace MediaBrowser.Model.SyncPlay; + +/// <summary> +/// Group update without data. +/// </summary> +public abstract class GroupUpdate { /// <summary> - /// Class GroupUpdate. + /// Initializes a new instance of the <see cref="GroupUpdate"/> class. /// </summary> - /// <typeparam name="T">The type of the data of the message.</typeparam> - public class GroupUpdate<T> + /// <param name="groupId">The group identifier.</param> + protected GroupUpdate(Guid groupId) { - /// <summary> - /// Initializes a new instance of the <see cref="GroupUpdate{T}"/> class. - /// </summary> - /// <param name="groupId">The group identifier.</param> - /// <param name="type">The update type.</param> - /// <param name="data">The update data.</param> - public GroupUpdate(Guid groupId, GroupUpdateType type, T data) - { - GroupId = groupId; - Type = type; - Data = data; - } - - /// <summary> - /// Gets the group identifier. - /// </summary> - /// <value>The group identifier.</value> - public Guid GroupId { get; } - - /// <summary> - /// Gets the update type. - /// </summary> - /// <value>The update type.</value> - public GroupUpdateType Type { get; } - - /// <summary> - /// Gets the update data. - /// </summary> - /// <value>The update data.</value> - public T Data { get; } + GroupId = groupId; } + + /// <summary> + /// Gets the group identifier. + /// </summary> + /// <value>The group identifier.</value> + public Guid GroupId { get; } + + /// <summary> + /// Gets the update type. + /// </summary> + /// <value>The update type.</value> + public GroupUpdateType Type { get; init; } } diff --git a/MediaBrowser.Model/SyncPlay/GroupUpdateOfT.cs b/MediaBrowser.Model/SyncPlay/GroupUpdateOfT.cs new file mode 100644 index 0000000000..25cd444611 --- /dev/null +++ b/MediaBrowser.Model/SyncPlay/GroupUpdateOfT.cs @@ -0,0 +1,31 @@ +#pragma warning disable SA1649 + +using System; + +namespace MediaBrowser.Model.SyncPlay; + +/// <summary> +/// Class GroupUpdate. +/// </summary> +/// <typeparam name="T">The type of the data of the message.</typeparam> +public class GroupUpdate<T> : GroupUpdate +{ + /// <summary> + /// Initializes a new instance of the <see cref="GroupUpdate{T}"/> class. + /// </summary> + /// <param name="groupId">The group identifier.</param> + /// <param name="type">The update type.</param> + /// <param name="data">The update data.</param> + public GroupUpdate(Guid groupId, GroupUpdateType type, T data) + : base(groupId) + { + Data = data; + Type = type; + } + + /// <summary> + /// Gets the update data. + /// </summary> + /// <value>The update data.</value> + public T Data { get; } +} diff --git a/MediaBrowser.Model/SyncPlay/PlayQueueUpdate.cs b/MediaBrowser.Model/SyncPlay/PlayQueueUpdate.cs index cce99c77d5..376d926c9a 100644 --- a/MediaBrowser.Model/SyncPlay/PlayQueueUpdate.cs +++ b/MediaBrowser.Model/SyncPlay/PlayQueueUpdate.cs @@ -19,7 +19,7 @@ namespace MediaBrowser.Model.SyncPlay /// <param name="isPlaying">The playing item status.</param> /// <param name="shuffleMode">The shuffle mode.</param> /// <param name="repeatMode">The repeat mode.</param> - public PlayQueueUpdate(PlayQueueUpdateReason reason, DateTime lastUpdate, IReadOnlyList<QueueItem> playlist, int playingItemIndex, long startPositionTicks, bool isPlaying, GroupShuffleMode shuffleMode, GroupRepeatMode repeatMode) + public PlayQueueUpdate(PlayQueueUpdateReason reason, DateTime lastUpdate, IReadOnlyList<SyncPlayQueueItem> playlist, int playingItemIndex, long startPositionTicks, bool isPlaying, GroupShuffleMode shuffleMode, GroupRepeatMode repeatMode) { Reason = reason; LastUpdate = lastUpdate; @@ -47,7 +47,7 @@ namespace MediaBrowser.Model.SyncPlay /// Gets the playlist. /// </summary> /// <value>The playlist.</value> - public IReadOnlyList<QueueItem> Playlist { get; } + public IReadOnlyList<SyncPlayQueueItem> Playlist { get; } /// <summary> /// Gets the playing item index in the playlist. diff --git a/MediaBrowser.Model/SyncPlay/QueueItem.cs b/MediaBrowser.Model/SyncPlay/SyncPlayQueueItem.cs similarity index 80% rename from MediaBrowser.Model/SyncPlay/QueueItem.cs rename to MediaBrowser.Model/SyncPlay/SyncPlayQueueItem.cs index a6dcc109ed..da81fecbdc 100644 --- a/MediaBrowser.Model/SyncPlay/QueueItem.cs +++ b/MediaBrowser.Model/SyncPlay/SyncPlayQueueItem.cs @@ -5,13 +5,13 @@ namespace MediaBrowser.Model.SyncPlay /// <summary> /// Class QueueItem. /// </summary> - public class QueueItem + public class SyncPlayQueueItem { /// <summary> - /// Initializes a new instance of the <see cref="QueueItem"/> class. + /// Initializes a new instance of the <see cref="SyncPlayQueueItem"/> class. /// </summary> /// <param name="itemId">The item identifier.</param> - public QueueItem(Guid itemId) + public SyncPlayQueueItem(Guid itemId) { ItemId = itemId; } From ed88c8bd2d3b7d53d26e16386957e7bc13b7edd7 Mon Sep 17 00:00:00 2001 From: nyanmisaka <nst799610810@gmail.com> Date: Tue, 13 Jun 2023 10:44:06 -0400 Subject: [PATCH 345/858] Backport pull request #9411 from jellyfin/release-10.8.z Fix codec checking in CodecProfiles conditions Original-merge: 5921379a29583804fd1e99d1aad81bd4aad7f019 Merged-by: Bond-009 <bond.009@outlook.com> Backported-by: Bond_009 <bond.009@outlook.com> --- MediaBrowser.Model/Dlna/StreamBuilder.cs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index df185e40c5..2d2086ac3a 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -757,8 +757,8 @@ namespace MediaBrowser.Model.Dlna if (options.AllowVideoStreamCopy) { // prefer direct copy profile - float videoFramerate = videoStream is null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0; - TransportStreamTimestamp? timestamp = videoStream is null ? TransportStreamTimestamp.None : item.Timestamp; + float videoFramerate = videoStream?.AverageFrameRate ?? videoStream?.RealFrameRate ?? 0; + TransportStreamTimestamp? timestamp = videoStream == null ? TransportStreamTimestamp.None : item.Timestamp; int? numAudioStreams = item.GetStreamCount(MediaStreamType.Audio); int? numVideoStreams = item.GetStreamCount(MediaStreamType.Video); @@ -772,6 +772,7 @@ namespace MediaBrowser.Model.Dlna var container = transcodingProfile.Container; var appliedVideoConditions = options.Profile.CodecProfiles .Where(i => i.Type == CodecType.Video && + (string.IsNullOrEmpty(i.Codec) || string.Equals(i.Codec, videoStream?.Codec, StringComparison.OrdinalIgnoreCase)) && i.ContainsAnyCodec(videoCodec, container) && i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, videoStream?.Width, videoStream?.Height, videoStream?.BitDepth, videoStream?.BitRate, videoStream?.Profile, videoStream?.VideoRangeType, videoStream?.Level, videoFramerate, videoStream?.PacketLength, timestamp, videoStream?.IsAnamorphic, videoStream?.IsInterlaced, videoStream?.RefFrames, numVideoStreams, numAudioStreams, videoStream?.CodecTag, videoStream?.IsAVC))) .Select(i => @@ -905,6 +906,7 @@ namespace MediaBrowser.Model.Dlna var appliedVideoConditions = options.Profile.CodecProfiles .Where(i => i.Type == CodecType.Video && + (string.IsNullOrEmpty(i.Codec) || string.Equals(i.Codec, videoStream?.Codec, StringComparison.OrdinalIgnoreCase)) && i.ContainsAnyCodec(videoCodec, container) && i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc))); var isFirstAppliedCodecProfile = true; @@ -937,6 +939,7 @@ namespace MediaBrowser.Model.Dlna var appliedAudioConditions = options.Profile.CodecProfiles .Where(i => i.Type == CodecType.VideoAudio && + (string.IsNullOrEmpty(i.Codec) || string.Equals(i.Codec, audioStream?.Codec, StringComparison.OrdinalIgnoreCase)) && i.ContainsAnyCodec(audioCodec, container) && i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio))); isFirstAppliedCodecProfile = true; @@ -1176,7 +1179,9 @@ namespace MediaBrowser.Model.Dlna profile, "VideoCodecProfile", profile.CodecProfiles - .Where(codecProfile => codecProfile.Type == CodecType.Video && codecProfile.ContainsAnyCodec(videoStream?.Codec, container) && + .Where(codecProfile => codecProfile.Type == CodecType.Video && + (string.IsNullOrEmpty(codecProfile.Codec) || string.Equals(codecProfile.Codec, videoStream?.Codec, StringComparison.OrdinalIgnoreCase)) && + codecProfile.ContainsAnyCodec(videoStream?.Codec, container) && !checkVideoConditions(codecProfile.ApplyConditions).Any()) .SelectMany(codecProfile => checkVideoConditions(codecProfile.Conditions))); @@ -1585,7 +1590,9 @@ namespace MediaBrowser.Model.Dlna bool? isSecondaryAudio) { return codecProfiles - .Where(profile => profile.Type == CodecType.VideoAudio && profile.ContainsAnyCodec(codec, container) && + .Where(profile => profile.Type == CodecType.VideoAudio && + (string.IsNullOrEmpty(profile.Codec) || string.Equals(profile.Codec, codec, StringComparison.OrdinalIgnoreCase)) && + profile.ContainsAnyCodec(codec, container) && profile.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, isSecondaryAudio))) .SelectMany(profile => profile.Conditions) .Where(condition => !ConditionProcessor.IsVideoAudioConditionSatisfied(condition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, isSecondaryAudio)); @@ -1602,7 +1609,9 @@ namespace MediaBrowser.Model.Dlna bool checkConditions) { var conditions = codecProfiles - .Where(profile => profile.Type == CodecType.Audio && profile.ContainsAnyCodec(codec, container) && + .Where(profile => profile.Type == CodecType.Audio && + (string.IsNullOrEmpty(profile.Codec) || string.Equals(profile.Codec, codec, StringComparison.OrdinalIgnoreCase)) && + profile.ContainsAnyCodec(codec, container) && profile.ApplyConditions.All(applyCondition => ConditionProcessor.IsAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth))) .SelectMany(profile => profile.Conditions); From 67bc81ec96dd7a3ef712907303aacb10b175cf56 Mon Sep 17 00:00:00 2001 From: TheTyrius <16269108+TheTyrius@users.noreply.github.com> Date: Tue, 13 Jun 2023 10:44:06 -0400 Subject: [PATCH 346/858] Backport pull request #9538 from jellyfin/release-10.8.z Fix nvenc preset order Original-merge: 79bb7560dc7848608cfab538b3aaf56dea545d8f Merged-by: Cody Robibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- CONTRIBUTORS.md | 1 + MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 0b322685d1..dfb61df0a1 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -165,6 +165,7 @@ - [MinecraftPlaye](https://github.com/MinecraftPlaye) - [RealGreenDragon](https://github.com/RealGreenDragon) - [ipitio](https://github.com/ipitio) + - [TheTyrius](https://github.com/TheTyrius) # Emby Contributors diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 920925fc68..906a04a610 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -1549,11 +1549,11 @@ namespace MediaBrowser.Controller.MediaEncoding param += " -preset p7"; break; - case "slow": + case "slower": param += " -preset p6"; break; - case "slower": + case "slow": param += " -preset p5"; break; @@ -1586,8 +1586,8 @@ namespace MediaBrowser.Controller.MediaEncoding switch (encodingOptions.EncoderPreset) { case "veryslow": - case "slow": case "slower": + case "slow": param += " -quality quality"; break; From 4c8d3827658342b630472cc4dc1601ae56ffe39c Mon Sep 17 00:00:00 2001 From: nyanmisaka <nst799610810@gmail.com> Date: Tue, 13 Jun 2023 10:53:35 -0400 Subject: [PATCH 347/858] Backport pull request #9642 from jellyfin/release-10.8.z Fix the brightness of VPP tonemap and add the tonemap mode Original-merge: d5a8419bc52ba06c070012849ba166dd2fbff8b0 Merged-by: Cody Robibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- .../MediaEncoding/EncodingHelper.cs | 25 ++++++++++++++----- .../Configuration/EncodingOptions.cs | 16 ++++++------ 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 906a04a610..b6fa49fcd3 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -45,6 +45,7 @@ namespace MediaBrowser.Controller.MediaEncoding private readonly Version _minFFmpegImplictHwaccel = new Version(6, 0); private readonly Version _minFFmpegHwaUnsafeOutput = new Version(6, 0); + private readonly Version _minFFmpegOclCuTonemapMode = new Version(5, 1, 3); private static readonly string[] _videoProfilesH264 = new[] { @@ -2929,7 +2930,7 @@ namespace MediaBrowser.Controller.MediaEncoding return string.Empty; } - public static string GetHwTonemapFilter(EncodingOptions options, string hwTonemapSuffix, string videoFormat) + public string GetHwTonemapFilter(EncodingOptions options, string hwTonemapSuffix, string videoFormat) { if (string.IsNullOrEmpty(hwTonemapSuffix)) { @@ -2941,7 +2942,8 @@ namespace MediaBrowser.Controller.MediaEncoding if (string.Equals(hwTonemapSuffix, "vaapi", StringComparison.OrdinalIgnoreCase)) { - args = "tonemap_vaapi=format={0}:p=bt709:t=bt709:m=bt709,procamp_vaapi=b={1}:c={2}:extra_hw_frames=16"; + args = "procamp_vaapi=b={2}:c={3}," + args + ":extra_hw_frames=32"; + return string.Format( CultureInfo.InvariantCulture, args, @@ -2972,14 +2974,24 @@ namespace MediaBrowser.Controller.MediaEncoding { args = "tonemap_{0}=format={1}:p=bt709:t=bt709:m=bt709:tonemap={2}:peak={3}:desat={4}"; - if (options.TonemappingParam != 0) + if (string.Equals(options.TonemappingMode, "max", StringComparison.OrdinalIgnoreCase) + || string.Equals(options.TonemappingMode, "rgb", StringComparison.OrdinalIgnoreCase)) { - args += ":param={5}"; + if (_mediaEncoder.EncoderVersion >= _minFFmpegOclCuTonemapMode) + { + args += ":tonemap_mode={5}"; + } } - if (!string.Equals(options.TonemappingRange, "auto", StringComparison.OrdinalIgnoreCase)) + if (options.TonemappingParam != 0) { - args += ":range={6}"; + args += ":param={6}"; + } + + if (string.Equals(options.TonemappingRange, "tv", StringComparison.OrdinalIgnoreCase) + || string.Equals(options.TonemappingRange, "pc", StringComparison.OrdinalIgnoreCase)) + { + args += ":range={7}"; } } @@ -2991,6 +3003,7 @@ namespace MediaBrowser.Controller.MediaEncoding algorithm, options.TonemappingPeak, options.TonemappingDesat, + options.TonemappingMode, options.TonemappingParam, options.TonemappingRange); } diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index f9f63f751a..ac2f1e71a3 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -27,13 +27,13 @@ public class EncodingOptions EnableTonemapping = false; EnableVppTonemapping = false; TonemappingAlgorithm = "bt2390"; + TonemappingMode = "auto"; TonemappingRange = "auto"; TonemappingDesat = 0; - TonemappingThreshold = 0.8; TonemappingPeak = 100; TonemappingParam = 0; - VppTonemappingBrightness = 0; - VppTonemappingContrast = 1.2; + VppTonemappingBrightness = 16; + VppTonemappingContrast = 1; H264Crf = 23; H265Crf = 28; DeinterlaceDoubleRate = false; @@ -137,6 +137,11 @@ public class EncodingOptions /// </summary> public string TonemappingAlgorithm { get; set; } + /// <summary> + /// Gets or sets the tone-mapping mode. + /// </summary> + public string TonemappingMode { get; set; } + /// <summary> /// Gets or sets the tone-mapping range. /// </summary> @@ -147,11 +152,6 @@ public class EncodingOptions /// </summary> public double TonemappingDesat { get; set; } - /// <summary> - /// Gets or sets the tone-mapping threshold. - /// </summary> - public double TonemappingThreshold { get; set; } - /// <summary> /// Gets or sets the tone-mapping peak. /// </summary> From fdc16e23c43bf46e551c9b5d64572f566a2afaf3 Mon Sep 17 00:00:00 2001 From: nyanmisaka <nst799610810@gmail.com> Date: Tue, 13 Jun 2023 10:55:15 -0400 Subject: [PATCH 348/858] Backport pull request #9671 from jellyfin/release-10.8.z Fix the canvas size for DVBSUB and DVDSUB subtitles Original-merge: eba95cc7f0304ebc9c1a8c0fc11a29ffca6fc97d Merged-by: Joshua M. Boniface <joshua@boniface.me> Backported-by: Bond_009 <bond.009@outlook.com> --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index b6fa49fcd3..ff183daa03 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -755,9 +755,12 @@ namespace MediaBrowser.Controller.MediaEncoding public string GetGraphicalSubCanvasSize(EncodingJobInfo state) { + // DVBSUB and DVDSUB use the fixed canvas size 720x576 if (state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode - && !state.SubtitleStream.IsTextSubtitleStream) + && !state.SubtitleStream.IsTextSubtitleStream + && !string.Equals(state.SubtitleStream.Codec, "DVBSUB", StringComparison.OrdinalIgnoreCase) + && !string.Equals(state.SubtitleStream.Codec, "DVDSUB", StringComparison.OrdinalIgnoreCase)) { var inW = state.VideoStream?.Width; var inH = state.VideoStream?.Height; From 14d061f543d3f9da6568272227dd0666065a1673 Mon Sep 17 00:00:00 2001 From: Dmitry Lyzo <ashephard0@gmail.com> Date: Tue, 13 Jun 2023 10:55:16 -0400 Subject: [PATCH 349/858] Backport pull request #9723 from jellyfin/release-10.8.z Fix multiple codec checking in CodecProfiles conditions Original-merge: bec8d7b3f5923c240bbc5bcf160c19f7cc591211 Merged-by: Bond-009 <bond.009@outlook.com> Backported-by: Bond_009 <bond.009@outlook.com> --- MediaBrowser.Model/Dlna/StreamBuilder.cs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 2d2086ac3a..0a955e917f 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -768,11 +768,10 @@ namespace MediaBrowser.Model.Dlna if (ContainerProfile.ContainsContainer(videoCodecs, item.VideoStream?.Codec)) { - var videoCodec = transcodingProfile.VideoCodec; + var videoCodec = videoStream?.Codec; var container = transcodingProfile.Container; var appliedVideoConditions = options.Profile.CodecProfiles .Where(i => i.Type == CodecType.Video && - (string.IsNullOrEmpty(i.Codec) || string.Equals(i.Codec, videoStream?.Codec, StringComparison.OrdinalIgnoreCase)) && i.ContainsAnyCodec(videoCodec, container) && i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, videoStream?.Width, videoStream?.Height, videoStream?.BitDepth, videoStream?.BitRate, videoStream?.Profile, videoStream?.VideoRangeType, videoStream?.Level, videoFramerate, videoStream?.PacketLength, timestamp, videoStream?.IsAnamorphic, videoStream?.IsInterlaced, videoStream?.RefFrames, numVideoStreams, numAudioStreams, videoStream?.CodecTag, videoStream?.IsAVC))) .Select(i => @@ -906,8 +905,7 @@ namespace MediaBrowser.Model.Dlna var appliedVideoConditions = options.Profile.CodecProfiles .Where(i => i.Type == CodecType.Video && - (string.IsNullOrEmpty(i.Codec) || string.Equals(i.Codec, videoStream?.Codec, StringComparison.OrdinalIgnoreCase)) && - i.ContainsAnyCodec(videoCodec, container) && + i.ContainsAnyCodec(videoStream?.Codec, container) && i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc))); var isFirstAppliedCodecProfile = true; foreach (var i in appliedVideoConditions) @@ -939,8 +937,7 @@ namespace MediaBrowser.Model.Dlna var appliedAudioConditions = options.Profile.CodecProfiles .Where(i => i.Type == CodecType.VideoAudio && - (string.IsNullOrEmpty(i.Codec) || string.Equals(i.Codec, audioStream?.Codec, StringComparison.OrdinalIgnoreCase)) && - i.ContainsAnyCodec(audioCodec, container) && + i.ContainsAnyCodec(audioStream?.Codec, container) && i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio))); isFirstAppliedCodecProfile = true; foreach (var codecProfile in appliedAudioConditions) @@ -1180,7 +1177,6 @@ namespace MediaBrowser.Model.Dlna "VideoCodecProfile", profile.CodecProfiles .Where(codecProfile => codecProfile.Type == CodecType.Video && - (string.IsNullOrEmpty(codecProfile.Codec) || string.Equals(codecProfile.Codec, videoStream?.Codec, StringComparison.OrdinalIgnoreCase)) && codecProfile.ContainsAnyCodec(videoStream?.Codec, container) && !checkVideoConditions(codecProfile.ApplyConditions).Any()) .SelectMany(codecProfile => checkVideoConditions(codecProfile.Conditions))); @@ -1591,7 +1587,6 @@ namespace MediaBrowser.Model.Dlna { return codecProfiles .Where(profile => profile.Type == CodecType.VideoAudio && - (string.IsNullOrEmpty(profile.Codec) || string.Equals(profile.Codec, codec, StringComparison.OrdinalIgnoreCase)) && profile.ContainsAnyCodec(codec, container) && profile.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, isSecondaryAudio))) .SelectMany(profile => profile.Conditions) @@ -1610,7 +1605,6 @@ namespace MediaBrowser.Model.Dlna { var conditions = codecProfiles .Where(profile => profile.Type == CodecType.Audio && - (string.IsNullOrEmpty(profile.Codec) || string.Equals(profile.Codec, codec, StringComparison.OrdinalIgnoreCase)) && profile.ContainsAnyCodec(codec, container) && profile.ApplyConditions.All(applyCondition => ConditionProcessor.IsAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth))) .SelectMany(profile => profile.Conditions); From 23b90555476de9509dcc2bb42bfa6f8ad0e42b74 Mon Sep 17 00:00:00 2001 From: nyanmisaka <nst799610810@gmail.com> Date: Tue, 13 Jun 2023 01:13:48 +0800 Subject: [PATCH 350/858] Rearrage the Amd vaapi-vulkan pipeline for synchronization Signed-off-by: nyanmisaka <nst799610810@gmail.com> --- .../MediaEncoding/EncodingHelper.cs | 339 +++++++++++------- 1 file changed, 204 insertions(+), 135 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index ff183daa03..39d53768e9 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -163,7 +163,8 @@ namespace MediaBrowser.Controller.MediaEncoding private bool IsVaapiFullSupported() { - return _mediaEncoder.SupportsHwaccel("vaapi") + return _mediaEncoder.SupportsHwaccel("drm") + && _mediaEncoder.SupportsHwaccel("vaapi") && _mediaEncoder.SupportsFilter("scale_vaapi") && _mediaEncoder.SupportsFilter("deinterlace_vaapi") && _mediaEncoder.SupportsFilter("tonemap_vaapi") @@ -713,28 +714,43 @@ namespace MediaBrowser.Controller.MediaEncoding options); } - private string GetVaapiDeviceArgs(string renderNodePath, string driver, string kernelDriver, string alias) + private string GetVaapiDeviceArgs(string renderNodePath, string driver, string kernelDriver, string srcDeviceAlias, string alias) { alias ??= VaapiAlias; renderNodePath = renderNodePath ?? "/dev/dri/renderD128"; - var options = string.IsNullOrEmpty(driver) - ? renderNodePath - : ",driver=" + driver + (string.IsNullOrEmpty(kernelDriver) ? string.Empty : ",kernel_driver=" + kernelDriver); + var driverOpts = string.IsNullOrEmpty(driver) + ? ":" + renderNodePath + : ":,driver=" + driver + (string.IsNullOrEmpty(kernelDriver) ? string.Empty : ",kernel_driver=" + kernelDriver); + var options = string.IsNullOrEmpty(srcDeviceAlias) + ? driverOpts + : "@" + srcDeviceAlias; return string.Format( CultureInfo.InvariantCulture, - " -init_hw_device vaapi={0}:{1}", + " -init_hw_device vaapi={0}{1}", alias, options); } + private string GetDrmDeviceArgs(string renderNodePath, string alias) + { + alias ??= DrmAlias; + renderNodePath = renderNodePath ?? "/dev/dri/renderD128"; + + return string.Format( + CultureInfo.InvariantCulture, + " -init_hw_device drm={0}:{1}", + alias, + renderNodePath); + } + private string GetQsvDeviceArgs(string alias) { var arg = " -init_hw_device qsv=" + (alias ?? QsvAlias); if (OperatingSystem.IsLinux()) { // derive qsv from vaapi device - return GetVaapiDeviceArgs(null, "iHD", "i915", VaapiAlias) + arg + "@" + VaapiAlias; + return GetVaapiDeviceArgs(null, "iHD", "i915", null, VaapiAlias) + arg + "@" + VaapiAlias; } if (OperatingSystem.IsWindows()) @@ -828,21 +844,17 @@ namespace MediaBrowser.Controller.MediaEncoding if (_mediaEncoder.IsVaapiDeviceInteliHD) { - args.Append(GetVaapiDeviceArgs(null, "iHD", null, VaapiAlias)); + args.Append(GetVaapiDeviceArgs(null, "iHD", null, null, VaapiAlias)); } else if (_mediaEncoder.IsVaapiDeviceInteli965) { // Only override i965 since it has lower priority than iHD in libva lookup. Environment.SetEnvironmentVariable("LIBVA_DRIVER_NAME", "i965"); Environment.SetEnvironmentVariable("LIBVA_DRIVER_NAME_JELLYFIN", "i965"); - args.Append(GetVaapiDeviceArgs(null, "i965", null, VaapiAlias)); - } - else - { - args.Append(GetVaapiDeviceArgs(options.VaapiDevice, null, null, VaapiAlias)); + args.Append(GetVaapiDeviceArgs(null, "i965", null, null, VaapiAlias)); } - var filterDevArgs = GetFilterHwDeviceArgs(VaapiAlias); + var filterDevArgs = string.Empty; var doOclTonemap = isHwTonemapAvailable && IsOpenclFullSupported(); if (_mediaEncoder.IsVaapiDeviceInteliHD || _mediaEncoder.IsVaapiDeviceInteli965) @@ -859,15 +871,24 @@ namespace MediaBrowser.Controller.MediaEncoding && _mediaEncoder.IsVaapiDeviceSupportVulkanFmtModifier && Environment.OSVersion.Version >= _minKernelVersionAmdVkFmtModifier) { + args.Append(GetDrmDeviceArgs(options.VaapiDevice, DrmAlias)); + args.Append(GetVaapiDeviceArgs(null, null, null, DrmAlias, VaapiAlias)); + args.Append(GetVulkanDeviceArgs(0, null, DrmAlias, VulkanAlias)); + // libplacebo wants an explicitly set vulkan filter device. - args.Append(GetVulkanDeviceArgs(0, null, VaapiAlias, VulkanAlias)); filterDevArgs = GetFilterHwDeviceArgs(VulkanAlias); } - else if (doOclTonemap) + else { - // ROCm/ROCr OpenCL runtime - args.Append(GetOpenclDeviceArgs(0, "Advanced Micro Devices", null, OpenclAlias)); - filterDevArgs = GetFilterHwDeviceArgs(OpenclAlias); + args.Append(GetVaapiDeviceArgs(options.VaapiDevice, null, null, null, VaapiAlias)); + filterDevArgs = GetFilterHwDeviceArgs(VaapiAlias); + + if (doOclTonemap) + { + // ROCm/ROCr OpenCL runtime + args.Append(GetOpenclDeviceArgs(0, "Advanced Micro Devices", null, OpenclAlias)); + filterDevArgs = GetFilterHwDeviceArgs(OpenclAlias); + } } } else if (doOclTonemap) @@ -3011,6 +3032,75 @@ namespace MediaBrowser.Controller.MediaEncoding options.TonemappingRange); } + public string GetLibplaceboFilter( + EncodingOptions options, + string videoFormat, + bool doTonemap, + int? videoWidth, + int? videoHeight, + int? requestedWidth, + int? requestedHeight, + int? requestedMaxWidth, + int? requestedMaxHeight) + { + var (outWidth, outHeight) = GetFixedOutputSize( + videoWidth, + videoHeight, + requestedWidth, + requestedHeight, + requestedMaxWidth, + requestedMaxHeight); + + var isFormatFixed = !string.IsNullOrEmpty(videoFormat); + var isSizeFixed = !videoWidth.HasValue + || outWidth.Value != videoWidth.Value + || !videoHeight.HasValue + || outHeight.Value != videoHeight.Value; + + var sizeArg = isSizeFixed ? (":w=" + outWidth.Value + ":h=" + outHeight.Value) : string.Empty; + var formatArg = isFormatFixed ? (":format=" + videoFormat) : string.Empty; + var tonemapArg = string.Empty; + + if (doTonemap) + { + var algorithm = options.TonemappingAlgorithm; + var mode = options.TonemappingMode; + var range = options.TonemappingRange; + + if (string.Equals(algorithm, "bt2390", StringComparison.OrdinalIgnoreCase)) + { + algorithm = "bt.2390"; + } + else if (string.Equals(algorithm, "none", StringComparison.OrdinalIgnoreCase)) + { + algorithm = "clip"; + } + + tonemapArg = ":tonemapping=" + algorithm; + + if (string.Equals(mode, "max", StringComparison.OrdinalIgnoreCase) + || string.Equals(mode, "rgb", StringComparison.OrdinalIgnoreCase)) + { + tonemapArg += ":tonemapping_mode=" + mode; + } + + tonemapArg += ":peak_detect=0:color_primaries=bt709:color_trc=bt709:colorspace=bt709"; + + if (string.Equals(range, "tv", StringComparison.OrdinalIgnoreCase) + || string.Equals(range, "pc", StringComparison.OrdinalIgnoreCase)) + { + tonemapArg += ":range=" + range; + } + } + + return string.Format( + CultureInfo.InvariantCulture, + "libplacebo=upscaler=none:downscaler=none{0}{1}{2}", + sizeArg, + formatArg, + tonemapArg); + } + /// <summary> /// Gets the parameter of software filter chain. /// </summary> @@ -4240,7 +4330,6 @@ namespace MediaBrowser.Controller.MediaEncoding var isVaapiEncoder = vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase); var isSwDecoder = string.IsNullOrEmpty(vidDecoder); var isSwEncoder = !isVaapiEncoder; - var isVaInVaOut = isVaapiDecoder && isVaapiEncoder; var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true); var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true); @@ -4269,99 +4358,81 @@ namespace MediaBrowser.Controller.MediaEncoding mainFilters.Add(swDeintFilter); } - var outFormat = doVkTonemap ? "yuv420p10le" : "nv12"; - var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH); - // sw scale - mainFilters.Add(swScaleFilter); - mainFilters.Add("format=" + outFormat); - - // keep video at memory except vk tonemap, - // since the overhead caused by hwupload >>> using sw filter. - // sw => hw - if (doVkTonemap) + if (doVkTonemap || hasSubs) { - mainFilters.Add("hwupload=derive_device=vaapi"); - mainFilters.Add("format=vaapi"); - mainFilters.Add("hwmap=derive_device=vulkan"); + // sw => hw + mainFilters.Add("hwupload=derive_device=vulkan"); mainFilters.Add("format=vulkan"); } + else + { + // sw scale + var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH); + mainFilters.Add(swScaleFilter); + mainFilters.Add("format=nv12"); + } } else if (isVaapiDecoder) { // INPUT vaapi surface(vram) + if (doVkTonemap || hasSubs) + { + // map from vaapi to vulkan/drm via interop (Vega/gfx9+). + mainFilters.Add("hwmap=derive_device=vulkan"); + mainFilters.Add("format=vulkan"); + } + else + { + // hw deint + if (doDeintH2645) + { + var deintFilter = GetHwDeinterlaceFilter(state, options, "vaapi"); + mainFilters.Add(deintFilter); + } + + // hw scale + var hwScaleFilter = GetHwScaleFilter("vaapi", "nv12", inW, inH, reqW, reqH, reqMaxW, reqMaxH); + mainFilters.Add(hwScaleFilter); + } + } + + // vk libplacebo + if (doVkTonemap || hasSubs) + { + var libplaceboFilter = GetLibplaceboFilter(options, "bgra", doVkTonemap, inW, inH, reqW, reqH, reqMaxW, reqMaxH); + mainFilters.Add(libplaceboFilter); + } + + if (doVkTonemap && !hasSubs) + { + // OUTPUT vaapi(nv12) surface(vram) + // map from vulkan/drm to vaapi via interop (Vega/gfx9+). + mainFilters.Add("hwmap=derive_device=drm"); + mainFilters.Add("format=drm_prime"); + mainFilters.Add("hwmap=derive_device=vaapi"); + mainFilters.Add("format=vaapi"); + + // clear the surf->meta_offset and output nv12 + mainFilters.Add("scale_vaapi=format=nv12"); + // hw deint if (doDeintH2645) { var deintFilter = GetHwDeinterlaceFilter(state, options, "vaapi"); mainFilters.Add(deintFilter); } - - var outFormat = doVkTonemap ? string.Empty : (hasSubs && isVaInVaOut ? "bgra" : "nv12"); - var hwScaleFilter = GetHwScaleFilter("vaapi", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH); - - // allocate extra pool sizes for overlay_vulkan - if (!string.IsNullOrEmpty(hwScaleFilter) && isVaInVaOut && hasSubs) - { - hwScaleFilter += ":extra_hw_frames=32"; - } - - // hw scale - mainFilters.Add(hwScaleFilter); } - if ((isVaapiDecoder && doVkTonemap) || (isVaInVaOut && (doVkTonemap || hasSubs))) + if (!hasSubs) { - // map from vaapi to vulkan via vaapi-vulkan interop (Vega/gfx9+). - mainFilters.Add("hwmap=derive_device=vulkan"); - mainFilters.Add("format=vulkan"); - } - - // vk tonemap - if (doVkTonemap) - { - var outFormat = isVaInVaOut && hasSubs ? "bgra" : "nv12"; - var tonemapFilter = GetHwTonemapFilter(options, "vulkan", outFormat); - mainFilters.Add(tonemapFilter); - } - - if (doVkTonemap && isVaInVaOut && !hasSubs) - { - // OUTPUT vaapi(nv12/bgra) surface(vram) - // reverse-mapping via vaapi-vulkan interop. - mainFilters.Add("hwmap=derive_device=vaapi:reverse=1"); - mainFilters.Add("format=vaapi"); - } - - var memoryOutput = false; - var isUploadForVkTonemap = isSwDecoder && doVkTonemap; - if ((isVaapiDecoder && isSwEncoder) || isUploadForVkTonemap) - { - memoryOutput = true; - // OUTPUT nv12 surface(memory) - mainFilters.Add("hwdownload"); - mainFilters.Add("format=nv12"); - } - - // OUTPUT nv12 surface(memory) - if (isSwDecoder && isVaapiEncoder) - { - memoryOutput = true; - } - - if (memoryOutput) - { - // text subtitles - if (hasTextSubs) + if (isSwEncoder && (doVkTonemap || isVaapiDecoder)) { - var textSubtitlesFilter = GetTextSubtitlesFilter(state, false, false); - mainFilters.Add(textSubtitlesFilter); + mainFilters.Add("hwdownload"); + mainFilters.Add("format=nv12"); } - } - if (memoryOutput && isVaapiEncoder) - { - if (!hasGraphicalSubs) + if (isSwDecoder && isVaapiEncoder && !doVkTonemap) { mainFilters.Add("hwupload_vaapi"); } @@ -4370,55 +4441,53 @@ namespace MediaBrowser.Controller.MediaEncoding /* Make sub and overlay filters for subtitle stream */ var subFilters = new List<string>(); var overlayFilters = new List<string>(); - if (isVaInVaOut) - { - if (hasSubs) - { - if (hasGraphicalSubs) - { - // scale=s=1280x720,format=bgra,hwupload - var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); - subFilters.Add(subSwScaleFilter); - subFilters.Add("format=bgra"); - } - else if (hasTextSubs) - { - var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, reqMaxH, hasAssSubs ? 10 : 5); - var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true); - subFilters.Add(alphaSrcFilter); - subFilters.Add("format=bgra"); - subFilters.Add(subTextSubtitlesFilter); - } - - // prefer vaapi hwupload to vulkan hwupload, - // Mesa RADV does not support a dedicated transfer queue. - subFilters.Add("hwupload=derive_device=vaapi"); - subFilters.Add("format=vaapi"); - subFilters.Add("hwmap=derive_device=vulkan"); - subFilters.Add("format=vulkan"); - - overlayFilters.Add("overlay_vulkan=eof_action=endall:shortest=1:repeatlast=0"); - - // TODO: figure out why libplacebo can sync without vaSyncSurface VPP support in radeonsi. - overlayFilters.Add("libplacebo=format=nv12:apply_filmgrain=0:apply_dolbyvision=0:upscaler=none:downscaler=none:dithering=none"); - - // OUTPUT vaapi(nv12/bgra) surface(vram) - // reverse-mapping via vaapi-vulkan interop. - overlayFilters.Add("hwmap=derive_device=vaapi:reverse=1"); - overlayFilters.Add("format=vaapi"); - } - } - else if (memoryOutput) + if (hasSubs) { if (hasGraphicalSubs) { + // scale=s=1280x720,format=bgra,hwupload var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subSwScaleFilter); - overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0"); + subFilters.Add("format=bgra"); + } + else if (hasTextSubs) + { + var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, reqMaxH, hasAssSubs ? 10 : 5); + var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true); + subFilters.Add(alphaSrcFilter); + subFilters.Add("format=bgra"); + subFilters.Add(subTextSubtitlesFilter); + } - if (isVaapiEncoder) + subFilters.Add("hwupload=derive_device=vulkan"); + subFilters.Add("format=vulkan"); + + overlayFilters.Add("overlay_vulkan=eof_action=endall:shortest=1:repeatlast=0"); + + if (isSwEncoder) + { + // OUTPUT nv12 surface(memory) + overlayFilters.Add("scale_vulkan=format=nv12"); + overlayFilters.Add("hwdownload"); + overlayFilters.Add("format=nv12"); + } + else if (isVaapiEncoder) + { + // OUTPUT vaapi(nv12) surface(vram) + // map from vulkan/drm to vaapi via interop (Vega/gfx9+). + overlayFilters.Add("hwmap=derive_device=drm"); + overlayFilters.Add("format=drm_prime"); + overlayFilters.Add("hwmap=derive_device=vaapi"); + overlayFilters.Add("format=vaapi"); + + // clear the surf->meta_offset and output nv12 + overlayFilters.Add("scale_vaapi=format=nv12"); + + // hw deint + if (doDeintH2645) { - overlayFilters.Add("hwupload_vaapi"); + var deintFilter = GetHwDeinterlaceFilter(state, options, "vaapi"); + overlayFilters.Add(deintFilter); } } } From 6771b5cabe96b4b3cbd1cd0c998d564f3dd17ed4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 15 Jun 2023 09:37:38 -0600 Subject: [PATCH 351/858] chore(deps): update dependency serilog.sinks.graylog to v3.0.1 (#9892) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 49081399e1..1c3f68a800 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -62,7 +62,7 @@ <PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" /> <PackageVersion Include="Serilog.Sinks.Console" Version="4.1.0" /> <PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" /> - <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.0.0" /> + <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.0.1" /> <PackageVersion Include="SerilogAnalyzer" Version="0.15.0" /> <PackageVersion Include="SharpFuzz" Version="2.0.2" /> <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.3" /> From 0dffe64489f826b91410bddc32ad2ceb1c5cc0d0 Mon Sep 17 00:00:00 2001 From: Dominik <git@secnd.me> Date: Thu, 15 Jun 2023 19:55:11 +0200 Subject: [PATCH 352/858] Add baseUrlParam back in and fix indentation --- Jellyfin.Api/Controllers/DynamicHlsController.cs | 9 +++++++++ MediaBrowser.Model/Configuration/EncodingOptions.cs | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 86be68004d..9f2088e36e 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1632,6 +1632,15 @@ public class DynamicHlsController : BaseJellyfinApiController ? _encodingOptions.MaxMuxingQueueSize.ToString(CultureInfo.InvariantCulture) : "128"; + var baseUrlParam = string.Empty; + if (isEventPlaylist) + { + baseUrlParam = string.Format( + CultureInfo.InvariantCulture, + " -hls_base_url \"hls/{0}/\"", + Path.GetFileNameWithoutExtension(outputPath)); + } + var hlsArguments = GetHlsArguments(isEventPlaylist, state.SegmentLength); return string.Format( diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index b5b6e86c8c..f11a43122a 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -103,12 +103,12 @@ public class EncodingOptions /// Gets or sets the delay after which throttling happens. /// </summary> public int ThrottleDelaySeconds { get; set; } - + /// <summary> /// Gets or sets a value indicating whether segment deletion is enabled. /// </summary> public bool EnableSegmentDeletion { get; set; } - + /// <summary> /// Gets or sets seconds for which segments should be kept before being deleted. /// </summary> From dbe44a591cbc19859161cd6cd7f63dab91110594 Mon Sep 17 00:00:00 2001 From: Dominik <git@secnd.me> Date: Thu, 15 Jun 2023 21:38:45 +0200 Subject: [PATCH 353/858] Pick safer default value for segments to keep --- MediaBrowser.Model/Configuration/EncodingOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index f11a43122a..a53be0fee7 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -21,7 +21,7 @@ public class EncodingOptions EnableThrottling = false; ThrottleDelaySeconds = 180; EnableSegmentDeletion = false; - SegmentKeepSeconds = 360; + SegmentKeepSeconds = 720; EncodingThreadCount = -1; // This is a DRM device that is almost guaranteed to be there on every intel platform, // plus it's the default one in ffmpeg if you don't specify anything From a81f3e7c97ef804bf89d891170e97f9c708a1ea9 Mon Sep 17 00:00:00 2001 From: nyanmisaka <nst799610810@gmail.com> Date: Sun, 18 Jun 2023 09:59:55 +0800 Subject: [PATCH 354/858] Fix #9642 backport Signed-off-by: nyanmisaka <nst799610810@gmail.com> --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 39d53768e9..c5fb4dd683 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -2966,7 +2966,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (string.Equals(hwTonemapSuffix, "vaapi", StringComparison.OrdinalIgnoreCase)) { - args = "procamp_vaapi=b={2}:c={3}," + args + ":extra_hw_frames=32"; + args = "procamp_vaapi=b={1}:c={2},tonemap_vaapi=format={0}:p=bt709:t=bt709:m=bt709:extra_hw_frames=32"; return string.Format( CultureInfo.InvariantCulture, From f81b004d3ffbcf6720d252e7ad54ddf3845dd1af Mon Sep 17 00:00:00 2001 From: nyanmisaka <nst799610810@gmail.com> Date: Sun, 18 Jun 2023 10:00:53 +0800 Subject: [PATCH 355/858] Removed unused lines Signed-off-by: nyanmisaka <nst799610810@gmail.com> --- .../MediaEncoding/EncodingHelper.cs | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index c5fb4dd683..26564740d8 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -2975,25 +2975,6 @@ namespace MediaBrowser.Controller.MediaEncoding options.VppTonemappingBrightness, options.VppTonemappingContrast); } - - if (string.Equals(hwTonemapSuffix, "vulkan", StringComparison.OrdinalIgnoreCase)) - { - args = "libplacebo=format={1}:tonemapping={2}:color_primaries=bt709:color_trc=bt709:colorspace=bt709:peak_detect=0:upscaler=none:downscaler=none"; - - if (!string.Equals(options.TonemappingRange, "auto", StringComparison.OrdinalIgnoreCase)) - { - args += ":range={6}"; - } - - if (string.Equals(options.TonemappingAlgorithm, "bt2390", StringComparison.OrdinalIgnoreCase)) - { - algorithm = "bt.2390"; - } - else if (string.Equals(options.TonemappingAlgorithm, "none", StringComparison.OrdinalIgnoreCase)) - { - algorithm = "clip"; - } - } else { args = "tonemap_{0}=format={1}:p=bt709:t=bt709:m=bt709:tonemap={2}:peak={3}:desat={4}"; From 7dec2608fc3762991706d8a4cce61f71fa371e8c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 18 Jun 2023 19:22:57 -0600 Subject: [PATCH 356/858] chore(deps): update dotnet monorepo (#9886) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Cody Robibero <cody@robibe.ro> --- Directory.Packages.props | 20 ++++++++++---------- deployment/Dockerfile.centos.amd64 | 2 +- deployment/Dockerfile.fedora.amd64 | 2 +- deployment/Dockerfile.ubuntu.amd64 | 2 +- deployment/Dockerfile.ubuntu.arm64 | 2 +- deployment/Dockerfile.ubuntu.armhf | 2 +- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 1c3f68a800..002e0403d0 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -23,13 +23,13 @@ <PackageVersion Include="libse" Version="3.6.13" /> <PackageVersion Include="LrcParser" Version="2023.524.0" /> <PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" /> - <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.5" /> - <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.5" /> + <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.7" /> + <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.7" /> <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.5" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.5" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.5" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.5" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.7" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.7" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.7" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.7" /> <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" /> @@ -38,11 +38,11 @@ <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" /> - <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.5" /> - <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.5" /> + <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.7" /> + <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.7" /> <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Http" Version="7.0.0" /> - <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" /> + <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" /> <PackageVersion Include="Microsoft.Extensions.Logging" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Options" Version="7.0.1" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.6.2" /> @@ -77,7 +77,7 @@ <PackageVersion Include="System.Globalization" Version="4.3.0" /> <PackageVersion Include="System.Linq.Async" Version="6.0.1" /> <PackageVersion Include="System.Text.Encoding.CodePages" Version="7.0.0" /> - <PackageVersion Include="System.Text.Json" Version="7.0.2" /> + <PackageVersion Include="System.Text.Json" Version="7.0.3" /> <PackageVersion Include="System.Threading.Tasks.Dataflow" Version="7.0.0" /> <PackageVersion Include="TagLibSharp" Version="2.3.0" /> <PackageVersion Include="TMDbLib" Version="2.0.0" /> diff --git a/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64 index 16bb99a3d0..ee1bc911bb 100644 --- a/deployment/Dockerfile.centos.amd64 +++ b/deployment/Dockerfile.centos.amd64 @@ -13,7 +13,7 @@ RUN yum update -yq \ && yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget # Install DotNET SDK -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ebfd0bf8-79bd-480a-9e81-0b217463738d/9adc6bf0614ce02670101e278a2d8555/dotnet-sdk-7.0.203-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/9c86d7b4-acb2-4be4-8a89-d13bc3c3f28f/1d044c7c29df018e8f2837bb343e8a84/dotnet-sdk-7.0.304-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.fedora.amd64 b/deployment/Dockerfile.fedora.amd64 index 8d708f9028..0a2ee7b3f3 100644 --- a/deployment/Dockerfile.fedora.amd64 +++ b/deployment/Dockerfile.fedora.amd64 @@ -12,7 +12,7 @@ RUN dnf update -yq \ && dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd wget make # Install DotNET SDK -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ebfd0bf8-79bd-480a-9e81-0b217463738d/9adc6bf0614ce02670101e278a2d8555/dotnet-sdk-7.0.203-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/9c86d7b4-acb2-4be4-8a89-d13bc3c3f28f/1d044c7c29df018e8f2837bb343e8a84/dotnet-sdk-7.0.304-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64 index b440a21421..457f28f50c 100644 --- a/deployment/Dockerfile.ubuntu.amd64 +++ b/deployment/Dockerfile.ubuntu.amd64 @@ -17,7 +17,7 @@ RUN apt-get update -yqq \ libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ebfd0bf8-79bd-480a-9e81-0b217463738d/9adc6bf0614ce02670101e278a2d8555/dotnet-sdk-7.0.203-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/9c86d7b4-acb2-4be4-8a89-d13bc3c3f28f/1d044c7c29df018e8f2837bb343e8a84/dotnet-sdk-7.0.304-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.arm64 b/deployment/Dockerfile.ubuntu.arm64 index f195d70045..e3f1032a82 100644 --- a/deployment/Dockerfile.ubuntu.arm64 +++ b/deployment/Dockerfile.ubuntu.arm64 @@ -16,7 +16,7 @@ RUN apt-get update -yqq \ mmv build-essential lsb-release # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ebfd0bf8-79bd-480a-9e81-0b217463738d/9adc6bf0614ce02670101e278a2d8555/dotnet-sdk-7.0.203-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/9c86d7b4-acb2-4be4-8a89-d13bc3c3f28f/1d044c7c29df018e8f2837bb343e8a84/dotnet-sdk-7.0.304-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.armhf b/deployment/Dockerfile.ubuntu.armhf index 0fb59d5ca1..aebcaeccff 100644 --- a/deployment/Dockerfile.ubuntu.armhf +++ b/deployment/Dockerfile.ubuntu.armhf @@ -16,7 +16,7 @@ RUN apt-get update -yqq \ mmv build-essential lsb-release # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ebfd0bf8-79bd-480a-9e81-0b217463738d/9adc6bf0614ce02670101e278a2d8555/dotnet-sdk-7.0.203-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/9c86d7b4-acb2-4be4-8a89-d13bc3c3f28f/1d044c7c29df018e8f2837bb343e8a84/dotnet-sdk-7.0.304-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet From db15c590e3ec4eade5dc12adb49bf27ab435e1f3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 18 Jun 2023 19:23:06 -0600 Subject: [PATCH 357/858] chore(deps): update github/codeql-action action to v2.20.0 (#9887) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 9f1be02327..4173e21046 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@83f0fe6c4988d98a455712a27f0255212bba9bd4 # v2.3.6 + uses: github/codeql-action/init@6c089f53dd51dc3fc7e599c3cb5356453a52ca9e # v2.20.0 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@83f0fe6c4988d98a455712a27f0255212bba9bd4 # v2.3.6 + uses: github/codeql-action/autobuild@6c089f53dd51dc3fc7e599c3cb5356453a52ca9e # v2.20.0 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@83f0fe6c4988d98a455712a27f0255212bba9bd4 # v2.3.6 + uses: github/codeql-action/analyze@6c089f53dd51dc3fc7e599c3cb5356453a52ca9e # v2.20.0 From dc91d34c6077810295a9e098b8f937418e5ae496 Mon Sep 17 00:00:00 2001 From: Cody Robibero <cody@robibe.ro> Date: Mon, 19 Jun 2023 07:56:15 -0600 Subject: [PATCH 358/858] Use intermediate env --- .github/workflows/openapi.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index ad1cedd52e..f00bb9e9bd 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -45,10 +45,12 @@ jobs: repository: ${{ github.event.pull_request.head.repo.full_name }} fetch-depth: 0 - name: Checkout common ancestor + env: + HEAD_REF: ${{ github.head_ref }} run: | git remote add upstream https://github.com/${{ github.event.pull_request.base.repo.full_name }} git -c protocol.version=2 fetch --prune --progress --no-recurse-submodules upstream +refs/heads/*:refs/remotes/upstream/* +refs/tags/*:refs/tags/* - ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/${{ github.head_ref }}) + ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/${{ $HEAD_REF }}) git checkout --progress --force $ANCESTOR_REF - name: Setup .NET uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0 From 1952a915e65a87fa92b3e92c113cc1ab7b845b3e Mon Sep 17 00:00:00 2001 From: Frank Riley <fhriley@gmail.com> Date: Thu, 15 Jun 2023 18:48:52 -0700 Subject: [PATCH 359/858] Move hardcoded LibraryUpdateDuration to ServerConfiguration. Fixes #9893. Signed-off-by: Frank Riley <fhriley@gmail.com> --- .../EntryPoints/LibraryChangedNotifier.cs | 23 +++++++++---------- .../Configuration/ServerConfiguration.cs | 6 +++++ 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs index 2e3988f9eb..be36bbd2c1 100644 --- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs @@ -12,6 +12,7 @@ using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Events; using MediaBrowser.Controller.Channels; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; @@ -26,12 +27,8 @@ namespace Emby.Server.Implementations.EntryPoints { public class LibraryChangedNotifier : IServerEntryPoint { - /// <summary> - /// The library update duration. - /// </summary> - private const int LibraryUpdateDuration = 30000; - private readonly ILibraryManager _libraryManager; + private readonly IServerConfigurationManager _configurationManager; private readonly IProviderManager _providerManager; private readonly ISessionManager _sessionManager; private readonly IUserManager _userManager; @@ -51,12 +48,14 @@ namespace Emby.Server.Implementations.EntryPoints public LibraryChangedNotifier( ILibraryManager libraryManager, + IServerConfigurationManager configurationManager, ISessionManager sessionManager, IUserManager userManager, ILogger<LibraryChangedNotifier> logger, IProviderManager providerManager) { _libraryManager = libraryManager; + _configurationManager = configurationManager; _sessionManager = sessionManager; _userManager = userManager; _logger = logger; @@ -196,12 +195,12 @@ namespace Emby.Server.Implementations.EntryPoints LibraryUpdateTimer = new Timer( LibraryUpdateTimerCallback, null, - LibraryUpdateDuration, - Timeout.Infinite); + TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), + Timeout.InfiniteTimeSpan); } else { - LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite); + LibraryUpdateTimer.Change(TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan); } if (e.Item.GetParent() is Folder parent) @@ -229,11 +228,11 @@ namespace Emby.Server.Implementations.EntryPoints { if (LibraryUpdateTimer is null) { - LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration, Timeout.Infinite); + LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan); } else { - LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite); + LibraryUpdateTimer.Change(TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan); } _itemsUpdated.Add(e.Item); @@ -256,11 +255,11 @@ namespace Emby.Server.Implementations.EntryPoints { if (LibraryUpdateTimer is null) { - LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration, Timeout.Infinite); + LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan); } else { - LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite); + LibraryUpdateTimer.Change(TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan); } if (e.Parent is Folder parent) diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 07f02d1879..8af782b3d5 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -165,6 +165,12 @@ namespace MediaBrowser.Model.Configuration /// <value>The file watcher delay.</value> public int LibraryMonitorDelay { get; set; } = 60; + /// <summary> + /// Gets or sets the duration in seconds that we will wait after a library updated event before executing the library changed notification. + /// </summary> + /// <value>The library update duration.</value> + public int LibraryUpdateDuration { get; set; } = 30; + /// <summary> /// Gets or sets the image saving convention. /// </summary> From 30f6c0a397df25a95b31de517afb3ae89400e84e Mon Sep 17 00:00:00 2001 From: Cody Robibero <cody@robibe.ro> Date: Mon, 19 Jun 2023 08:51:01 -0600 Subject: [PATCH 360/858] Update .github/workflows/openapi.yml Co-authored-by: Jorge <46056498+jorgectf@users.noreply.github.com> --- .github/workflows/openapi.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index f00bb9e9bd..d3dfd0a6aa 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -50,7 +50,7 @@ jobs: run: | git remote add upstream https://github.com/${{ github.event.pull_request.base.repo.full_name }} git -c protocol.version=2 fetch --prune --progress --no-recurse-submodules upstream +refs/heads/*:refs/remotes/upstream/* +refs/tags/*:refs/tags/* - ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/${{ $HEAD_REF }}) + ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/$HEAD_REF) git checkout --progress --force $ANCESTOR_REF - name: Setup .NET uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0 From 0df6fd9cf28ddec98e0418ca08e8b42046ff677f Mon Sep 17 00:00:00 2001 From: nyanmisaka <nst799610810@gmail.com> Date: Mon, 19 Jun 2023 22:50:09 +0800 Subject: [PATCH 361/858] Add AV1 support in HLS streaming Signed-off-by: nyanmisaka <nst799610810@gmail.com> --- Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 42 ++++++++++++-- Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs | 56 +++++++++++++++++++ Jellyfin.Api/Helpers/StreamingHelpers.cs | 9 ++- MediaBrowser.Model/Dlna/StreamBuilder.cs | 2 +- 4 files changed, 100 insertions(+), 9 deletions(-) diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index 4486954c62..b66aa69350 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -210,6 +210,7 @@ public class DynamicHlsHelper // Provide SDR HEVC entrance for backward compatibility. if (encodingOptions.AllowHevcEncoding + && !encodingOptions.AllowAv1Encoding && EncodingHelper.IsCopyCodec(state.OutputVideoCodec) && !string.IsNullOrEmpty(state.VideoStream.VideoRange) && string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase) @@ -252,7 +253,9 @@ public class DynamicHlsHelper // Provide Level 5.0 entrance for backward compatibility. // e.g. Apple A10 chips refuse the master playlist containing SDR HEVC Main Level 5.1 video, // but in fact it is capable of playing videos up to Level 6.1. - if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec) + if (encodingOptions.AllowHevcEncoding + && !encodingOptions.AllowAv1Encoding + && EncodingHelper.IsCopyCodec(state.OutputVideoCodec) && state.VideoStream.Level.HasValue && state.VideoStream.Level > 150 && !string.IsNullOrEmpty(state.VideoStream.VideoRange) @@ -555,6 +558,12 @@ public class DynamicHlsHelper levelString = state.GetRequestedLevel("h265") ?? state.GetRequestedLevel("hevc") ?? "120"; levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString); } + + if (string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase)) + { + levelString = state.GetRequestedLevel("av1") ?? "19"; + levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString); + } } if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel)) @@ -566,11 +575,11 @@ public class DynamicHlsHelper } /// <summary> - /// Get the H.26X profile of the output video stream. + /// Get the profile of the output video stream. /// </summary> /// <param name="state">StreamState of the current stream.</param> /// <param name="codec">Video codec.</param> - /// <returns>H.26X profile of the output video stream.</returns> + /// <returns>Profile of the output video stream.</returns> private string GetOutputVideoCodecProfile(StreamState state, string codec) { string profileString = string.Empty; @@ -592,6 +601,11 @@ public class DynamicHlsHelper { profileString ??= "main"; } + + if (string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase)) + { + profileString ??= "main"; + } } return profileString; @@ -658,9 +672,9 @@ public class DynamicHlsHelper { if (level == 0) { - // This is 0 when there's no requested H.26X level in the device profile - // and the source is not encoded in H.26X - _logger.LogError("Got invalid H.26X level when building CODECS field for HLS master playlist"); + // This is 0 when there's no requested level in the device profile + // and the source is not encoded in H.26X or AV1 + _logger.LogError("Got invalid level when building CODECS field for HLS master playlist"); return string.Empty; } @@ -677,6 +691,22 @@ public class DynamicHlsHelper return HlsCodecStringHelpers.GetH265String(profile, level); } + if (string.Equals(codec, "av1", StringComparison.OrdinalIgnoreCase)) + { + string profile = GetOutputVideoCodecProfile(state, "av1"); + + // Currently we only transcode to 8 bits AV1 + int bitDepth = 8; + if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec) + && state.VideoStream != null + && state.VideoStream.BitDepth.HasValue) + { + bitDepth = state.VideoStream.BitDepth.Value; + } + + return HlsCodecStringHelpers.GetAv1String(profile, level, false, bitDepth); + } + return string.Empty; } diff --git a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs index 995488397e..ad1fce0f14 100644 --- a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs +++ b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs @@ -179,4 +179,60 @@ public static class HlsCodecStringHelpers return result.ToString(); } + + /// <summary> + /// Gets a AV1 codec string. + /// </summary> + /// <param name="profile">AV1 profile.</param> + /// <param name="level">AV1 level.</param> + /// <param name="tierFlag">AV1 tier flag.</param> + /// <param name="bitDepth">AV1 bit depth.</param> + /// <returns>AV1 string.</returns> + public static string GetAv1String(string? profile, int level, bool tierFlag, int bitDepth) + { + // https://aomedia.org/av1/specification/annex-a/ + // FORMAT: [codecTag].[profile].[level][tier].[bitDepth] + StringBuilder result = new StringBuilder("av01", 13); + + if (string.Equals(profile, "Main", StringComparison.OrdinalIgnoreCase)) + { + result.Append(".0"); + } + else if (string.Equals(profile, "High", StringComparison.OrdinalIgnoreCase)) + { + result.Append(".1"); + } + else if (string.Equals(profile, "Professional", StringComparison.OrdinalIgnoreCase)) + { + result.Append(".2"); + } + else + { + // Default to Main + result.Append(".0"); + } + + if (level <= 0 + || level > 31) + { + // Default to the maximum defined level 6.3 + level = 19; + } + + if (bitDepth != 8 + && bitDepth != 10 + && bitDepth != 12) + { + // Default to 8 bits + bitDepth = 8; + } + + result.Append("." + level) + .Append(tierFlag ? "H" : "M"); + + string bitDepthD2 = bitDepth.ToString("D2", CultureInfo.InvariantCulture); + result.Append("." + bitDepthD2); + + return result.ToString(); + } } diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index 9c91dcc6fe..782cd65685 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -430,12 +430,17 @@ public static class StreamingHelpers { var videoCodec = state.Request.VideoCodec; - if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase) || - string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase)) { return ".ts"; } + if (string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoCodec, "av1", StringComparison.OrdinalIgnoreCase)) + { + return ".mp4"; + } + if (string.Equals(videoCodec, "theora", StringComparison.OrdinalIgnoreCase)) { return ".ogv"; diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 0a955e917f..b82c9cf26b 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -23,7 +23,7 @@ namespace MediaBrowser.Model.Dlna private readonly ILogger _logger; private readonly ITranscoderSupport _transcoderSupport; - private static readonly string[] _supportedHlsVideoCodecs = new string[] { "h264", "hevc" }; + private static readonly string[] _supportedHlsVideoCodecs = new string[] { "h264", "hevc", "av1" }; private static readonly string[] _supportedHlsAudioCodecsTs = new string[] { "aac", "ac3", "eac3", "mp3" }; private static readonly string[] _supportedHlsAudioCodecsMp4 = new string[] { "aac", "ac3", "eac3", "mp3", "alac", "flac", "opus", "dca", "truehd" }; From be01aeecd9d211243c6d67935c593aeb93219260 Mon Sep 17 00:00:00 2001 From: nyanmisaka <nst799610810@gmail.com> Date: Tue, 20 Jun 2023 03:49:26 +0800 Subject: [PATCH 362/858] Add AV1 hardware and software encoding Signed-off-by: nyanmisaka <nst799610810@gmail.com> --- .../MediaEncoding/EncodingHelper.cs | 191 +++++++++++++++--- .../Encoder/EncoderValidator.cs | 5 + .../Configuration/EncodingOptions.cs | 6 + 3 files changed, 173 insertions(+), 29 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index b155d674de..d5049ca6bd 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -46,6 +46,7 @@ namespace MediaBrowser.Controller.MediaEncoding private readonly Version _minFFmpegImplictHwaccel = new Version(6, 0); private readonly Version _minFFmpegHwaUnsafeOutput = new Version(6, 0); private readonly Version _minFFmpegOclCuTonemapMode = new Version(5, 1, 3); + private readonly Version _minFFmpegSvtAv1Params = new Version(5, 1); private static readonly string[] _videoProfilesH264 = new[] { @@ -65,6 +66,13 @@ namespace MediaBrowser.Controller.MediaEncoding "Main10" }; + private static readonly string[] _videoProfilesAv1 = new[] + { + "Main", + "High", + "Professional", + }; + private static readonly HashSet<string> _mp4ContainerNames = new(StringComparer.OrdinalIgnoreCase) { "mp4", @@ -113,12 +121,15 @@ namespace MediaBrowser.Controller.MediaEncoding } public string GetH264Encoder(EncodingJobInfo state, EncodingOptions encodingOptions) - => GetH264OrH265Encoder("libx264", "h264", state, encodingOptions); + => GetH26xOrAv1Encoder("libx264", "h264", state, encodingOptions); public string GetH265Encoder(EncodingJobInfo state, EncodingOptions encodingOptions) - => GetH264OrH265Encoder("libx265", "hevc", state, encodingOptions); + => GetH26xOrAv1Encoder("libx265", "hevc", state, encodingOptions); - private string GetH264OrH265Encoder(string defaultEncoder, string hwEncoder, EncodingJobInfo state, EncodingOptions encodingOptions) + public string GetAv1Encoder(EncodingJobInfo state, EncodingOptions encodingOptions) + => GetH26xOrAv1Encoder("libsvtav1", "av1", state, encodingOptions); + + private string GetH26xOrAv1Encoder(string defaultEncoder, string hwEncoder, EncodingJobInfo state, EncodingOptions encodingOptions) { // Only use alternative encoders for video files. // When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully @@ -266,6 +277,11 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(codec)) { + if (string.Equals(codec, "av1", StringComparison.OrdinalIgnoreCase)) + { + return GetAv1Encoder(state, encodingOptions); + } + if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)) { @@ -565,6 +581,11 @@ namespace MediaBrowser.Controller.MediaEncoding return Array.FindIndex(_videoProfilesH265, x => string.Equals(x, profile, StringComparison.OrdinalIgnoreCase)); } + if (string.Equals("av1", videoCodec, StringComparison.OrdinalIgnoreCase)) + { + return Array.FindIndex(_videoProfilesAv1, x => string.Equals(x, profile, StringComparison.OrdinalIgnoreCase)); + } + return -1; } @@ -1204,6 +1225,11 @@ namespace MediaBrowser.Controller.MediaEncoding return FormattableString.Invariant($" -b:v {bitrate}"); } + if (string.Equals(videoCodec, "libsvtav1", StringComparison.OrdinalIgnoreCase)) + { + return FormattableString.Invariant($" -b:v {bitrate} -bufsize {bufsize}"); + } + if (string.Equals(videoCodec, "libx264", StringComparison.OrdinalIgnoreCase) || string.Equals(videoCodec, "libx265", StringComparison.OrdinalIgnoreCase)) { @@ -1211,14 +1237,16 @@ namespace MediaBrowser.Controller.MediaEncoding } if (string.Equals(videoCodec, "h264_amf", StringComparison.OrdinalIgnoreCase) - || string.Equals(videoCodec, "hevc_amf", StringComparison.OrdinalIgnoreCase)) + || string.Equals(videoCodec, "hevc_amf", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoCodec, "av1_amf", StringComparison.OrdinalIgnoreCase)) { // Override the too high default qmin 18 in transcoding preset return FormattableString.Invariant($" -rc cbr -qmin 0 -qmax 32 -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}"); } if (string.Equals(videoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase) - || string.Equals(videoCodec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)) + || string.Equals(videoCodec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoCodec, "av1_vaapi", StringComparison.OrdinalIgnoreCase)) { // VBR in i965 driver may result in pixelated output. if (_mediaEncoder.IsVaapiDeviceInteli965) @@ -1236,14 +1264,23 @@ namespace MediaBrowser.Controller.MediaEncoding { if (double.TryParse(level, CultureInfo.InvariantCulture, out double requestLevel)) { - if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase) - || string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase)) + { + // Transcode to level 5.3 (15) and lower for maximum compatibility. + // https://en.wikipedia.org/wiki/AV1#Levels + if (requestLevel < 0 || requestLevel >= 15) + { + return "15"; + } + } + else if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase) + || string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)) { // Transcode to level 5.0 and lower for maximum compatibility. // Level 5.0 is suitable for up to 4k 30fps hevc encoding, otherwise let the encoder to handle it. // https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding_tiers_and_levels // MaxLumaSampleRate = 3840*2160*30 = 248832000 < 267386880. - if (requestLevel >= 150) + if (requestLevel < 0 || requestLevel >= 150) { return "150"; } @@ -1253,7 +1290,7 @@ namespace MediaBrowser.Controller.MediaEncoding // Transcode to level 5.1 and lower for maximum compatibility. // h264 4k 30fps requires at least level 5.1 otherwise it will break on safari fmp4. // https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels - if (requestLevel >= 51) + if (requestLevel < 0 || requestLevel >= 51) { return "51"; } @@ -1391,14 +1428,18 @@ namespace MediaBrowser.Controller.MediaEncoding || string.Equals(codec, "h264_amf", StringComparison.OrdinalIgnoreCase) || string.Equals(codec, "hevc_qsv", StringComparison.OrdinalIgnoreCase) || string.Equals(codec, "hevc_nvenc", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "hevc_amf", StringComparison.OrdinalIgnoreCase)) + || string.Equals(codec, "av1_qsv", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "av1_nvenc", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "av1_amf", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "libsvtav1", StringComparison.OrdinalIgnoreCase)) { args += gopArg; } else if (string.Equals(codec, "libx264", StringComparison.OrdinalIgnoreCase) || string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase) || string.Equals(codec, "h264_vaapi", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)) + || string.Equals(codec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "av1_vaapi", StringComparison.OrdinalIgnoreCase)) { args += keyFrameArg; @@ -1534,18 +1575,60 @@ namespace MediaBrowser.Controller.MediaEncoding param += " -crf " + defaultCrf; } } - else if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) // h264 (h264_qsv) - || string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_qsv) + else if (string.Equals(videoEncoder, "libsvtav1", StringComparison.OrdinalIgnoreCase)) { - string[] valid_h264_qsv = { "veryslow", "slower", "slow", "medium", "fast", "faster", "veryfast" }; + // Default to use the recommended preset 10. + // Omit presets < 5, which are too slow for on the fly encoding. + // https://gitlab.com/AOMediaCodec/SVT-AV1/-/blob/master/Docs/Ffmpeg.md + param += encodingOptions.EncoderPreset switch + { + "veryslow" => " -preset 5", + "slower" => " -preset 6", + "slow" => " -preset 7", + "medium" => " -preset 8", + "fast" => " -preset 9", + "faster" => " -preset 10", + "veryfast" => " -preset 11", + "superfast" => " -preset 12", + "ultrafast" => " -preset 13", + _ => " -preset 10" + }; + } + else if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "av1_vaapi", StringComparison.OrdinalIgnoreCase)) + { + // -compression_level is not reliable on AMD. + if (_mediaEncoder.IsVaapiDeviceInteliHD) + { + param += encodingOptions.EncoderPreset switch + { + "veryslow" => " -compression_level 1", + "slower" => " -compression_level 2", + "slow" => " -compression_level 3", + "medium" => " -compression_level 4", + "fast" => " -compression_level 5", + "faster" => " -compression_level 6", + "veryfast" => " -compression_level 7", + "superfast" => " -compression_level 7", + "ultrafast" => " -compression_level 7", + _ => string.Empty + }; + } + } + else if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) // h264 (h264_qsv) + || string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase) // hevc (hevc_qsv) + || string.Equals(videoEncoder, "av1_qsv", StringComparison.OrdinalIgnoreCase)) // av1 (av1_qsv) + { + string[] valid_presets = { "veryslow", "slower", "slow", "medium", "fast", "faster", "veryfast" }; - if (valid_h264_qsv.Contains(encodingOptions.EncoderPreset, StringComparison.OrdinalIgnoreCase)) + if (valid_presets.Contains(encodingOptions.EncoderPreset, StringComparison.OrdinalIgnoreCase)) { param += " -preset " + encodingOptions.EncoderPreset; } else { - param += " -preset 7"; + param += " -preset veryfast"; } // Only h264_qsv has look_ahead option @@ -1555,7 +1638,8 @@ namespace MediaBrowser.Controller.MediaEncoding } } else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) // h264 (h264_nvenc) - || string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_nvenc) + || string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase) // hevc (hevc_nvenc) + || string.Equals(videoEncoder, "av1_nvenc", StringComparison.OrdinalIgnoreCase)) // av1 (av1_nvenc) { switch (encodingOptions.EncoderPreset) { @@ -1595,7 +1679,8 @@ namespace MediaBrowser.Controller.MediaEncoding } } else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) // h264 (h264_amf) - || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_amf) + || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase) // hevc (hevc_amf) + || string.Equals(videoEncoder, "av1_amf", StringComparison.OrdinalIgnoreCase)) // av1 (av1_amf) { switch (encodingOptions.EncoderPreset) { @@ -1622,9 +1707,15 @@ namespace MediaBrowser.Controller.MediaEncoding break; } + if (string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "av1_amf", StringComparison.OrdinalIgnoreCase)) + { + param += " -header_insertion_mode gop"; + } + if (string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)) { - param += " -header_insertion_mode gop -gops_per_idr 1"; + param += " -gops_per_idr 1"; } } else if (string.Equals(videoEncoder, "libvpx", StringComparison.OrdinalIgnoreCase)) // vp8 @@ -1755,6 +1846,14 @@ namespace MediaBrowser.Controller.MediaEncoding profile = "high"; } + // We only need Main profile of AV1 encoders. + if (videoEncoder.Contains("av1", StringComparison.OrdinalIgnoreCase) + && (profile.Contains("high", StringComparison.OrdinalIgnoreCase) + || profile.Contains("professional", StringComparison.OrdinalIgnoreCase))) + { + profile = "main"; + } + // h264_vaapi does not support Baseline profile, force Constrained Baseline in this case, // which is compatible (and ugly). if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) @@ -1822,19 +1921,41 @@ namespace MediaBrowser.Controller.MediaEncoding param += " -level " + (hevcLevel / 3); } } + else if (string.Equals(videoEncoder, "av1_qsv", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "libsvtav1", StringComparison.OrdinalIgnoreCase)) + { + // libsvtav1 and av1_qsv use -level 60 instead of -level 16 + // https://aomedia.org/av1/specification/annex-a/ + if (int.TryParse(level, NumberStyles.Any, CultureInfo.InvariantCulture, out int av1Level)) + { + var x = 2 + (av1Level >> 2); + var y = av1Level & 3; + var res = (x * 10) + y; + param += " -level " + res; + } + } else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) - || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)) + || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "av1_amf", StringComparison.OrdinalIgnoreCase)) { param += " -level " + level; } else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) || string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase) - || string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) - || string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)) + || string.Equals(videoEncoder, "av1_nvenc", StringComparison.OrdinalIgnoreCase)) { // level option may cause NVENC to fail. // NVENC cannot adjust the given level, just throw an error. + } + else if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "av1_vaapi", StringComparison.OrdinalIgnoreCase)) + { // level option may cause corrupted frames on AMD VAAPI. + if (_mediaEncoder.IsVaapiDeviceInteliHD || _mediaEncoder.IsVaapiDeviceInteli965) + { + param += " -level " + level; + } } else if (!string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase)) { @@ -1856,6 +1977,12 @@ namespace MediaBrowser.Controller.MediaEncoding param += " -x265-params:0 no-info=1"; } + if (string.Equals(videoEncoder, "libsvtav1", StringComparison.OrdinalIgnoreCase) + && _mediaEncoder.EncoderVersion >= _minFFmpegSvtAv1Params) + { + param += " -svtav1-params:0 rc=1:tune=0:film-grain=0:enable-overlays=1:enable-tf=0"; + } + return param; } @@ -5810,19 +5937,25 @@ namespace MediaBrowser.Controller.MediaEncoding private void ShiftVideoCodecsIfNeeded(List<string> videoCodecs, EncodingOptions encodingOptions) { - // Shift hevc/h265 to the end of list if hevc encoding is not allowed. - if (encodingOptions.AllowHevcEncoding) - { - return; - } - // No need to shift if there is only one supported video codec. if (videoCodecs.Count < 2) { return; } - var shiftVideoCodecs = new[] { "hevc", "h265" }; + // Shift codecs to the end of list if it's not allowed. + var shiftVideoCodecs = new List<string>(); + if (!encodingOptions.AllowHevcEncoding) + { + shiftVideoCodecs.Add("hevc"); + shiftVideoCodecs.Add("h265"); + } + + if (!encodingOptions.AllowAv1Encoding) + { + shiftVideoCodecs.Add("av1"); + } + if (videoCodecs.All(i => shiftVideoCodecs.Contains(i, StringComparison.OrdinalIgnoreCase))) { return; diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index d3843796f7..e1a0e8d670 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -52,6 +52,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { "libx264", "libx265", + "libsvtav1", "mpeg4", "msmpeg4", "libvpx", @@ -69,12 +70,16 @@ namespace MediaBrowser.MediaEncoding.Encoder "srt", "h264_amf", "hevc_amf", + "av1_amf", "h264_qsv", "hevc_qsv", + "av1_qsv", "h264_nvenc", "hevc_nvenc", + "av1_nvenc", "h264_vaapi", "hevc_vaapi", + "av1_vaapi", "h264_v4l2m2m", "h264_videotoolbox", "hevc_videotoolbox" diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index a53be0fee7..3f0e98ec8e 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -49,6 +49,7 @@ public class EncodingOptions EnableIntelLowPowerHevcHwEncoder = false; EnableHardwareEncoding = true; AllowHevcEncoding = false; + AllowAv1Encoding = false; EnableSubtitleExtraction = true; AllowOnDemandMetadataBasedKeyframeExtractionForExtensions = new[] { "mkv" }; HardwareDecodingCodecs = new string[] { "h264", "vc1" }; @@ -249,6 +250,11 @@ public class EncodingOptions /// </summary> public bool AllowHevcEncoding { get; set; } + /// <summary> + /// Gets or sets a value indicating whether AV1 encoding is enabled. + /// </summary> + public bool AllowAv1Encoding { get; set; } + /// <summary> /// Gets or sets a value indicating whether subtitle extraction is enabled. /// </summary> From 27d0d8a7f206c9bb92dd1fda10d7d7cfcb0162a3 Mon Sep 17 00:00:00 2001 From: nyanmisaka <nst799610810@gmail.com> Date: Mon, 19 Jun 2023 23:01:15 +0800 Subject: [PATCH 363/858] Refine SwDec and QSV encoding Signed-off-by: nyanmisaka <nst799610810@gmail.com> --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index d5049ca6bd..cb40579f91 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -3772,7 +3772,7 @@ namespace MediaBrowser.Controller.MediaEncoding mainFilters.Add(swDeintFilter); } - var outFormat = doOclTonemap ? "yuv420p10le" : "yuv420p"; + var outFormat = doOclTonemap ? "yuv420p10le" : (hasGraphicalSubs ? "yuv420p" : "nv12"); var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH); // sw scale mainFilters.Add(swScaleFilter); @@ -3973,7 +3973,7 @@ namespace MediaBrowser.Controller.MediaEncoding mainFilters.Add(swDeintFilter); } - var outFormat = doOclTonemap ? "yuv420p10le" : "yuv420p"; + var outFormat = doOclTonemap ? "yuv420p10le" : (hasGraphicalSubs ? "yuv420p" : "nv12"); var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH); // sw scale mainFilters.Add(swScaleFilter); From 990bcc507ff9902b2bac7527f201194991a58f98 Mon Sep 17 00:00:00 2001 From: nyanmisaka <nst799610810@gmail.com> Date: Mon, 19 Jun 2023 23:01:36 +0800 Subject: [PATCH 364/858] Remove unused lines Signed-off-by: nyanmisaka <nst799610810@gmail.com> --- .../Dlna/ResolutionNormalizer.cs | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs b/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs index ce422a2288..5d7daa81aa 100644 --- a/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs +++ b/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs @@ -73,27 +73,5 @@ namespace MediaBrowser.Model.Dlna return null; } - - private static double GetVideoBitrateScaleFactor(string codec) - { - if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase)) - { - return .6; - } - - return 1; - } - - public static int ScaleBitrate(int bitrate, string inputVideoCodec, string outputVideoCodec) - { - var inputScaleFactor = GetVideoBitrateScaleFactor(inputVideoCodec); - var outputScaleFactor = GetVideoBitrateScaleFactor(outputVideoCodec); - var scaleFactor = outputScaleFactor / inputScaleFactor; - var newBitrate = scaleFactor * bitrate; - - return Convert.ToInt32(newBitrate); - } } } From 3b12dc6d7ae0984aa96929b89dfbeb73f55568d9 Mon Sep 17 00:00:00 2001 From: Nyanmisaka <nst799610810@gmail.com> Date: Tue, 20 Jun 2023 04:18:55 +0800 Subject: [PATCH 365/858] Apply suggestions from code review Co-authored-by: Cody Robibero <cody@robibe.ro> --- Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs index ad1fce0f14..adb2486709 100644 --- a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs +++ b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs @@ -227,11 +227,13 @@ public static class HlsCodecStringHelpers bitDepth = 8; } - result.Append("." + level) - .Append(tierFlag ? "H" : "M"); + result.Append('.') + .Append(level) + .Append(tierFlag ? 'H' : 'M'); string bitDepthD2 = bitDepth.ToString("D2", CultureInfo.InvariantCulture); - result.Append("." + bitDepthD2); + result.Append('.') + .Append(bitDepthD2); return result.ToString(); } From f8d7f4acdb486c8bc2f2f6a3ced604b0e5f25f16 Mon Sep 17 00:00:00 2001 From: Nyanmisaka <nst799610810@gmail.com> Date: Tue, 20 Jun 2023 05:09:22 +0800 Subject: [PATCH 366/858] Apply suggestions from code review Co-authored-by: Shadowghost <Shadowghost@users.noreply.github.com> --- Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs index adb2486709..9a141a16d9 100644 --- a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs +++ b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs @@ -181,13 +181,13 @@ public static class HlsCodecStringHelpers } /// <summary> - /// Gets a AV1 codec string. + /// Gets an AV1 codec string. /// </summary> /// <param name="profile">AV1 profile.</param> /// <param name="level">AV1 level.</param> /// <param name="tierFlag">AV1 tier flag.</param> /// <param name="bitDepth">AV1 bit depth.</param> - /// <returns>AV1 string.</returns> + /// <returns>The AV1 codec string.</returns> public static string GetAv1String(string? profile, int level, bool tierFlag, int bitDepth) { // https://aomedia.org/av1/specification/annex-a/ From f04cfd6ef480f0a31ecc0ad995779ba24408c5f7 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Tue, 20 Jun 2023 18:06:30 +0200 Subject: [PATCH 367/858] Don't ignore parentId for playlists --- Jellyfin.Api/Controllers/ItemsController.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 7650b861f4..80128536da 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -256,8 +256,7 @@ public class ItemsController : BaseJellyfinApiController .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); if (includeItemTypes.Length == 1 - && (includeItemTypes[0] == BaseItemKind.Playlist - || includeItemTypes[0] == BaseItemKind.BoxSet)) + && includeItemTypes[0] == BaseItemKind.BoxSet) { parentId = null; } From 5c9f880ae55e81b7629b41822800f1b3ba9d169c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 21 Jun 2023 13:13:34 +0000 Subject: [PATCH 368/858] chore(deps): update github/codeql-action action to v2.20.1 --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 4173e21046..f83b38949c 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@6c089f53dd51dc3fc7e599c3cb5356453a52ca9e # v2.20.0 + uses: github/codeql-action/init@f6e388ebf0efc915c6c5b165b019ee61a6746a38 # v2.20.1 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@6c089f53dd51dc3fc7e599c3cb5356453a52ca9e # v2.20.0 + uses: github/codeql-action/autobuild@f6e388ebf0efc915c6c5b165b019ee61a6746a38 # v2.20.1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@6c089f53dd51dc3fc7e599c3cb5356453a52ca9e # v2.20.0 + uses: github/codeql-action/analyze@f6e388ebf0efc915c6c5b165b019ee61a6746a38 # v2.20.1 From b84eedd0b9c0df9fc3f0ca4a9a295dd42b72a25b Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Thu, 22 Jun 2023 00:39:32 +0200 Subject: [PATCH 369/858] Update stylecop.analyzers to v1.2.0-beta.507 --- Directory.Packages.props | 2 +- Jellyfin.Api/Models/LiveTvDtos/ChannelMappingOptionsDto.cs | 4 ++-- Jellyfin.Api/Models/UserDtos/CreateUserByName.cs | 2 +- Jellyfin.Api/Models/UserDtos/ForgotPasswordDto.cs | 2 +- Jellyfin.Api/Models/UserDtos/ForgotPasswordPinDto.cs | 2 +- MediaBrowser.Model/Dlna/MediaOptions.cs | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 002e0403d0..28059fe845 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -71,7 +71,7 @@ <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> <PackageVersion Include="SQLitePCL.pretty.netstandard" Version="3.1.0" /> <PackageVersion Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.5" /> - <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.435" /> + <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.507" /> <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.4.0" /> <PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" /> <PackageVersion Include="System.Globalization" Version="4.3.0" /> diff --git a/Jellyfin.Api/Models/LiveTvDtos/ChannelMappingOptionsDto.cs b/Jellyfin.Api/Models/LiveTvDtos/ChannelMappingOptionsDto.cs index 75222ed01a..cbc3548b10 100644 --- a/Jellyfin.Api/Models/LiveTvDtos/ChannelMappingOptionsDto.cs +++ b/Jellyfin.Api/Models/LiveTvDtos/ChannelMappingOptionsDto.cs @@ -13,12 +13,12 @@ public class ChannelMappingOptionsDto /// <summary> /// Gets or sets list of tuner channels. /// </summary> - required public IReadOnlyList<TunerChannelMapping> TunerChannels { get; set; } + public required IReadOnlyList<TunerChannelMapping> TunerChannels { get; set; } /// <summary> /// Gets or sets list of provider channels. /// </summary> - required public IReadOnlyList<NameIdPair> ProviderChannels { get; set; } + public required IReadOnlyList<NameIdPair> ProviderChannels { get; set; } /// <summary> /// Gets or sets list of mappings. diff --git a/Jellyfin.Api/Models/UserDtos/CreateUserByName.cs b/Jellyfin.Api/Models/UserDtos/CreateUserByName.cs index 6b6d9682ba..4f9fc4e78b 100644 --- a/Jellyfin.Api/Models/UserDtos/CreateUserByName.cs +++ b/Jellyfin.Api/Models/UserDtos/CreateUserByName.cs @@ -11,7 +11,7 @@ public class CreateUserByName /// Gets or sets the username. /// </summary> [Required] - required public string Name { get; set; } + public required string Name { get; set; } /// <summary> /// Gets or sets the password. diff --git a/Jellyfin.Api/Models/UserDtos/ForgotPasswordDto.cs b/Jellyfin.Api/Models/UserDtos/ForgotPasswordDto.cs index a0631fd07b..8ea51af2b9 100644 --- a/Jellyfin.Api/Models/UserDtos/ForgotPasswordDto.cs +++ b/Jellyfin.Api/Models/UserDtos/ForgotPasswordDto.cs @@ -11,5 +11,5 @@ public class ForgotPasswordDto /// Gets or sets the entered username to have its password reset. /// </summary> [Required] - required public string EnteredUsername { get; set; } + public required string EnteredUsername { get; set; } } diff --git a/Jellyfin.Api/Models/UserDtos/ForgotPasswordPinDto.cs b/Jellyfin.Api/Models/UserDtos/ForgotPasswordPinDto.cs index 79b8a5d63f..91b5520ee2 100644 --- a/Jellyfin.Api/Models/UserDtos/ForgotPasswordPinDto.cs +++ b/Jellyfin.Api/Models/UserDtos/ForgotPasswordPinDto.cs @@ -11,5 +11,5 @@ public class ForgotPasswordPinDto /// Gets or sets the entered pin to have the password reset. /// </summary> [Required] - required public string Pin { get; set; } + public required string Pin { get; set; } } diff --git a/MediaBrowser.Model/Dlna/MediaOptions.cs b/MediaBrowser.Model/Dlna/MediaOptions.cs index 7ec0dd473b..eca971e95e 100644 --- a/MediaBrowser.Model/Dlna/MediaOptions.cs +++ b/MediaBrowser.Model/Dlna/MediaOptions.cs @@ -62,7 +62,7 @@ namespace MediaBrowser.Model.Dlna /// <summary> /// Gets or sets the device profile. /// </summary> - required public DeviceProfile Profile { get; set; } + public required DeviceProfile Profile { get; set; } /// <summary> /// Gets or sets a media source id. Optional. Only needed if a specific AudioStreamIndex or SubtitleStreamIndex are requested. From 3982b0e057cc692875ba35573197b86abf8d7adb Mon Sep 17 00:00:00 2001 From: Bond-009 <bond.009@outlook.com> Date: Thu, 22 Jun 2023 05:01:47 +0200 Subject: [PATCH 370/858] Reduce bottlenecks scan code (#9863) --- Jellyfin.Api/Controllers/LibraryController.cs | 20 ++---- .../BaseItemManager/BaseItemManager.cs | 65 ++----------------- .../BaseItemManager/IBaseItemManager.cs | 5 -- .../Entities/Audio/MusicArtist.cs | 2 +- MediaBrowser.Controller/Entities/BaseItem.cs | 33 +++------- MediaBrowser.Controller/Entities/Folder.cs | 38 +++-------- .../MetadataConfigurationExtensions.cs | 17 +++-- .../Providers/IProviderManager.cs | 17 ----- .../Encoder/MediaEncoder.cs | 5 +- .../Configuration/ServerConfiguration.cs | 8 +-- .../Manager/ProviderManager.cs | 63 ++++-------------- .../StudioImages/StudiosImageProvider.cs | 8 +-- src/Jellyfin.Drawing/ImageProcessor.cs | 2 - 13 files changed, 64 insertions(+), 219 deletions(-) diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index e094d2d774..46c0a8d527 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -969,12 +969,8 @@ public class LibraryController : BaseJellyfinApiController || string.Equals(name, "MusicBrainz", StringComparison.OrdinalIgnoreCase); } - var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions - .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) - .ToArray(); - - return metadataOptions.Length == 0 - || metadataOptions.Any(i => !i.DisabledMetadataFetchers.Contains(name, StringComparison.OrdinalIgnoreCase)); + var metadataOptions = _serverConfigurationManager.GetMetadataOptionsForType(type); + return metadataOptions is null || !metadataOptions.DisabledMetadataFetchers.Contains(name, StringComparison.OrdinalIgnoreCase); } private bool IsImageFetcherEnabledByDefault(string name, string type, bool isNewLibrary) @@ -995,15 +991,7 @@ public class LibraryController : BaseJellyfinApiController || string.Equals(name, "Image Extractor", StringComparison.OrdinalIgnoreCase); } - var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions - .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) - .ToArray(); - - if (metadataOptions.Length == 0) - { - return true; - } - - return metadataOptions.Any(i => !i.DisabledImageFetchers.Contains(name, StringComparison.OrdinalIgnoreCase)); + var metadataOptions = _serverConfigurationManager.GetMetadataOptionsForType(type); + return metadataOptions is null || !metadataOptions.DisabledImageFetchers.Contains(name, StringComparison.OrdinalIgnoreCase); } } diff --git a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs index ed7c2c2c17..b263c173eb 100644 --- a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs +++ b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs @@ -1,11 +1,10 @@ using System; -using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Threading; using Jellyfin.Extensions; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; using MediaBrowser.Model.Configuration; namespace MediaBrowser.Controller.BaseItemManager @@ -15,8 +14,6 @@ namespace MediaBrowser.Controller.BaseItemManager { private readonly IServerConfigurationManager _serverConfigurationManager; - private int _metadataRefreshConcurrency; - /// <summary> /// Initializes a new instance of the <see cref="BaseItemManager"/> class. /// </summary> @@ -24,16 +21,8 @@ namespace MediaBrowser.Controller.BaseItemManager public BaseItemManager(IServerConfigurationManager serverConfigurationManager) { _serverConfigurationManager = serverConfigurationManager; - - _metadataRefreshConcurrency = GetMetadataRefreshConcurrency(); - SetupMetadataThrottler(); - - _serverConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated; } - /// <inheritdoc /> - public SemaphoreSlim MetadataRefreshThrottler { get; private set; } - /// <inheritdoc /> public bool IsMetadataFetcherEnabled(BaseItem baseItem, TypeOptions? libraryTypeOptions, string name) { @@ -51,12 +40,11 @@ namespace MediaBrowser.Controller.BaseItemManager if (libraryTypeOptions is not null) { - return libraryTypeOptions.MetadataFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase); + return libraryTypeOptions.MetadataFetchers.Contains(name, StringComparison.OrdinalIgnoreCase); } - var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, baseItem.GetType().Name, StringComparison.OrdinalIgnoreCase)); - - return itemConfig is null || !itemConfig.DisabledMetadataFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase); + var itemConfig = _serverConfigurationManager.GetMetadataOptionsForType(baseItem.GetType().Name); + return itemConfig is null || !itemConfig.DisabledMetadataFetchers.Contains(name, StringComparison.OrdinalIgnoreCase); } /// <inheritdoc /> @@ -76,50 +64,11 @@ namespace MediaBrowser.Controller.BaseItemManager if (libraryTypeOptions is not null) { - return libraryTypeOptions.ImageFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase); + return libraryTypeOptions.ImageFetchers.Contains(name, StringComparison.OrdinalIgnoreCase); } - var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, baseItem.GetType().Name, StringComparison.OrdinalIgnoreCase)); - - return itemConfig is null || !itemConfig.DisabledImageFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase); - } - - /// <summary> - /// Called when the configuration is updated. - /// It will refresh the metadata throttler if the relevant config changed. - /// </summary> - private void OnConfigurationUpdated(object? sender, EventArgs e) - { - int newMetadataRefreshConcurrency = GetMetadataRefreshConcurrency(); - if (_metadataRefreshConcurrency != newMetadataRefreshConcurrency) - { - _metadataRefreshConcurrency = newMetadataRefreshConcurrency; - SetupMetadataThrottler(); - } - } - - /// <summary> - /// Creates the metadata refresh throttler. - /// </summary> - [MemberNotNull(nameof(MetadataRefreshThrottler))] - private void SetupMetadataThrottler() - { - MetadataRefreshThrottler = new SemaphoreSlim(_metadataRefreshConcurrency); - } - - /// <summary> - /// Returns the metadata refresh concurrency. - /// </summary> - private int GetMetadataRefreshConcurrency() - { - var concurrency = _serverConfigurationManager.Configuration.LibraryMetadataRefreshConcurrency; - - if (concurrency <= 0) - { - concurrency = Environment.ProcessorCount; - } - - return concurrency; + var itemConfig = _serverConfigurationManager.GetMetadataOptionsForType(baseItem.GetType().Name); + return itemConfig is null || !itemConfig.DisabledImageFetchers.Contains(name, StringComparison.OrdinalIgnoreCase); } } } diff --git a/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs b/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs index b07c80879d..ac20120d97 100644 --- a/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs +++ b/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs @@ -9,11 +9,6 @@ namespace MediaBrowser.Controller.BaseItemManager /// </summary> public interface IBaseItemManager { - /// <summary> - /// Gets the semaphore used to limit the amount of concurrent metadata refreshes. - /// </summary> - SemaphoreSlim MetadataRefreshThrottler { get; } - /// <summary> /// Is metadata fetcher enabled. /// </summary> diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs index 15a79fa1fc..18d948a62f 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs @@ -59,7 +59,7 @@ namespace MediaBrowser.Controller.Entities.Audio { if (IsAccessedByName) { - return new List<BaseItem>(); + return Enumerable.Empty<BaseItem>(); } return base.Children; diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 1e868194e6..5018110035 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1244,14 +1244,6 @@ namespace MediaBrowser.Controller.Entities return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken); } - protected virtual void TriggerOnRefreshStart() - { - } - - protected virtual void TriggerOnRefreshComplete() - { - } - /// <summary> /// Overrides the base implementation to refresh metadata for local trailers. /// </summary> @@ -1260,8 +1252,6 @@ namespace MediaBrowser.Controller.Entities /// <returns>true if a provider reports we changed.</returns> public async Task<ItemUpdateType> RefreshMetadata(MetadataRefreshOptions options, CancellationToken cancellationToken) { - TriggerOnRefreshStart(); - var requiresSave = false; if (SupportsOwnedItems) @@ -1281,21 +1271,14 @@ namespace MediaBrowser.Controller.Entities } } - try - { - var refreshOptions = requiresSave - ? new MetadataRefreshOptions(options) - { - ForceSave = true - } - : options; + var refreshOptions = requiresSave + ? new MetadataRefreshOptions(options) + { + ForceSave = true + } + : options; - return await ProviderManager.RefreshSingleItem(this, refreshOptions, cancellationToken).ConfigureAwait(false); - } - finally - { - TriggerOnRefreshComplete(); - } + return await ProviderManager.RefreshSingleItem(this, refreshOptions, cancellationToken).ConfigureAwait(false); } protected bool IsVisibleStandaloneInternal(User user, bool checkFolders) @@ -1367,7 +1350,7 @@ namespace MediaBrowser.Controller.Entities private async Task<bool> RefreshExtras(BaseItem item, MetadataRefreshOptions options, IReadOnlyList<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) { var extras = LibraryManager.FindExtras(item, fileSystemChildren, options.DirectoryService).ToArray(); - var newExtraIds = extras.Select(i => i.Id).ToArray(); + var newExtraIds = Array.ConvertAll(extras, x => x.Id); var extrasChanged = !item.ExtraIds.SequenceEqual(newExtraIds); if (!extrasChanged && !options.ReplaceAllMetadata && options.MetadataRefreshMode != MetadataRefreshMode.FullRefresh) diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 84952295c4..44fe65103e 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -301,14 +301,6 @@ namespace MediaBrowser.Controller.Entities return dictionary; } - protected override void TriggerOnRefreshStart() - { - } - - protected override void TriggerOnRefreshComplete() - { - } - /// <summary> /// Validates the children internal. /// </summary> @@ -510,26 +502,17 @@ namespace MediaBrowser.Controller.Entities private async Task RefreshAllMetadataForContainer(IMetadataContainer container, MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken) { - // limit the amount of concurrent metadata refreshes - await ProviderManager.RunMetadataRefresh( - async () => - { - var series = container as Series; - if (series is not null) - { - await series.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); - } + if (container is Series series) + { + await series.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); + } - await container.RefreshAllMetadata(refreshOptions, progress, cancellationToken).ConfigureAwait(false); - }, - cancellationToken).ConfigureAwait(false); + await container.RefreshAllMetadata(refreshOptions, progress, cancellationToken).ConfigureAwait(false); } private async Task RefreshChildMetadata(BaseItem child, MetadataRefreshOptions refreshOptions, bool recursive, IProgress<double> progress, CancellationToken cancellationToken) { - var container = child as IMetadataContainer; - - if (container is not null) + if (child is IMetadataContainer container) { await RefreshAllMetadataForContainer(container, refreshOptions, progress, cancellationToken).ConfigureAwait(false); } @@ -537,10 +520,7 @@ namespace MediaBrowser.Controller.Entities { if (refreshOptions.RefreshItem(child)) { - // limit the amount of concurrent metadata refreshes - await ProviderManager.RunMetadataRefresh( - async () => await child.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false), - cancellationToken).ConfigureAwait(false); + await child.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); } if (recursive && child is Folder folder) @@ -586,7 +566,7 @@ namespace MediaBrowser.Controller.Entities } var fanoutConcurrency = ConfigurationManager.Configuration.LibraryScanFanoutConcurrency; - var parallelism = fanoutConcurrency == 0 ? Environment.ProcessorCount : fanoutConcurrency; + var parallelism = fanoutConcurrency > 0 ? fanoutConcurrency : 2 * Environment.ProcessorCount; var actionBlock = new ActionBlock<int>( async i => @@ -618,7 +598,7 @@ namespace MediaBrowser.Controller.Entities for (var i = 0; i < childrenCount; i++) { - actionBlock.Post(i); + await actionBlock.SendAsync(i).ConfigureAwait(false); } actionBlock.Complete(); diff --git a/MediaBrowser.Controller/Library/MetadataConfigurationExtensions.cs b/MediaBrowser.Controller/Library/MetadataConfigurationExtensions.cs index 41cfcae163..ee9420cb43 100644 --- a/MediaBrowser.Controller/Library/MetadataConfigurationExtensions.cs +++ b/MediaBrowser.Controller/Library/MetadataConfigurationExtensions.cs @@ -1,8 +1,8 @@ -#nullable disable - #pragma warning disable CS1591 +using System; using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Model.Configuration; namespace MediaBrowser.Controller.Library @@ -10,8 +10,15 @@ namespace MediaBrowser.Controller.Library public static class MetadataConfigurationExtensions { public static MetadataConfiguration GetMetadataConfiguration(this IConfigurationManager config) - { - return config.GetConfiguration<MetadataConfiguration>("metadata"); - } + => config.GetConfiguration<MetadataConfiguration>("metadata"); + + /// <summary> + /// Gets the <see cref="MetadataOptions" /> for the specified type. + /// </summary> + /// <param name="config">The <see cref="IServerConfigurationManager"/>.</param> + /// <param name="type">The type to get the <see cref="MetadataOptions" /> for.</param> + /// <returns>The <see cref="MetadataOptions" /> for the specified type or <c>null</c>.</returns> + public static MetadataOptions? GetMetadataOptionsForType(this IServerConfigurationManager config, string type) + => Array.Find(config.Configuration.MetadataOptions, i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)); } } diff --git a/MediaBrowser.Controller/Providers/IProviderManager.cs b/MediaBrowser.Controller/Providers/IProviderManager.cs index 7e0a69586c..16943f6aaa 100644 --- a/MediaBrowser.Controller/Providers/IProviderManager.cs +++ b/MediaBrowser.Controller/Providers/IProviderManager.cs @@ -54,14 +54,6 @@ namespace MediaBrowser.Controller.Providers /// <returns>Task.</returns> Task<ItemUpdateType> RefreshSingleItem(BaseItem item, MetadataRefreshOptions options, CancellationToken cancellationToken); - /// <summary> - /// Runs multiple metadata refreshes concurrently. - /// </summary> - /// <param name="action">The action to run.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>A <see cref="Task"/> representing the result of the asynchronous operation.</returns> - Task RunMetadataRefresh(Func<Task> action, CancellationToken cancellationToken); - /// <summary> /// Saves the image. /// </summary> @@ -207,15 +199,6 @@ namespace MediaBrowser.Controller.Providers where TItemType : BaseItem, new() where TLookupType : ItemLookupInfo; - /// <summary> - /// Gets the search image. - /// </summary> - /// <param name="providerName">Name of the provider.</param> - /// <param name="url">The URL.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task{HttpResponseInfo}.</returns> - Task<HttpResponseMessage> GetSearchImage(string providerName, string url, CancellationToken cancellationToken); - HashSet<Guid> GetRefreshQueue(); void OnRefreshStart(BaseItem item); diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index d2112e5dc2..4e63d205c9 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -57,7 +57,7 @@ namespace MediaBrowser.MediaEncoding.Encoder private readonly IServerConfigurationManager _serverConfig; private readonly string _startupOptionFFmpegPath; - private readonly SemaphoreSlim _thumbnailResourcePool = new SemaphoreSlim(2, 2); + private readonly SemaphoreSlim _thumbnailResourcePool; private readonly object _runningProcessesLock = new object(); private readonly List<ProcessWrapper> _runningProcesses = new List<ProcessWrapper>(); @@ -113,6 +113,9 @@ namespace MediaBrowser.MediaEncoding.Encoder _jsonSerializerOptions = new JsonSerializerOptions(JsonDefaults.Options); _jsonSerializerOptions.Converters.Add(new JsonBoolStringConverter()); + + var semaphoreCount = 2 * Environment.ProcessorCount; + _thumbnailResourcePool = new SemaphoreSlim(semaphoreCount, semaphoreCount); } /// <inheritdoc /> diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 8af782b3d5..78a310f0b1 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -189,7 +189,7 @@ namespace MediaBrowser.Model.Configuration public NameValuePair[] ContentTypes { get; set; } = Array.Empty<NameValuePair>(); - public int RemoteClientBitrateLimit { get; set; } = 0; + public int RemoteClientBitrateLimit { get; set; } public bool EnableFolderView { get; set; } = false; @@ -203,7 +203,7 @@ namespace MediaBrowser.Model.Configuration public bool EnableExternalContentInSuggestions { get; set; } = true; - public int ImageExtractionTimeoutMs { get; set; } = 0; + public int ImageExtractionTimeoutMs { get; set; } public PathSubstitution[] PathSubstitutions { get; set; } = Array.Empty<PathSubstitution>(); @@ -251,7 +251,7 @@ namespace MediaBrowser.Model.Configuration /// Gets or sets the dummy chapter duration in seconds, use 0 (zero) or less to disable generation alltogether. /// </summary> /// <value>The dummy chapters duration.</value> - public int DummyChapterDuration { get; set; } = 0; + public int DummyChapterDuration { get; set; } /// <summary> /// Gets or sets the chapter image resolution. @@ -263,6 +263,6 @@ namespace MediaBrowser.Model.Configuration /// Gets or sets the limit for parallel image encoding. /// </summary> /// <value>The limit for parallel image encoding.</value> - public int ParallelImageEncodingLimit { get; set; } = 0; + public int ParallelImageEncodingLimit { get; set; } } } diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 1028da32ba..5cb28402e8 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -131,12 +131,12 @@ namespace MediaBrowser.Providers.Manager { var type = item.GetType(); - var service = _metadataServices.FirstOrDefault(current => current.CanRefreshPrimary(type)); - service ??= _metadataServices.FirstOrDefault(current => current.CanRefresh(item)); + var service = _metadataServices.FirstOrDefault(current => current.CanRefreshPrimary(type)) + ?? _metadataServices.FirstOrDefault(current => current.CanRefresh(item)); if (service is null) { - _logger.LogError("Unable to find a metadata service for item of type {TypeName}", item.GetType().Name); + _logger.LogError("Unable to find a metadata service for item of type {TypeName}", type.Name); return Task.FromResult(ItemUpdateType.None); } @@ -160,7 +160,7 @@ namespace MediaBrowser.Providers.Manager // TODO: Isolate this hack into the tvh plugin if (string.IsNullOrEmpty(contentType)) { - if (url.IndexOf("/imagecache/", StringComparison.OrdinalIgnoreCase) != -1) + if (url.Contains("/imagecache/", StringComparison.OrdinalIgnoreCase)) { contentType = "image/png"; } @@ -232,6 +232,11 @@ namespace MediaBrowser.Providers.Manager providers = providers.Where(i => string.Equals(i.Name, providerName, StringComparison.OrdinalIgnoreCase)); } + if (query.ImageType is not null) + { + providers = providers.Where(i => i.GetSupportedImages(item).Contains(query.ImageType.Value)); + } + var preferredLanguage = item.GetPreferredMetadataLanguage(); var tasks = providers.Select(i => GetImages(item, i, preferredLanguage, query.IncludeAllLanguages, cancellationToken, query.ImageType)); @@ -568,13 +573,7 @@ namespace MediaBrowser.Providers.Manager /// <inheritdoc/> public MetadataOptions GetMetadataOptions(BaseItem item) - { - var type = item.GetType().Name; - - return _configurationManager.Configuration.MetadataOptions - .FirstOrDefault(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) ?? - new MetadataOptions(); - } + => _configurationManager.GetMetadataOptionsForType(item.GetType().Name) ?? new MetadataOptions(); /// <inheritdoc/> public Task SaveMetadataAsync(BaseItem item, ItemUpdateType updateType) @@ -809,27 +808,12 @@ namespace MediaBrowser.Providers.Manager { var results = await provider.GetSearchResults(searchInfo, cancellationToken).ConfigureAwait(false); - var list = results.ToList(); - - foreach (var item in list) + foreach (var item in results) { item.SearchProviderName = provider.Name; } - return list; - } - - /// <inheritdoc/> - public Task<HttpResponseMessage> GetSearchImage(string providerName, string url, CancellationToken cancellationToken) - { - var provider = _metadataProviders.OfType<IRemoteSearchProvider>().FirstOrDefault(i => string.Equals(i.Name, providerName, StringComparison.OrdinalIgnoreCase)); - - if (provider is null) - { - throw new ArgumentException("Search provider not found."); - } - - return provider.GetImageResponse(url, cancellationToken); + return results; } private IEnumerable<IExternalId> GetExternalIds(IHasProviderIds item) @@ -1102,29 +1086,6 @@ namespace MediaBrowser.Providers.Manager return RefreshItem(item, options, cancellationToken); } - /// <summary> - /// Runs multiple metadata refreshes concurrently. - /// </summary> - /// <param name="action">The action to run.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>A <see cref="Task"/> representing the result of the asynchronous operation.</returns> - public async Task RunMetadataRefresh(Func<Task> action, CancellationToken cancellationToken) - { - // create a variable for this since it is possible MetadataRefreshThrottler could change due to a config update during a scan - var metadataRefreshThrottler = _baseItemManager.MetadataRefreshThrottler; - - await metadataRefreshThrottler.WaitAsync(cancellationToken).ConfigureAwait(false); - - try - { - await action().ConfigureAwait(false); - } - finally - { - metadataRefreshThrottler.Release(); - } - } - /// <inheritdoc/> public void Dispose() { diff --git a/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs b/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs index ae244da19b..a8461e9912 100644 --- a/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs @@ -64,7 +64,7 @@ namespace MediaBrowser.Providers.Plugins.StudioImages { var thumbsPath = Path.Combine(_config.ApplicationPaths.CachePath, "imagesbyname", "remotestudiothumbs.txt"); - thumbsPath = await EnsureThumbsList(thumbsPath, cancellationToken).ConfigureAwait(false); + await EnsureThumbsList(thumbsPath, cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); @@ -107,7 +107,7 @@ namespace MediaBrowser.Providers.Plugins.StudioImages return string.Format(CultureInfo.InvariantCulture, "{0}/images/{1}/{2}.jpg", GetRepositoryUrl(), image, filename); } - private Task<string> EnsureThumbsList(string file, CancellationToken cancellationToken) + private Task EnsureThumbsList(string file, CancellationToken cancellationToken) { string url = string.Format(CultureInfo.InvariantCulture, "{0}/thumbs.txt", GetRepositoryUrl()); @@ -129,7 +129,7 @@ namespace MediaBrowser.Providers.Plugins.StudioImages /// <param name="fileSystem">The file system.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>A Task to ensure existence of a file listing.</returns> - public async Task<string> EnsureList(string url, string file, IFileSystem fileSystem, CancellationToken cancellationToken) + public async Task EnsureList(string url, string file, IFileSystem fileSystem, CancellationToken cancellationToken) { var fileInfo = fileSystem.GetFileInfo(file); @@ -148,8 +148,6 @@ namespace MediaBrowser.Providers.Plugins.StudioImages } } } - - return file; } /// <summary> diff --git a/src/Jellyfin.Drawing/ImageProcessor.cs b/src/Jellyfin.Drawing/ImageProcessor.cs index 533baba4fa..4e5d3b4d55 100644 --- a/src/Jellyfin.Drawing/ImageProcessor.cs +++ b/src/Jellyfin.Drawing/ImageProcessor.cs @@ -50,14 +50,12 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable /// <param name="appPaths">The server application paths.</param> /// <param name="fileSystem">The filesystem.</param> /// <param name="imageEncoder">The image encoder.</param> - /// <param name="mediaEncoder">The media encoder.</param> /// <param name="config">The configuration.</param> public ImageProcessor( ILogger<ImageProcessor> logger, IServerApplicationPaths appPaths, IFileSystem fileSystem, IImageEncoder imageEncoder, - IMediaEncoder mediaEncoder, IServerConfigurationManager config) { _logger = logger; From ca7d1a13000ad948eebbfdeb40542312f3e37d3e Mon Sep 17 00:00:00 2001 From: nicknsy <20588554+nicknsy@users.noreply.github.com> Date: Wed, 22 Feb 2023 00:08:35 -0800 Subject: [PATCH 371/858] Trickplay generation, manager, storage --- .../ApplicationHost.cs | 3 + .../Data/SqliteItemRepository.cs | 123 ++++++ Emby.Server.Implementations/Dto/DtoService.cs | 5 + .../MediaEncoding/EncodingHelper.cs | 35 ++ .../MediaEncoding/IMediaEncoder.cs | 27 ++ .../Persistence/IItemRepository.cs | 21 + .../Trickplay/ITrickplayManager.cs | 54 +++ .../Encoder/MediaEncoder.cs | 173 +++++++++ .../Configuration/EncodingOptions.cs | 12 + MediaBrowser.Model/Dto/BaseItemDto.cs | 6 + .../Entities/TrickplayTilesInfo.cs | 50 +++ MediaBrowser.Model/Querying/ItemFields.cs | 5 + .../MediaBrowser.Providers.csproj | 1 + .../Trickplay/TrickplayImagesTask.cs | 116 ++++++ .../Trickplay/TrickplayManager.cs | 363 ++++++++++++++++++ .../Trickplay/TrickplayProvider.cs | 109 ++++++ 16 files changed, 1103 insertions(+) create mode 100644 MediaBrowser.Controller/Trickplay/ITrickplayManager.cs create mode 100644 MediaBrowser.Model/Entities/TrickplayTilesInfo.cs create mode 100644 MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs create mode 100644 MediaBrowser.Providers/Trickplay/TrickplayManager.cs create mode 100644 MediaBrowser.Providers/Trickplay/TrickplayProvider.cs diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 7969577bc0..1e0bb0cd6f 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -78,6 +78,7 @@ using MediaBrowser.Controller.Session; using MediaBrowser.Controller.Sorting; using MediaBrowser.Controller.Subtitles; using MediaBrowser.Controller.SyncPlay; +using MediaBrowser.Controller.Trickplay; using MediaBrowser.Controller.TV; using MediaBrowser.LocalMetadata.Savers; using MediaBrowser.MediaEncoding.BdInfo; @@ -96,6 +97,7 @@ using MediaBrowser.Providers.Lyric; using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Plugins.Tmdb; using MediaBrowser.Providers.Subtitles; +using MediaBrowser.Providers.Trickplay; using MediaBrowser.XbmcMetadata.Providers; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -591,6 +593,7 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>(); serviceCollection.AddSingleton<IChapterManager, ChapterManager>(); + serviceCollection.AddSingleton<ITrickplayManager, TrickplayManager>(); serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>(); diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index ca8f605a02..8ec24522b0 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -48,6 +48,7 @@ namespace Emby.Server.Implementations.Data { private const string FromText = " from TypedBaseItems A"; private const string ChaptersTableName = "Chapters2"; + private const string TrickplayTableName = "Trickplay"; private const string SaveItemCommandText = @"replace into TypedBaseItems @@ -383,6 +384,8 @@ namespace Emby.Server.Implementations.Data "create table if not exists " + ChaptersTableName + " (ItemId GUID, ChapterIndex INT NOT NULL, StartPositionTicks BIGINT NOT NULL, Name TEXT, ImagePath TEXT, PRIMARY KEY (ItemId, ChapterIndex))", + "create table if not exists " + TrickplayTableName + " (ItemId GUID, Width INT NOT NULL, Height INT NOT NULL, TileWidth INT NOT NULL, TileHeight INT NOT NULL, TileCount INT NOT NULL, Interval INT NOT NULL, Bandwidth INT NOT NULL, PRIMARY KEY (ItemId, Width))", + CreateMediaStreamsTableCommand, CreateMediaAttachmentsTableCommand, @@ -2135,6 +2138,126 @@ namespace Emby.Server.Implementations.Data } } + /// <inheritdoc /> + public Dictionary<int, TrickplayTilesInfo> GetTilesResolutions(Guid itemId) + { + CheckDisposed(); + + var tilesResolutions = new Dictionary<int, TrickplayTilesInfo>(); + using (var connection = GetConnection(true)) + { + using (var statement = PrepareStatement(connection, "select Width,Height,TileWidth,TileHeight,TileCount,Interval,Bandwidth from " + TrickplayTableName + " where ItemId = @ItemId order by Width asc")) + { + statement.TryBind("@ItemId", itemId); + + foreach (var row in statement.ExecuteQuery()) + { + TrickplayTilesInfo tilesInfo = GetTrickplayTilesInfo(row); + tilesResolutions[tilesInfo.Width] = tilesInfo; + } + } + } + + return tilesResolutions; + } + + /// <inheritdoc /> + public void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo) + { + CheckDisposed(); + + ArgumentNullException.ThrowIfNull(tilesInfo); + + var idBlob = itemId.ToByteArray(); + using (var connection = GetConnection(false)) + { + connection.RunInTransaction( + db => + { + // Delete old tiles info + db.Execute("delete from " + TrickplayTableName + " where ItemId=@ItemId and Width=@Width", idBlob, tilesInfo.Width); + db.Execute( + "insert into " + TrickplayTableName + " values (@ItemId, @Width, @Height, @TileWidth, @TileHeight, @TileCount, @Interval, @Bandwidth)", + idBlob, + tilesInfo.Width, + tilesInfo.Height, + tilesInfo.TileWidth, + tilesInfo.TileHeight, + tilesInfo.TileCount, + tilesInfo.Interval, + tilesInfo.Bandwidth); + }, + TransactionMode); + } + } + + /// <inheritdoc /> + public Dictionary<Guid, Dictionary<int, TrickplayTilesInfo>> GetTrickplayManifest(BaseItem item) + { + CheckDisposed(); + + var trickplayManifest = new Dictionary<Guid, Dictionary<int, TrickplayTilesInfo>>(); + foreach (var mediaSource in item.GetMediaSources(false)) + { + var mediaSourceId = Guid.Parse(mediaSource.Id); + var tilesResolutions = GetTilesResolutions(mediaSourceId); + + if (tilesResolutions.Count > 0) + { + trickplayManifest[mediaSourceId] = tilesResolutions; + } + } + + return trickplayManifest; + } + + /// <summary> + /// Gets the trickplay tiles info. + /// </summary> + /// <param name="reader">The reader.</param> + /// <returns>TrickplayTilesInfo.</returns> + private TrickplayTilesInfo GetTrickplayTilesInfo(IReadOnlyList<ResultSetValue> reader) + { + var tilesInfo = new TrickplayTilesInfo(); + + if (reader.TryGetInt32(0, out var width)) + { + tilesInfo.Width = width; + } + + if (reader.TryGetInt32(1, out var height)) + { + tilesInfo.Height = height; + } + + if (reader.TryGetInt32(2, out var tileWidth)) + { + tilesInfo.TileWidth = tileWidth; + } + + if (reader.TryGetInt32(3, out var tileHeight)) + { + tilesInfo.TileHeight = tileHeight; + } + + if (reader.TryGetInt32(4, out var tileCount)) + { + tilesInfo.TileCount = tileCount; + } + + if (reader.TryGetInt32(5, out var interval)) + { + tilesInfo.Interval = interval; + } + + if (reader.TryGetInt32(6, out var bandwidth)) + { + tilesInfo.Bandwidth = bandwidth; + } + + return tilesInfo; + } + private static bool EnableJoinUserData(InternalItemsQuery query) { if (query.User is null) diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 7a6ed2cb80..10352b6ff8 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -1058,6 +1058,11 @@ namespace Emby.Server.Implementations.Dto dto.Chapters = _itemRepo.GetChapters(item); } + if (options.ContainsField(ItemFields.Trickplay)) + { + dto.Trickplay = _itemRepo.GetTrickplayManifest(item); + } + if (video.ExtraType.HasValue) { dto.ExtraType = video.ExtraType.Value.ToString(); diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index b155d674de..0889a90f48 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -149,6 +149,36 @@ namespace MediaBrowser.Controller.MediaEncoding return defaultEncoder; } + private string GetMjpegEncoder(EncodingJobInfo state, EncodingOptions encodingOptions) + { + var defaultEncoder = "mjpeg"; + + if (state.VideoType == VideoType.VideoFile) + { + var hwType = encodingOptions.HardwareAccelerationType; + + var codecMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) + { + { "vaapi", defaultEncoder + "_vaapi" }, + { "qsv", defaultEncoder + "_qsv" } + }; + + if (!string.IsNullOrEmpty(hwType) + && encodingOptions.EnableHardwareEncoding + && codecMap.ContainsKey(hwType)) + { + var preferredEncoder = codecMap[hwType]; + + if (_mediaEncoder.SupportsEncoder(preferredEncoder)) + { + return preferredEncoder; + } + } + } + + return defaultEncoder; + } + private bool IsVaapiSupported(EncodingJobInfo state) { // vaapi will throw an error with this input @@ -277,6 +307,11 @@ namespace MediaBrowser.Controller.MediaEncoding return GetH264Encoder(state, encodingOptions); } + if (string.Equals(codec, "mjpeg", StringComparison.OrdinalIgnoreCase)) + { + return GetMjpegEncoder(state, encodingOptions); + } + if (string.Equals(codec, "vp8", StringComparison.OrdinalIgnoreCase) || string.Equals(codec, "vpx", StringComparison.OrdinalIgnoreCase)) { diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index f830b9f298..aa9faa9369 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; @@ -137,6 +138,32 @@ namespace MediaBrowser.Controller.MediaEncoding /// <returns>Location of video image.</returns> Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, ImageFormat? targetFormat, CancellationToken cancellationToken); + /// <summary> + /// Extracts the video images on interval. + /// </summary> + /// <param name="inputFile">Input file.</param> + /// <param name="container">Video container type.</param> + /// <param name="mediaSource">Media source information.</param> + /// <param name="imageStream">Media stream information.</param> + /// <param name="interval">The interval.</param> + /// <param name="maxWidth">The maximum width.</param> + /// <param name="allowHwAccel">Allow for hardware acceleration.</param> + /// <param name="allowHwEncode">Allow for hardware encoding. allowHwAccel must also be true.</param> + /// <param name="encodingHelper">EncodingHelper instance.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Directory where images where extracted. A given image made before another will always be named with a lower number.</returns> + Task<string> ExtractVideoImagesOnIntervalAccelerated( + string inputFile, + string container, + MediaSourceInfo mediaSource, + MediaStream imageStream, + TimeSpan interval, + int maxWidth, + bool allowHwAccel, + bool allowHwEncode, + EncodingHelper encodingHelper, + CancellationToken cancellationToken); + /// <summary> /// Gets the media info. /// </summary> diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index 2c52b2b45e..11eb4932c9 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -61,6 +61,27 @@ namespace MediaBrowser.Controller.Persistence /// <param name="chapters">The list of chapters to save.</param> void SaveChapters(Guid id, IReadOnlyList<ChapterInfo> chapters); + /// <summary> + /// Get available trickplay resolutions and corresponding info. + /// </summary> + /// <param name="itemId">The item.</param> + /// <returns>Map of width resolutions to trickplay tiles info.</returns> + Dictionary<int, TrickplayTilesInfo> GetTilesResolutions(Guid itemId); + + /// <summary> + /// Saves trickplay tiles info. + /// </summary> + /// <param name="itemId">The item.</param> + /// <param name="tilesInfo">The trickplay tiles info.</param> + void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo); + + /// <summary> + /// Gets trickplay data for an item. + /// </summary> + /// <param name="item">The item.</param> + /// <returns>A map of media source id to a map of tile width to tile info.</returns> + Dictionary<Guid, Dictionary<int, TrickplayTilesInfo>> GetTrickplayManifest(BaseItem item); + /// <summary> /// Gets the media streams. /// </summary> diff --git a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs new file mode 100644 index 0000000000..bae458f98c --- /dev/null +++ b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Controller.Trickplay +{ + /// <summary> + /// Interface ITrickplayManager. + /// </summary> + public interface ITrickplayManager + { + /// <summary> + /// Generate or replace trickplay data. + /// </summary> + /// <param name="video">The video.</param> + /// <param name="replace">Whether or not existing data should be replaced.</param> + /// <param name="cancellationToken">CancellationToken to use for operation.</param> + /// <returns>Task.</returns> + Task RefreshTrickplayData(Video video, bool replace, CancellationToken cancellationToken); + + /// <summary> + /// Get available trickplay resolutions and corresponding info. + /// </summary> + /// <param name="itemId">The item.</param> + /// <returns>Map of width resolutions to trickplay tiles info.</returns> + Dictionary<int, TrickplayTilesInfo> GetTilesResolutions(Guid itemId); + + /// <summary> + /// Saves trickplay tiles info. + /// </summary> + /// <param name="itemId">The item.</param> + /// <param name="tilesInfo">The trickplay tiles info.</param> + void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo); + + /// <summary> + /// Gets the trickplay manifest. + /// </summary> + /// <param name="item">The item.</param> + /// <returns>A map of media source id to a map of tile width to tile info.</returns> + Dictionary<Guid, Dictionary<int, TrickplayTilesInfo>> GetTrickplayManifest(BaseItem item); + + /// <summary> + /// Gets the path to a trickplay tiles image. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="width">The width of a single tile.</param> + /// <param name="index">The tile grid's index.</param> + /// <returns>The absolute path.</returns> + string GetTrickplayTilePath(BaseItem item, int width, int index); + } +} diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 4e63d205c9..7f8ec03fac 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -21,6 +21,7 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.MediaEncoding.Probing; +using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; @@ -28,8 +29,10 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; +using Microsoft.AspNetCore.Components.Forms; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; +using static Nikse.SubtitleEdit.Core.Common.IfoParser; namespace MediaBrowser.MediaEncoding.Encoder { @@ -775,6 +778,176 @@ namespace MediaBrowser.MediaEncoding.Encoder } /// <inheritdoc /> + public Task<string> ExtractVideoImagesOnIntervalAccelerated( + string inputFile, + string container, + MediaSourceInfo mediaSource, + MediaStream imageStream, + TimeSpan interval, + int maxWidth, + bool allowHwAccel, + bool allowHwEncode, + EncodingHelper encodingHelper, + CancellationToken cancellationToken) + { + var options = allowHwAccel ? _configurationManager.GetEncodingOptions() : new EncodingOptions(); + + // A new EncodingOptions instance must be used as to not disable HW acceleration for all of Jellyfin. + // Additionally, we must set a few fields without defaults to prevent null pointer exceptions. + if (!allowHwAccel) + { + options.EnableHardwareEncoding = false; + options.HardwareAccelerationType = string.Empty; + options.EnableTonemapping = false; + } + + var baseRequest = new BaseEncodingJobOptions { MaxWidth = maxWidth }; + var jobState = new EncodingJobInfo(TranscodingJobType.Progressive) + { + IsVideoRequest = true, // must be true for InputVideoHwaccelArgs to return non-empty value + MediaSource = mediaSource, + VideoStream = imageStream, + BaseRequest = baseRequest, // GetVideoProcessingFilterParam errors if null + MediaPath = inputFile, + OutputVideoCodec = "mjpeg" + }; + var vidEncoder = options.AllowMjpegEncoding ? encodingHelper.GetVideoEncoder(jobState, options) : jobState.OutputVideoCodec; + + // Get input and filter arguments + var inputArg = encodingHelper.GetInputArgument(jobState, options, container).Trim(); + if (string.IsNullOrWhiteSpace(inputArg)) + { + throw new InvalidOperationException("EncodingHelper returned empty input arguments."); + } + + if (!allowHwAccel) + { + inputArg = "-threads " + _threads + " " + inputArg; // HW accel args set a different input thread count, only set if disabled + } + + var filterParam = encodingHelper.GetVideoProcessingFilterParam(jobState, options, jobState.OutputVideoCodec).Trim(); + if (string.IsNullOrWhiteSpace(filterParam) || filterParam.IndexOf("\"", StringComparison.Ordinal) == -1) + { + throw new InvalidOperationException("EncodingHelper returned empty or invalid filter parameters."); + } + + return ExtractVideoImagesOnIntervalInternal(inputArg, filterParam, interval, vidEncoder, _threads, cancellationToken); + } + + private async Task<string> ExtractVideoImagesOnIntervalInternal( + string inputArg, + string filterParam, + TimeSpan interval, + string vidEncoder, + int outputThreads, + CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(inputArg)) + { + throw new InvalidOperationException("Empty or invalid input argument."); + } + + // Output arguments + string fps = "fps=1/" + interval.TotalSeconds.ToString(CultureInfo.InvariantCulture); + if (string.IsNullOrWhiteSpace(filterParam)) + { + filterParam = "-vf \"" + fps + "\""; + } + else + { + filterParam = filterParam.Insert(filterParam.IndexOf("\"", StringComparison.Ordinal) + 1, fps + ","); + } + + var targetDirectory = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(targetDirectory); + var outputPath = Path.Combine(targetDirectory, "%08d.jpg"); + + // Final command arguments + var args = string.Format( + CultureInfo.InvariantCulture, + "-loglevel error {0} -an -sn {1} -threads {2} -c:v {3} -f {4} \"{5}\"", + inputArg, + filterParam, + outputThreads, + vidEncoder, + "image2", + outputPath); + + // Start ffmpeg process + var process = new Process + { + StartInfo = new ProcessStartInfo + { + CreateNoWindow = true, + UseShellExecute = false, + FileName = _ffmpegPath, + Arguments = args, + WindowStyle = ProcessWindowStyle.Hidden, + ErrorDialog = false, + }, + EnableRaisingEvents = true + }; + + var processDescription = string.Format(CultureInfo.InvariantCulture, "{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments); + _logger.LogDebug("{ProcessDescription}", processDescription); + + using (var processWrapper = new ProcessWrapper(process, this)) + { + bool ranToCompletion = false; + + await _thumbnailResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + StartProcess(processWrapper); + + // Need to give ffmpeg enough time to make all the thumbnails, which could be a while, + // but we still need to detect if the process hangs. + // Making the assumption that as long as new jpegs are showing up, everything is good. + + bool isResponsive = true; + int lastCount = 0; + + while (isResponsive) + { + if (await process.WaitForExitAsync(TimeSpan.FromSeconds(30)).ConfigureAwait(false)) + { + ranToCompletion = true; + break; + } + + cancellationToken.ThrowIfCancellationRequested(); + + var jpegCount = _fileSystem.GetFilePaths(targetDirectory) + .Count(i => string.Equals(Path.GetExtension(i), ".jpg", StringComparison.OrdinalIgnoreCase)); + + isResponsive = jpegCount > lastCount; + lastCount = jpegCount; + } + + if (!ranToCompletion) + { + _logger.LogInformation("Killing ffmpeg extraction process due to inactivity."); + StopProcess(processWrapper, 1000); + } + } + finally + { + _thumbnailResourcePool.Release(); + } + + var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1; + + if (exitCode == -1) + { + _logger.LogError("ffmpeg image extraction failed for {ProcessDescription}", processDescription); + + throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction failed for {0}", processDescription)); + } + + return targetDirectory; + } + } + public string GetTimeParameter(long ticks) { var time = TimeSpan.FromTicks(ticks); diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index a53be0fee7..e1d9e00b7a 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -48,7 +48,9 @@ public class EncodingOptions EnableIntelLowPowerH264HwEncoder = false; EnableIntelLowPowerHevcHwEncoder = false; EnableHardwareEncoding = true; + EnableTrickplayHwAccel = false; AllowHevcEncoding = false; + AllowMjpegEncoding = false; EnableSubtitleExtraction = true; AllowOnDemandMetadataBasedKeyframeExtractionForExtensions = new[] { "mkv" }; HardwareDecodingCodecs = new string[] { "h264", "vc1" }; @@ -244,11 +246,21 @@ public class EncodingOptions /// </summary> public bool EnableHardwareEncoding { get; set; } + /// <summary> + /// Gets or sets a value indicating whether hardware acceleration is enabled for trickplay generation. + /// </summary> + public bool EnableTrickplayHwAccel { get; set; } + /// <summary> /// Gets or sets a value indicating whether HEVC encoding is enabled. /// </summary> public bool AllowHevcEncoding { get; set; } + /// <summary> + /// Gets or sets a value indicating whether MJPEG encoding is enabled. + /// </summary> + public bool AllowMjpegEncoding { get; set; } + /// <summary> /// Gets or sets a value indicating whether subtitle extraction is enabled. /// </summary> diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs index 8fab1ca6d6..ab424c6f5c 100644 --- a/MediaBrowser.Model/Dto/BaseItemDto.cs +++ b/MediaBrowser.Model/Dto/BaseItemDto.cs @@ -568,6 +568,12 @@ namespace MediaBrowser.Model.Dto /// <value>The chapters.</value> public List<ChapterInfo> Chapters { get; set; } + /// <summary> + /// Gets or sets the trickplay manifest. + /// </summary> + /// <value>The trickplay manifest.</value> + public Dictionary<Guid, Dictionary<int, TrickplayTilesInfo>> Trickplay { get; set; } + /// <summary> /// Gets or sets the type of the location. /// </summary> diff --git a/MediaBrowser.Model/Entities/TrickplayTilesInfo.cs b/MediaBrowser.Model/Entities/TrickplayTilesInfo.cs new file mode 100644 index 0000000000..84b6b03228 --- /dev/null +++ b/MediaBrowser.Model/Entities/TrickplayTilesInfo.cs @@ -0,0 +1,50 @@ +namespace MediaBrowser.Model.Entities +{ + /// <summary> + /// Class TrickplayTilesInfo. + /// </summary> + public class TrickplayTilesInfo + { + /// <summary> + /// Gets or sets width of an individual tile. + /// </summary> + /// <value>The width.</value> + public int Width { get; set; } + + /// <summary> + /// Gets or sets height of an individual tile. + /// </summary> + /// <value>The height.</value> + public int Height { get; set; } + + /// <summary> + /// Gets or sets amount of tiles per row. + /// </summary> + /// <value>The tile grid's width.</value> + public int TileWidth { get; set; } + + /// <summary> + /// Gets or sets amount of tiles per column. + /// </summary> + /// <value>The tile grid's height.</value> + public int TileHeight { get; set; } + + /// <summary> + /// Gets or sets total amount of non-black tiles. + /// </summary> + /// <value>The tile count.</value> + public int TileCount { get; set; } + + /// <summary> + /// Gets or sets interval in milliseconds between each trickplay tile. + /// </summary> + /// <value>The interval.</value> + public int Interval { get; set; } + + /// <summary> + /// Gets or sets peak bandwith usage in bits per second. + /// </summary> + /// <value>The bandwidth.</value> + public int Bandwidth { get; set; } + } +} diff --git a/MediaBrowser.Model/Querying/ItemFields.cs b/MediaBrowser.Model/Querying/ItemFields.cs index 6fa1d778ad..242a1c6e99 100644 --- a/MediaBrowser.Model/Querying/ItemFields.cs +++ b/MediaBrowser.Model/Querying/ItemFields.cs @@ -34,6 +34,11 @@ namespace MediaBrowser.Model.Querying /// </summary> Chapters, + /// <summary> + /// The trickplay manifest. + /// </summary> + Trickplay, + ChildCount, /// <summary> diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 6a40833d7f..c836c8ed53 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -22,6 +22,7 @@ <PackageReference Include="Microsoft.Extensions.Http" /> <PackageReference Include="Newtonsoft.Json" /> <PackageReference Include="PlaylistsNET" /> + <PackageReference Include="SkiaSharp" /> <PackageReference Include="TagLibSharp" /> <PackageReference Include="TMDbLib" /> </ItemGroup> diff --git a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs new file mode 100644 index 0000000000..3d1450a906 --- /dev/null +++ b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Trickplay; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace MediaBrowser.Providers.Trickplay +{ + /// <summary> + /// Class TrickplayImagesTask. + /// </summary> + public class TrickplayImagesTask : IScheduledTask + { + private readonly ILogger<TrickplayImagesTask> _logger; + private readonly ILibraryManager _libraryManager; + private readonly ILocalizationManager _localization; + private readonly IServerConfigurationManager _configurationManager; + private readonly ITrickplayManager _trickplayManager; + + /// <summary> + /// Initializes a new instance of the <see cref="TrickplayImagesTask"/> class. + /// </summary> + /// <param name="logger">The logger.</param> + /// <param name="libraryManager">The library manager.</param> + /// <param name="localization">The localization manager.</param> + /// <param name="configurationManager">The configuration manager.</param> + /// <param name="trickplayManager">The trickplay manager.</param> + public TrickplayImagesTask( + ILogger<TrickplayImagesTask> logger, + ILibraryManager libraryManager, + ILocalizationManager localization, + IServerConfigurationManager configurationManager, + ITrickplayManager trickplayManager) + { + _libraryManager = libraryManager; + _logger = logger; + _localization = localization; + _configurationManager = configurationManager; + _trickplayManager = trickplayManager; + } + + /// <inheritdoc /> + public string Name => _localization.GetLocalizedString("TaskRefreshTrickplayImages"); + + /// <inheritdoc /> + public string Description => _localization.GetLocalizedString("TaskRefreshTrickplayImagesDescription"); + + /// <inheritdoc /> + public string Key => "RefreshTrickplayImages"; + + /// <inheritdoc /> + public string Category => _localization.GetLocalizedString("TasksLibraryCategory"); + + /// <inheritdoc /> + public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() + { + return new[] + { + new TaskTriggerInfo + { + Type = TaskTriggerInfo.TriggerDaily, + TimeOfDayTicks = TimeSpan.FromHours(3).Ticks, + MaxRuntimeTicks = TimeSpan.FromHours(5).Ticks + } + }; + } + + /// <inheritdoc /> + public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) + { + // TODO: libraryoptions dont run on libraries with trickplay disabled + var items = _libraryManager.GetItemList(new InternalItemsQuery + { + MediaTypes = new[] { MediaType.Video }, + IsVirtualItem = false, + IsFolder = false, + Recursive = false + }).OfType<Video>().ToList(); + + var numComplete = 0; + + foreach (var item in items) + { + try + { + cancellationToken.ThrowIfCancellationRequested(); + await _trickplayManager.RefreshTrickplayData(item, false, cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + break; + } + catch (Exception ex) + { + _logger.LogError("Error creating trickplay files for {ItemName}: {Msg}", item.Name, ex); + } + + numComplete++; + double percent = numComplete; + percent /= items.Count; + percent *= 100; + + progress.Report(percent); + } + } + } +} diff --git a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs new file mode 100644 index 0000000000..f1eb389ab7 --- /dev/null +++ b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs @@ -0,0 +1,363 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Controller.Trickplay; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using Microsoft.Extensions.Logging; +using SkiaSharp; + +namespace MediaBrowser.Providers.Trickplay +{ + /// <summary> + /// ITrickplayManager implementation. + /// </summary> + public class TrickplayManager : ITrickplayManager + { + private readonly ILogger<TrickplayManager> _logger; + private readonly IItemRepository _itemRepo; + private readonly IMediaEncoder _mediaEncoder; + private readonly IFileSystem _fileSystem; + private readonly EncodingHelper _encodingHelper; + + private static readonly SemaphoreSlim _resourcePool = new(1, 1); + + /// <summary> + /// Initializes a new instance of the <see cref="TrickplayManager"/> class. + /// </summary> + /// <param name="logger">The logger.</param> + /// <param name="itemRepo">The item repository.</param> + /// <param name="mediaEncoder">The media encoder.</param> + /// <param name="fileSystem">The file systen.</param> + /// <param name="encodingHelper">The encoding helper.</param> + public TrickplayManager( + ILogger<TrickplayManager> logger, + IItemRepository itemRepo, + IMediaEncoder mediaEncoder, + IFileSystem fileSystem, + EncodingHelper encodingHelper) + { + _logger = logger; + _itemRepo = itemRepo; + _mediaEncoder = mediaEncoder; + _fileSystem = fileSystem; + _encodingHelper = encodingHelper; + } + + /// <inheritdoc /> + public async Task RefreshTrickplayData(Video video, bool replace, CancellationToken cancellationToken) + { + _logger.LogDebug("Trickplay refresh for {ItemId} (replace existing: {Replace})", video.Id, replace); + + foreach (var width in new int[] { 320 } /*todo conf*/) + { + cancellationToken.ThrowIfCancellationRequested(); + await RefreshTrickplayData(video, replace, width, 10000/*todo conf*/, 10/*todo conf*/, 10/*todo conf*/, true/*todo conf*/, true/*todo conf*/, cancellationToken).ConfigureAwait(false); + } + } + + private async Task RefreshTrickplayData(Video video, bool replace, int width, int interval, int tileWidth, int tileHeight, bool doHwAccel, bool doHwEncode, CancellationToken cancellationToken) + { + if (!CanGenerateTrickplay(video)) + { + return; + } + + var imgTempDir = string.Empty; + var outputDir = GetTrickplayDirectory(video, width); + + try + { + await _resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); + + if (!replace && Directory.Exists(outputDir)) + { + _logger.LogDebug("Found existing trickplay files for {ItemId}. Exiting.", video.Id); + return; + } + + // Extract images + // Note: Media sources under parent items exist as their own video/item as well. Only use this video stream for trickplay. + var mediaSource = video.GetMediaSources(false).Find(source => Guid.Parse(source.Id).Equals(video.Id)); + + if (mediaSource is null) + { + _logger.LogDebug("Found no matching media source for item {ItemId}", video.Id); + return; + } + + var mediaPath = mediaSource.Path; + var mediaStream = mediaSource.VideoStream; + var container = mediaSource.Container; + + _logger.LogInformation("Creating trickplay files at {Width} width, for {Path} [ID: {ItemId}]", width, mediaPath, video.Id); + imgTempDir = await _mediaEncoder.ExtractVideoImagesOnIntervalAccelerated( + mediaPath, + container, + mediaSource, + mediaStream, + TimeSpan.FromMilliseconds(interval), + width, + doHwAccel, + doHwEncode, + _encodingHelper, + cancellationToken).ConfigureAwait(false); + + if (string.IsNullOrEmpty(imgTempDir) || !Directory.Exists(imgTempDir)) + { + throw new InvalidOperationException("Null or invalid directory from media encoder."); + } + + var images = _fileSystem.GetFiles(imgTempDir, new string[] { ".jpg" }, false, false) + .Where(img => string.Equals(img.Extension, ".jpg", StringComparison.Ordinal)) + .OrderBy(i => i.FullName) + .ToList(); + + // Create tiles + var tilesTempDir = Path.Combine(imgTempDir, Guid.NewGuid().ToString("N")); + var tilesInfo = CreateTiles(images, width, interval, tileWidth, tileHeight, tilesTempDir, outputDir); + + // Save tiles info + try + { + if (tilesInfo is not null) + { + SaveTilesInfo(video.Id, tilesInfo); + _logger.LogInformation("Finished creation of trickplay files for {0}", mediaPath); + } + else + { + throw new InvalidOperationException("Null trickplay tiles info from CreateTiles."); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while saving trickplay tiles info."); + + // Make sure no files stay in metadata folders on failure + // if tiles info wasn't saved. + Directory.Delete(outputDir, true); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating trickplay images."); + } + finally + { + _resourcePool.Release(); + + if (!string.IsNullOrEmpty(imgTempDir)) + { + Directory.Delete(imgTempDir, true); + } + } + } + + private TrickplayTilesInfo CreateTiles(List<FileSystemMetadata> images, int width, int interval, int tileWidth, int tileHeight, string workDir, string outputDir) + { + if (images.Count == 0) + { + throw new InvalidOperationException("Can't create trickplay from 0 images."); + } + + Directory.CreateDirectory(workDir); + + var tilesInfo = new TrickplayTilesInfo + { + Width = width, + Interval = interval, + TileWidth = tileWidth, + TileHeight = tileHeight, + TileCount = (int)Math.Ceiling((decimal)images.Count / tileWidth / tileHeight), + Bandwidth = 0 + }; + + var firstImg = SKBitmap.Decode(images[0].FullName); + if (firstImg == null) + { + throw new InvalidDataException("Could not decode image data."); + } + + tilesInfo.Height = firstImg.Height; + if (tilesInfo.Width != firstImg.Width) + { + throw new InvalidOperationException("Image width does not match config width."); + } + + /* + * Generate grids of trickplay image tiles + */ + var imgNo = 0; + var i = 0; + while (i < images.Count) + { + var tileGrid = new SKBitmap(tilesInfo.Width * tilesInfo.TileWidth, tilesInfo.Height * tilesInfo.TileHeight); + var tileCount = 0; + + using (var canvas = new SKCanvas(tileGrid)) + { + for (var y = 0; y < tilesInfo.TileHeight; y++) + { + for (var x = 0; x < tilesInfo.TileWidth; x++) + { + if (i >= images.Count) + { + break; + } + + var img = SKBitmap.Decode(images[i].FullName); + if (img == null) + { + throw new InvalidDataException("Could not decode image data."); + } + + if (tilesInfo.Width != img.Width) + { + throw new InvalidOperationException("Image width does not match config width."); + } + + if (tilesInfo.Height != img.Height) + { + throw new InvalidOperationException("Image height does not match first image height."); + } + + canvas.DrawBitmap(img, x * tilesInfo.Width, y * tilesInfo.Height); + tileCount++; + i++; + } + } + } + + // Output each tile grid to singular file + var tileGridPath = Path.Combine(workDir, $"{imgNo}.jpg"); + using (var stream = File.OpenWrite(tileGridPath)) + { + tileGrid.Encode(stream, SKEncodedImageFormat.Jpeg, 100/* todo _config.JpegQuality*/); + } + + var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tileGridPath).Length * 8 / tilesInfo.TileWidth / tilesInfo.TileHeight / (tilesInfo.Interval / 1000)); + tilesInfo.Bandwidth = Math.Max(tilesInfo.Bandwidth, bitrate); + + imgNo++; + } + + /* + * Move trickplay tiles to output directory + */ + Directory.CreateDirectory(outputDir); + + // Replace existing tile grids if they already exist + if (Directory.Exists(outputDir)) + { + Directory.Delete(outputDir, true); + } + + MoveDirectory(workDir, outputDir); + + return tilesInfo; + } + + private bool CanGenerateTrickplay(Video video) + { + var videoType = video.VideoType; + if (videoType == VideoType.Iso || videoType == VideoType.Dvd || videoType == VideoType.BluRay) + { + return false; + } + + if (video.IsPlaceHolder) + { + return false; + } + + /* TODO config options + var libraryOptions = _libraryManager.GetLibraryOptions(video); + if (libraryOptions is not null) + { + if (!libraryOptions.EnableChapterImageExtraction) + { + return false; + } + } + else + { + return false; + } + */ + + // TODO: media length is shorter than configured interval + + if (video.IsShortcut) + { + return false; + } + + if (!video.IsCompleteMedia) + { + return false; + } + + // Can't extract images if there are no video streams + return video.GetMediaStreams().Count > 0; + } + + /// <inheritdoc /> + public Dictionary<int, TrickplayTilesInfo> GetTilesResolutions(Guid itemId) + { + return _itemRepo.GetTilesResolutions(itemId); + } + + /// <inheritdoc /> + public void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo) + { + _itemRepo.SaveTilesInfo(itemId, tilesInfo); + } + + /// <inheritdoc /> + public Dictionary<Guid, Dictionary<int, TrickplayTilesInfo>> GetTrickplayManifest(BaseItem item) + { + return _itemRepo.GetTrickplayManifest(item); + } + + /// <inheritdoc /> + public string GetTrickplayTilePath(BaseItem item, int width, int index) + { + return Path.Combine(GetTrickplayDirectory(item, width), index + ".jpg"); + } + + private string GetTrickplayDirectory(BaseItem item, int? width = null) + { + var path = Path.Combine(item.GetInternalMetadataPath(), "trickplay"); + + return width.HasValue ? Path.Combine(path, width.Value.ToString(CultureInfo.InvariantCulture)) : path; + } + + private void MoveDirectory(string source, string destination) + { + try + { + Directory.Move(source, destination); + } + catch (System.IO.IOException) + { + // Cross device move requires a copy + Directory.CreateDirectory(destination); + foreach (string file in Directory.GetFiles(source)) + { + File.Copy(file, Path.Join(destination, Path.GetFileName(file)), true); + } + + Directory.Delete(source, true); + } + } + } +} diff --git a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs new file mode 100644 index 0000000000..8606e148b1 --- /dev/null +++ b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs @@ -0,0 +1,109 @@ +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Controller.Trickplay; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Providers.Trickplay +{ + /// <summary> + /// Class TrickplayProvider. Provides images and metadata for trickplay + /// scrubbing previews. + /// </summary> + public class TrickplayProvider : ICustomMetadataProvider<Episode>, + ICustomMetadataProvider<MusicVideo>, + ICustomMetadataProvider<Movie>, + ICustomMetadataProvider<Trailer>, + ICustomMetadataProvider<Video>, + IHasItemChangeMonitor, + IHasOrder, + IForcedProvider + { + private readonly ILogger<TrickplayProvider> _logger; + private readonly IServerConfigurationManager _configurationManager; + private readonly ITrickplayManager _trickplayManager; + + /// <summary> + /// Initializes a new instance of the <see cref="TrickplayProvider"/> class. + /// </summary> + /// <param name="logger">The logger.</param> + /// <param name="configurationManager">The configuration manager.</param> + /// <param name="trickplayManager">The trickplay manager.</param> + public TrickplayProvider( + ILogger<TrickplayProvider> logger, + IServerConfigurationManager configurationManager, + ITrickplayManager trickplayManager) + { + _logger = logger; + _configurationManager = configurationManager; + _trickplayManager = trickplayManager; + } + + /// <inheritdoc /> + public string Name => "Trickplay Preview"; + + /// <inheritdoc /> + public int Order => 1000; + + /// <inheritdoc /> + public bool HasChanged(BaseItem item, IDirectoryService directoryService) + { + if (item.IsFileProtocol) + { + var file = directoryService.GetFile(item.Path); + if (file is not null && item.DateModified != file.LastWriteTimeUtc) + { + return true; + } + } + + return false; + } + + /// <inheritdoc /> + public Task<ItemUpdateType> FetchAsync(Episode item, MetadataRefreshOptions options, CancellationToken cancellationToken) + { + return FetchInternal(item, options, cancellationToken); + } + + /// <inheritdoc /> + public Task<ItemUpdateType> FetchAsync(MusicVideo item, MetadataRefreshOptions options, CancellationToken cancellationToken) + { + return FetchInternal(item, options, cancellationToken); + } + + /// <inheritdoc /> + public Task<ItemUpdateType> FetchAsync(Movie item, MetadataRefreshOptions options, CancellationToken cancellationToken) + { + return FetchInternal(item, options, cancellationToken); + } + + /// <inheritdoc /> + public Task<ItemUpdateType> FetchAsync(Trailer item, MetadataRefreshOptions options, CancellationToken cancellationToken) + { + return FetchInternal(item, options, cancellationToken); + } + + /// <inheritdoc /> + public Task<ItemUpdateType> FetchAsync(Video item, MetadataRefreshOptions options, CancellationToken cancellationToken) + { + return FetchInternal(item, options, cancellationToken); + } + + private async Task<ItemUpdateType> FetchInternal(Video item, MetadataRefreshOptions options, CancellationToken cancellationToken) + { + // TODO: will "search for missing metadata" always trigger this? + // TODO: implement all config options --> + // TODO: this is always blocking for metadata collection, make non-blocking option + await _trickplayManager.RefreshTrickplayData(item, options.ReplaceAllImages, cancellationToken).ConfigureAwait(false); + + // The core doesn't need to trigger any save operations over this + return ItemUpdateType.None; + } + } +} From 515ee90fb96b32d89134852b95ebcd8dbb656b94 Mon Sep 17 00:00:00 2001 From: nicknsy <20588554+nicknsy@users.noreply.github.com> Date: Wed, 22 Feb 2023 18:17:54 -0800 Subject: [PATCH 372/858] Hls playlist --- .../Controllers/DynamicHlsController.cs | 26 +++- Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 125 +++++++++++++++++- .../Models/StreamingDtos/VideoRequestDto.cs | 7 +- .../Trickplay/TrickplayImagesTask.cs | 1 - .../Trickplay/TrickplayManager.cs | 31 ++--- .../Trickplay/TrickplayProvider.cs | 1 - 6 files changed, 170 insertions(+), 21 deletions(-) diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 9f2088e36e..2dbc343e96 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -407,6 +407,7 @@ public class DynamicHlsController : BaseJellyfinApiController /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param> /// <param name="streamOptions">Optional. The streaming options.</param> /// <param name="enableAdaptiveBitrateStreaming">Enable adaptive bitrate streaming.</param> + /// <param name="enableTrickplay">Enable trickplay image playlists being added to master playlist.</param> /// <response code="200">Video stream returned.</response> /// <returns>A <see cref="FileResult"/> containing the playlist file.</returns> [HttpGet("Videos/{itemId}/master.m3u8")] @@ -464,7 +465,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? videoStreamIndex, [FromQuery] EncodingContext? context, [FromQuery] Dictionary<string, string> streamOptions, - [FromQuery] bool enableAdaptiveBitrateStreaming = true) + [FromQuery] bool enableAdaptiveBitrateStreaming = true, + [FromQuery] bool enableTrickplay = true) { var streamingRequest = new HlsVideoRequestDto { @@ -518,7 +520,8 @@ public class DynamicHlsController : BaseJellyfinApiController VideoStreamIndex = videoStreamIndex, Context = context ?? EncodingContext.Streaming, StreamOptions = streamOptions, - EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming + EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming, + EnableTrickplay = enableTrickplay }; return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false); @@ -1025,6 +1028,25 @@ public class DynamicHlsController : BaseJellyfinApiController .ConfigureAwait(false); } + /// <summary> + /// Gets an image tiles playlist for trickplay. + /// </summary> + /// <param name="itemId">The item id.</param> + /// <param name="width">The width of a single tile.</param> + /// <param name="mediaSourceId">The media version id.</param> + /// <response code="200">Tiles stream returned.</response> + /// <returns>A <see cref="FileResult"/> containing the trickplay tiles file.</returns> + [HttpGet("Videos/{itemId}/tiles.m3u8")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesPlaylistFile] + public ActionResult GetTrickplayTilesHlsPlaylist( + [FromRoute, Required] Guid itemId, + [FromQuery, Required] int width, + [FromQuery, Required] string mediaSourceId) + { + return _dynamicHlsHelper.GetTilesHlsPlaylist(width, mediaSourceId); + } + /// <summary> /// Gets a video stream using HTTP live streaming. /// </summary> diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index 4486954c62..d8a16876df 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using Jellyfin.Api.Extensions; using Jellyfin.Api.Models.StreamingDtos; using Jellyfin.Extensions; +using Jellyfin.Data.Entities; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; @@ -18,6 +19,7 @@ using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Trickplay; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Net; @@ -45,6 +47,7 @@ public class DynamicHlsHelper private readonly ILogger<DynamicHlsHelper> _logger; private readonly IHttpContextAccessor _httpContextAccessor; private readonly EncodingHelper _encodingHelper; + private readonly ITrickplayManager _trickplayManager; /// <summary> /// Initializes a new instance of the <see cref="DynamicHlsHelper"/> class. @@ -61,6 +64,7 @@ public class DynamicHlsHelper /// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsHelper}"/> interface.</param> /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param> /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param> + /// <param name="trickplayManager">Instance of <see cref="ITrickplayManager"/>.</param> public DynamicHlsHelper( ILibraryManager libraryManager, IUserManager userManager, @@ -73,7 +77,8 @@ public class DynamicHlsHelper INetworkManager networkManager, ILogger<DynamicHlsHelper> logger, IHttpContextAccessor httpContextAccessor, - EncodingHelper encodingHelper) + EncodingHelper encodingHelper, + ITrickplayManager trickplayManager) { _libraryManager = libraryManager; _userManager = userManager; @@ -87,6 +92,7 @@ public class DynamicHlsHelper _logger = logger; _httpContextAccessor = httpContextAccessor; _encodingHelper = encodingHelper; + _trickplayManager = trickplayManager; } /// <summary> @@ -112,6 +118,81 @@ public class DynamicHlsHelper cancellationTokenSource).ConfigureAwait(false); } + /// <summary> + /// Get trickplay tiles hls playlist. + /// </summary> + /// <param name="width">The width of a single tile.</param> + /// <param name="mediaSourceId">The media version id.</param> + /// <returns>The resulting <see cref="ActionResult"/>.</returns> + public ActionResult GetTilesHlsPlaylist(int width, string mediaSourceId) + { + if (_httpContextAccessor.HttpContext is null) + { + throw new ResourceNotFoundException(nameof(_httpContextAccessor.HttpContext)); + } + + var tilesResolutions = _trickplayManager.GetTilesResolutions(Guid.Parse(mediaSourceId)); + if (tilesResolutions is not null && tilesResolutions.ContainsKey(width)) + { + var builder = new StringBuilder(128); + var tilesInfo = tilesResolutions[width]; + + if (tilesInfo.TileCount > 0) + { + const string urlFormat = "{0}/Trickplay/{1}/{2}.jpg?&api_key={3}"; + const string decimalFormat = "{0:0.###}"; + + var resolution = tilesInfo.Width.ToString(CultureInfo.InvariantCulture) + "x" + tilesInfo.Height.ToString(CultureInfo.InvariantCulture); + var layout = tilesInfo.TileWidth.ToString(CultureInfo.InvariantCulture) + "x" + tilesInfo.TileHeight.ToString(CultureInfo.InvariantCulture); + var tilesPerGrid = tilesInfo.TileWidth * tilesInfo.TileHeight; + var tileDuration = (decimal)tilesInfo.Interval / 1000; + var tileGridCount = (int)Math.Ceiling((decimal)tilesInfo.TileCount / tilesPerGrid); + + builder.AppendLine("#EXTM3U"); + builder.Append("#EXT-X-TARGETDURATION:").AppendLine(tileGridCount.ToString(CultureInfo.InvariantCulture)); + builder.AppendLine("#EXT-X-VERSION:7"); + builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:1"); + builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD"); + builder.AppendLine("#EXT-X-IMAGES-ONLY"); + + for (int i = 0; i < tileGridCount; i++) + { + // All tile grids before the last one must contain full amount of tiles. + // The final grid will be 0 < count <= maxTiles + if (i == tileGridCount - 1) + { + tilesPerGrid = tilesInfo.TileCount - (i * tilesPerGrid); + } + + var infDuration = tileDuration * tilesPerGrid; + var url = string.Format( + CultureInfo.InvariantCulture, + urlFormat, + mediaSourceId, + width.ToString(CultureInfo.InvariantCulture), + i.ToString(CultureInfo.InvariantCulture), + _httpContextAccessor.HttpContext.User.GetToken()); + + // EXTINF + builder.Append("#EXTINF:").Append(string.Format(CultureInfo.InvariantCulture, decimalFormat, infDuration)) + .AppendLine(","); + + // EXT-X-TILES + builder.Append("#EXT-X-TILES:RESOLUTION=").Append(resolution).Append(",LAYOUT=").Append(layout).Append(",DURATION=") + .AppendLine(string.Format(CultureInfo.InvariantCulture, decimalFormat, tileDuration)); + + // URL + builder.AppendLine(url); + } + + builder.AppendLine("#EXT-X-ENDLIST"); + return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8")); + } + } + + return new FileContentResult(Array.Empty<byte>(), MimeTypes.GetMimeType("playlist.m3u8")); + } + private async Task<ActionResult> GetMasterPlaylistInternal( StreamingRequestDto streamingRequest, bool isHeadRequest, @@ -299,6 +380,13 @@ public class DynamicHlsHelper AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup); } + if (!isLiveStream && (state.VideoRequest?.EnableTrickplay).GetValueOrDefault(false)) + { + var sourceId = Guid.Parse(state.Request.MediaSourceId); + var tilesResolutions = _trickplayManager.GetTilesResolutions(sourceId); + AddTrickplay(state, tilesResolutions, builder, _httpContextAccessor.HttpContext.User); + } + return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8")); } @@ -527,6 +615,41 @@ public class DynamicHlsHelper } } + /// <summary> + /// Appends EXT-X-IMAGE-STREAM-INF playlists for each available trickplay resolution. + /// </summary> + /// <param name="state">StreamState of the current stream.</param> + /// <param name="tilesResolutions">Dictionary of widths to corresponding tiles info.</param> + /// <param name="builder">StringBuilder to append the field to.</param> + /// <param name="user">Http user context.</param> + private void AddTrickplay(StreamState state, Dictionary<int, TrickplayTilesInfo> tilesResolutions, StringBuilder builder, ClaimsPrincipal user) + { + const string playlistFormat = "#EXT-X-IMAGE-STREAM-INF:BANDWIDTH={0},RESOLUTION={1}x{2},CODECS=\"jpeg\",URI=\"{3}\""; + + foreach (var resolution in tilesResolutions) + { + var width = resolution.Key; + var tilesInfo = resolution.Value; + + var url = string.Format( + CultureInfo.InvariantCulture, + "tiles.m3u8?Width={0}&MediaSourceId={1}&api_key={2}", + width.ToString(CultureInfo.InvariantCulture), + state.Request.MediaSourceId, + user.GetToken()); + + var line = string.Format( + CultureInfo.InvariantCulture, + playlistFormat, + tilesInfo.Bandwidth.ToString(CultureInfo.InvariantCulture), + tilesInfo.Width.ToString(CultureInfo.InvariantCulture), + tilesInfo.Height.ToString(CultureInfo.InvariantCulture), + url); + + builder.AppendLine(line); + } + } + /// <summary> /// Get the H.26X level of the output video stream. /// </summary> diff --git a/Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs b/Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs index 60c529d4ab..8548fec1a1 100644 --- a/Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs +++ b/Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs @@ -1,4 +1,4 @@ -namespace Jellyfin.Api.Models.StreamingDtos; +namespace Jellyfin.Api.Models.StreamingDtos; /// <summary> /// The video request dto. @@ -15,4 +15,9 @@ public class VideoRequestDto : StreamingRequestDto /// Gets or sets a value indicating whether to enable subtitles in the manifest. /// </summary> public bool EnableSubtitlesInManifest { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether to enable trickplay images. + /// </summary> + public bool EnableTrickplay { get; set; } } diff --git a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs index 3d1450a906..87ac145d75 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs +++ b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs @@ -11,7 +11,6 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; namespace MediaBrowser.Providers.Trickplay { diff --git a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs index f1eb389ab7..62180804f7 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs +++ b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs @@ -9,7 +9,6 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Trickplay; -using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; @@ -66,7 +65,7 @@ namespace MediaBrowser.Providers.Trickplay private async Task RefreshTrickplayData(Video video, bool replace, int width, int interval, int tileWidth, int tileHeight, bool doHwAccel, bool doHwEncode, CancellationToken cancellationToken) { - if (!CanGenerateTrickplay(video)) + if (!CanGenerateTrickplay(video, interval)) { return; } @@ -78,7 +77,7 @@ namespace MediaBrowser.Providers.Trickplay { await _resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); - if (!replace && Directory.Exists(outputDir)) + if (!replace && Directory.Exists(outputDir) && GetTilesResolutions(video.Id).ContainsKey(width)) { _logger.LogDebug("Found existing trickplay files for {ItemId}. Exiting.", video.Id); return; @@ -177,7 +176,7 @@ namespace MediaBrowser.Providers.Trickplay Interval = interval, TileWidth = tileWidth, TileHeight = tileHeight, - TileCount = (int)Math.Ceiling((decimal)images.Count / tileWidth / tileHeight), + TileCount = 0, Bandwidth = 0 }; @@ -201,7 +200,6 @@ namespace MediaBrowser.Providers.Trickplay while (i < images.Count) { var tileGrid = new SKBitmap(tilesInfo.Width * tilesInfo.TileWidth, tilesInfo.Height * tilesInfo.TileHeight); - var tileCount = 0; using (var canvas = new SKCanvas(tileGrid)) { @@ -231,7 +229,7 @@ namespace MediaBrowser.Providers.Trickplay } canvas.DrawBitmap(img, x * tilesInfo.Width, y * tilesInfo.Height); - tileCount++; + tilesInfo.TileCount++; i++; } } @@ -266,7 +264,7 @@ namespace MediaBrowser.Providers.Trickplay return tilesInfo; } - private bool CanGenerateTrickplay(Video video) + private bool CanGenerateTrickplay(Video video, int interval) { var videoType = video.VideoType; if (videoType == VideoType.Iso || videoType == VideoType.Dvd || videoType == VideoType.BluRay) @@ -279,6 +277,16 @@ namespace MediaBrowser.Providers.Trickplay return false; } + if (video.IsShortcut) + { + return false; + } + + if (!video.IsCompleteMedia) + { + return false; + } + /* TODO config options var libraryOptions = _libraryManager.GetLibraryOptions(video); if (libraryOptions is not null) @@ -294,14 +302,7 @@ namespace MediaBrowser.Providers.Trickplay } */ - // TODO: media length is shorter than configured interval - - if (video.IsShortcut) - { - return false; - } - - if (!video.IsCompleteMedia) + if (!video.RunTimeTicks.HasValue || video.RunTimeTicks.Value < TimeSpan.FromMilliseconds(interval).Ticks) { return false; } diff --git a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs index 8606e148b1..be66dea8ad 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs +++ b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs @@ -97,7 +97,6 @@ namespace MediaBrowser.Providers.Trickplay private async Task<ItemUpdateType> FetchInternal(Video item, MetadataRefreshOptions options, CancellationToken cancellationToken) { - // TODO: will "search for missing metadata" always trigger this? // TODO: implement all config options --> // TODO: this is always blocking for metadata collection, make non-blocking option await _trickplayManager.RefreshTrickplayData(item, options.ReplaceAllImages, cancellationToken).ConfigureAwait(false); From b18d6bd3562a5e5a69f806b461049fbf9f61b70e Mon Sep 17 00:00:00 2001 From: nicknsy <20588554+nicknsy@users.noreply.github.com> Date: Wed, 22 Feb 2023 23:13:55 -0800 Subject: [PATCH 373/858] Trickplay playlist and image controller --- .../Controllers/DynamicHlsController.cs | 19 -- .../Controllers/TrickplayController.cs | 177 ++++++++++++++++++ Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 77 +------- 3 files changed, 178 insertions(+), 95 deletions(-) create mode 100644 Jellyfin.Api/Controllers/TrickplayController.cs diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 2dbc343e96..acd8111435 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1028,25 +1028,6 @@ public class DynamicHlsController : BaseJellyfinApiController .ConfigureAwait(false); } - /// <summary> - /// Gets an image tiles playlist for trickplay. - /// </summary> - /// <param name="itemId">The item id.</param> - /// <param name="width">The width of a single tile.</param> - /// <param name="mediaSourceId">The media version id.</param> - /// <response code="200">Tiles stream returned.</response> - /// <returns>A <see cref="FileResult"/> containing the trickplay tiles file.</returns> - [HttpGet("Videos/{itemId}/tiles.m3u8")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesPlaylistFile] - public ActionResult GetTrickplayTilesHlsPlaylist( - [FromRoute, Required] Guid itemId, - [FromQuery, Required] int width, - [FromQuery, Required] string mediaSourceId) - { - return _dynamicHlsHelper.GetTilesHlsPlaylist(width, mediaSourceId); - } - /// <summary> /// Gets a video stream using HTTP live streaming. /// </summary> diff --git a/Jellyfin.Api/Controllers/TrickplayController.cs b/Jellyfin.Api/Controllers/TrickplayController.cs new file mode 100644 index 0000000000..389eb43ffc --- /dev/null +++ b/Jellyfin.Api/Controllers/TrickplayController.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net.Mime; +using System.Text; +using System.Threading.Tasks; +using Jellyfin.Api.Attributes; +using Jellyfin.Api.Extensions; +using Jellyfin.Api.Helpers; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Trickplay; +using MediaBrowser.Model; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Api.Controllers; + +/// <summary> +/// Trickplay controller. +/// </summary> +[Route("")] +[Authorize] +public class TrickplayController : BaseJellyfinApiController +{ + private readonly ILogger<TrickplayController> _logger; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly ILibraryManager _libraryManager; + private readonly ITrickplayManager _trickplayManager; + + /// <summary> + /// Initializes a new instance of the <see cref="TrickplayController"/> class. + /// </summary> + /// <param name="logger">Instance of the <see cref="ILogger{TrickplayController}"/> interface.</param> + /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param> + /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/>.</param> + /// <param name="trickplayManager">Instance of <see cref="ITrickplayManager"/>.</param> + public TrickplayController( + ILogger<TrickplayController> logger, + IHttpContextAccessor httpContextAccessor, + ILibraryManager libraryManager, + ITrickplayManager trickplayManager) + { + _logger = logger; + _httpContextAccessor = httpContextAccessor; + _libraryManager = libraryManager; + _trickplayManager = trickplayManager; + } + + /// <summary> + /// Gets an image tiles playlist for trickplay. + /// </summary> + /// <param name="itemId">The item id.</param> + /// <param name="width">The width of a single tile.</param> + /// <param name="mediaSourceId">The media version id, if using an alternate version.</param> + /// <response code="200">Tiles stream returned.</response> + /// <returns>A <see cref="FileResult"/> containing the trickplay tiles file.</returns> + [HttpGet("Videos/{itemId}/Trickplay/{width}/tiles.m3u8")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesPlaylistFile] + public ActionResult GetTrickplayHlsPlaylist( + [FromRoute, Required] Guid itemId, + [FromRoute, Required] int width, + [FromQuery] string? mediaSourceId) + { + return GetTrickplayPlaylistInternal(width, mediaSourceId ?? itemId.ToString("N")); + } + + /// <summary> + /// Gets a trickplay tile grid image. + /// </summary> + /// <param name="itemId">The item id.</param> + /// <param name="width">The width of a single tile.</param> + /// <param name="index">The index of the desired tile grid.</param> + /// <param name="mediaSourceId">The media version id, if using an alternate version.</param> + /// <response code="200">Tiles image returned.</response> + /// <response code="200">Tiles image not found at specified index.</response> + /// <returns>A <see cref="FileResult"/> containing the trickplay tiles image.</returns> + [HttpGet("Videos/{itemId}/Trickplay/{width}/{index}.jpg")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesImageFile] + public ActionResult GetTrickplayHlsPlaylist( + [FromRoute, Required] Guid itemId, + [FromRoute, Required] int width, + [FromRoute, Required] int index, + [FromQuery] string? mediaSourceId) + { + var item = _libraryManager.GetItemById(mediaSourceId ?? itemId.ToString("N")); + if (item is null) + { + return NotFound(); + } + + var path = _trickplayManager.GetTrickplayTilePath(item, width, index); + if (System.IO.File.Exists(path)) + { + return PhysicalFile(path, MediaTypeNames.Image.Jpeg); + } + + return NotFound(); + } + + private ActionResult GetTrickplayPlaylistInternal(int width, string mediaSourceId) + { + if (_httpContextAccessor.HttpContext is null) + { + throw new ResourceNotFoundException(nameof(_httpContextAccessor.HttpContext)); + } + + var tilesResolutions = _trickplayManager.GetTilesResolutions(Guid.Parse(mediaSourceId)); + if (tilesResolutions is not null && tilesResolutions.ContainsKey(width)) + { + var builder = new StringBuilder(128); + var tilesInfo = tilesResolutions[width]; + + if (tilesInfo.TileCount > 0) + { + const string urlFormat = "Trickplay/{0}/{1}.jpg?MediaSourceId={2}&api_key={3}"; + const string decimalFormat = "{0:0.###}"; + + var resolution = tilesInfo.Width.ToString(CultureInfo.InvariantCulture) + "x" + tilesInfo.Height.ToString(CultureInfo.InvariantCulture); + var layout = tilesInfo.TileWidth.ToString(CultureInfo.InvariantCulture) + "x" + tilesInfo.TileHeight.ToString(CultureInfo.InvariantCulture); + var tilesPerGrid = tilesInfo.TileWidth * tilesInfo.TileHeight; + var tileDuration = (decimal)tilesInfo.Interval / 1000; + var tileGridCount = (int)Math.Ceiling((decimal)tilesInfo.TileCount / tilesPerGrid); + + builder.AppendLine("#EXTM3U"); + builder.Append("#EXT-X-TARGETDURATION:").AppendLine(tileGridCount.ToString(CultureInfo.InvariantCulture)); + builder.AppendLine("#EXT-X-VERSION:7"); + builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:1"); + builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD"); + builder.AppendLine("#EXT-X-IMAGES-ONLY"); + + for (int i = 0; i < tileGridCount; i++) + { + // All tile grids before the last one must contain full amount of tiles. + // The final grid will be 0 < count <= maxTiles + if (i == tileGridCount - 1) + { + tilesPerGrid = tilesInfo.TileCount - (i * tilesPerGrid); + } + + var infDuration = tileDuration * tilesPerGrid; + var url = string.Format( + CultureInfo.InvariantCulture, + urlFormat, + width.ToString(CultureInfo.InvariantCulture), + i.ToString(CultureInfo.InvariantCulture), + mediaSourceId, + _httpContextAccessor.HttpContext.User.GetToken()); + + // EXTINF + builder.Append("#EXTINF:").Append(string.Format(CultureInfo.InvariantCulture, decimalFormat, infDuration)) + .AppendLine(","); + + // EXT-X-TILES + builder.Append("#EXT-X-TILES:RESOLUTION=").Append(resolution).Append(",LAYOUT=").Append(layout).Append(",DURATION=") + .AppendLine(string.Format(CultureInfo.InvariantCulture, decimalFormat, tileDuration)); + + // URL + builder.AppendLine(url); + } + + builder.AppendLine("#EXT-X-ENDLIST"); + return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8")); + } + } + + return new FileContentResult(Array.Empty<byte>(), MimeTypes.GetMimeType("playlist.m3u8")); + } +} diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index d8a16876df..6508dd8cf9 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -118,81 +118,6 @@ public class DynamicHlsHelper cancellationTokenSource).ConfigureAwait(false); } - /// <summary> - /// Get trickplay tiles hls playlist. - /// </summary> - /// <param name="width">The width of a single tile.</param> - /// <param name="mediaSourceId">The media version id.</param> - /// <returns>The resulting <see cref="ActionResult"/>.</returns> - public ActionResult GetTilesHlsPlaylist(int width, string mediaSourceId) - { - if (_httpContextAccessor.HttpContext is null) - { - throw new ResourceNotFoundException(nameof(_httpContextAccessor.HttpContext)); - } - - var tilesResolutions = _trickplayManager.GetTilesResolutions(Guid.Parse(mediaSourceId)); - if (tilesResolutions is not null && tilesResolutions.ContainsKey(width)) - { - var builder = new StringBuilder(128); - var tilesInfo = tilesResolutions[width]; - - if (tilesInfo.TileCount > 0) - { - const string urlFormat = "{0}/Trickplay/{1}/{2}.jpg?&api_key={3}"; - const string decimalFormat = "{0:0.###}"; - - var resolution = tilesInfo.Width.ToString(CultureInfo.InvariantCulture) + "x" + tilesInfo.Height.ToString(CultureInfo.InvariantCulture); - var layout = tilesInfo.TileWidth.ToString(CultureInfo.InvariantCulture) + "x" + tilesInfo.TileHeight.ToString(CultureInfo.InvariantCulture); - var tilesPerGrid = tilesInfo.TileWidth * tilesInfo.TileHeight; - var tileDuration = (decimal)tilesInfo.Interval / 1000; - var tileGridCount = (int)Math.Ceiling((decimal)tilesInfo.TileCount / tilesPerGrid); - - builder.AppendLine("#EXTM3U"); - builder.Append("#EXT-X-TARGETDURATION:").AppendLine(tileGridCount.ToString(CultureInfo.InvariantCulture)); - builder.AppendLine("#EXT-X-VERSION:7"); - builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:1"); - builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD"); - builder.AppendLine("#EXT-X-IMAGES-ONLY"); - - for (int i = 0; i < tileGridCount; i++) - { - // All tile grids before the last one must contain full amount of tiles. - // The final grid will be 0 < count <= maxTiles - if (i == tileGridCount - 1) - { - tilesPerGrid = tilesInfo.TileCount - (i * tilesPerGrid); - } - - var infDuration = tileDuration * tilesPerGrid; - var url = string.Format( - CultureInfo.InvariantCulture, - urlFormat, - mediaSourceId, - width.ToString(CultureInfo.InvariantCulture), - i.ToString(CultureInfo.InvariantCulture), - _httpContextAccessor.HttpContext.User.GetToken()); - - // EXTINF - builder.Append("#EXTINF:").Append(string.Format(CultureInfo.InvariantCulture, decimalFormat, infDuration)) - .AppendLine(","); - - // EXT-X-TILES - builder.Append("#EXT-X-TILES:RESOLUTION=").Append(resolution).Append(",LAYOUT=").Append(layout).Append(",DURATION=") - .AppendLine(string.Format(CultureInfo.InvariantCulture, decimalFormat, tileDuration)); - - // URL - builder.AppendLine(url); - } - - builder.AppendLine("#EXT-X-ENDLIST"); - return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8")); - } - } - - return new FileContentResult(Array.Empty<byte>(), MimeTypes.GetMimeType("playlist.m3u8")); - } - private async Task<ActionResult> GetMasterPlaylistInternal( StreamingRequestDto streamingRequest, bool isHeadRequest, @@ -633,7 +558,7 @@ public class DynamicHlsHelper var url = string.Format( CultureInfo.InvariantCulture, - "tiles.m3u8?Width={0}&MediaSourceId={1}&api_key={2}", + "Trickplay/{0}/tiles.m3u8?MediaSourceId={1}&api_key={2}", width.ToString(CultureInfo.InvariantCulture), state.Request.MediaSourceId, user.GetToken()); From 31a858f52030df85001382ae584e34edb11839ac Mon Sep 17 00:00:00 2001 From: nicknsy <20588554+nicknsy@users.noreply.github.com> Date: Thu, 23 Feb 2023 16:04:35 -0800 Subject: [PATCH 374/858] IsAutomated not set on copy --- .../Providers/MetadataRefreshOptions.cs | 1 + .../Configuration/EncodingOptions.cs | 6 ---- .../Configuration/LibraryOptions.cs | 4 +++ .../Trickplay/TrickplayManager.cs | 21 ++++++++------ .../Trickplay/TrickplayProvider.cs | 29 +++++++++++++++---- 5 files changed, 40 insertions(+), 21 deletions(-) diff --git a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs index 9e91a8bcd7..004c16ba23 100644 --- a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs +++ b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs @@ -25,6 +25,7 @@ namespace MediaBrowser.Controller.Providers ForceSave = copy.ForceSave; ReplaceAllMetadata = copy.ReplaceAllMetadata; EnableRemoteContentProbe = copy.EnableRemoteContentProbe; + IsAutomated = copy.IsAutomated; IsAutomated = copy.IsAutomated; ImageRefreshMode = copy.ImageRefreshMode; diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index e1d9e00b7a..2990cedcd1 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -48,7 +48,6 @@ public class EncodingOptions EnableIntelLowPowerH264HwEncoder = false; EnableIntelLowPowerHevcHwEncoder = false; EnableHardwareEncoding = true; - EnableTrickplayHwAccel = false; AllowHevcEncoding = false; AllowMjpegEncoding = false; EnableSubtitleExtraction = true; @@ -246,11 +245,6 @@ public class EncodingOptions /// </summary> public bool EnableHardwareEncoding { get; set; } - /// <summary> - /// Gets or sets a value indicating whether hardware acceleration is enabled for trickplay generation. - /// </summary> - public bool EnableTrickplayHwAccel { get; set; } - /// <summary> /// Gets or sets a value indicating whether HEVC encoding is enabled. /// </summary> diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs index df68299465..7718f822bb 100644 --- a/MediaBrowser.Model/Configuration/LibraryOptions.cs +++ b/MediaBrowser.Model/Configuration/LibraryOptions.cs @@ -36,6 +36,10 @@ namespace MediaBrowser.Model.Configuration public bool ExtractChapterImagesDuringLibraryScan { get; set; } + public bool EnableTrickplayImageExtraction { get; set; } + + public bool ExtractTrickplayImagesDuringLibraryScan { get; set; } + public MediaPathInfo[] PathInfos { get; set; } public bool SaveLocalMetadata { get; set; } diff --git a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs index 62180804f7..4b45148974 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs +++ b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Trickplay; @@ -26,6 +27,7 @@ namespace MediaBrowser.Providers.Trickplay private readonly IMediaEncoder _mediaEncoder; private readonly IFileSystem _fileSystem; private readonly EncodingHelper _encodingHelper; + private readonly ILibraryManager _libraryManager; private static readonly SemaphoreSlim _resourcePool = new(1, 1); @@ -37,18 +39,21 @@ namespace MediaBrowser.Providers.Trickplay /// <param name="mediaEncoder">The media encoder.</param> /// <param name="fileSystem">The file systen.</param> /// <param name="encodingHelper">The encoding helper.</param> + /// <param name="libraryManager">The library manager.</param> public TrickplayManager( ILogger<TrickplayManager> logger, IItemRepository itemRepo, IMediaEncoder mediaEncoder, IFileSystem fileSystem, - EncodingHelper encodingHelper) + EncodingHelper encodingHelper, + ILibraryManager libraryManager) { _logger = logger; _itemRepo = itemRepo; _mediaEncoder = mediaEncoder; _fileSystem = fileSystem; _encodingHelper = encodingHelper; + _libraryManager = libraryManager; } /// <inheritdoc /> @@ -287,11 +292,15 @@ namespace MediaBrowser.Providers.Trickplay return false; } - /* TODO config options + if (!video.RunTimeTicks.HasValue || video.RunTimeTicks.Value < TimeSpan.FromMilliseconds(interval).Ticks) + { + return false; + } + var libraryOptions = _libraryManager.GetLibraryOptions(video); if (libraryOptions is not null) { - if (!libraryOptions.EnableChapterImageExtraction) + if (!libraryOptions.EnableTrickplayImageExtraction) { return false; } @@ -300,12 +309,6 @@ namespace MediaBrowser.Providers.Trickplay { return false; } - */ - - if (!video.RunTimeTicks.HasValue || video.RunTimeTicks.Value < TimeSpan.FromMilliseconds(interval).Ticks) - { - return false; - } // Can't extract images if there are no video streams return video.GetMediaStreams().Count > 0; diff --git a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs index be66dea8ad..2b3879ca31 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs +++ b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs @@ -27,6 +27,7 @@ namespace MediaBrowser.Providers.Trickplay private readonly ILogger<TrickplayProvider> _logger; private readonly IServerConfigurationManager _configurationManager; private readonly ITrickplayManager _trickplayManager; + private readonly ILibraryManager _libraryManager; /// <summary> /// Initializes a new instance of the <see cref="TrickplayProvider"/> class. @@ -34,21 +35,24 @@ namespace MediaBrowser.Providers.Trickplay /// <param name="logger">The logger.</param> /// <param name="configurationManager">The configuration manager.</param> /// <param name="trickplayManager">The trickplay manager.</param> + /// <param name="libraryManager">The library manager.</param> public TrickplayProvider( ILogger<TrickplayProvider> logger, IServerConfigurationManager configurationManager, - ITrickplayManager trickplayManager) + ITrickplayManager trickplayManager, + ILibraryManager libraryManager) { _logger = logger; _configurationManager = configurationManager; _trickplayManager = trickplayManager; + _libraryManager = libraryManager; } /// <inheritdoc /> - public string Name => "Trickplay Preview"; + public string Name => "Trickplay Provider"; /// <inheritdoc /> - public int Order => 1000; + public int Order => 100; /// <inheritdoc /> public bool HasChanged(BaseItem item, IDirectoryService directoryService) @@ -95,11 +99,24 @@ namespace MediaBrowser.Providers.Trickplay return FetchInternal(item, options, cancellationToken); } - private async Task<ItemUpdateType> FetchInternal(Video item, MetadataRefreshOptions options, CancellationToken cancellationToken) + private async Task<ItemUpdateType> FetchInternal(Video video, MetadataRefreshOptions options, CancellationToken cancellationToken) { - // TODO: implement all config options --> + var libraryOptions = _libraryManager.GetLibraryOptions(video); + bool? enableDuringScan = libraryOptions?.ExtractTrickplayImagesDuringLibraryScan; + bool replace = options.ReplaceAllImages; + + if (options.IsAutomated && !enableDuringScan.GetValueOrDefault(false)) + { + _logger.LogDebug("exit refresh: automated - {0} enable scan - {1}", options.IsAutomated, enableDuringScan.GetValueOrDefault(false)); + return ItemUpdateType.None; + } + // TODO: this is always blocking for metadata collection, make non-blocking option - await _trickplayManager.RefreshTrickplayData(item, options.ReplaceAllImages, cancellationToken).ConfigureAwait(false); + if (true) + { + _logger.LogDebug("called refresh"); + await _trickplayManager.RefreshTrickplayData(video, replace, cancellationToken).ConfigureAwait(false); + } // The core doesn't need to trigger any save operations over this return ItemUpdateType.None; From 16ea7baad4030074e291159f3b35ebb009872544 Mon Sep 17 00:00:00 2001 From: nicknsy <20588554+nicknsy@users.noreply.github.com> Date: Thu, 23 Feb 2023 16:10:18 -0800 Subject: [PATCH 375/858] Stay consistent with patch branch --- MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs index 004c16ba23..9e91a8bcd7 100644 --- a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs +++ b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs @@ -25,7 +25,6 @@ namespace MediaBrowser.Controller.Providers ForceSave = copy.ForceSave; ReplaceAllMetadata = copy.ReplaceAllMetadata; EnableRemoteContentProbe = copy.EnableRemoteContentProbe; - IsAutomated = copy.IsAutomated; IsAutomated = copy.IsAutomated; ImageRefreshMode = copy.ImageRefreshMode; From 6c649a7e723454e94303d95d178e91b820ba6b50 Mon Sep 17 00:00:00 2001 From: nicknsy <20588554+nicknsy@users.noreply.github.com> Date: Thu, 23 Feb 2023 17:58:34 -0800 Subject: [PATCH 376/858] Options --- .../Encoder/MediaEncoder.cs | 6 +- .../Configuration/TrickplayOptions.cs | 61 +++++++++++++++++++ .../Configuration/TrickplayScanBehavior.cs | 18 ++++++ 3 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 MediaBrowser.Model/Configuration/TrickplayOptions.cs create mode 100644 MediaBrowser.Model/Configuration/TrickplayScanBehavior.cs diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 7f8ec03fac..9b58f83b48 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -853,10 +853,14 @@ namespace MediaBrowser.MediaEncoding.Encoder { filterParam = "-vf \"" + fps + "\""; } - else + else if (filterParam.IndexOf("\"", StringComparison.Ordinal) != -1) { filterParam = filterParam.Insert(filterParam.IndexOf("\"", StringComparison.Ordinal) + 1, fps + ","); } + else + { + filterParam += fps + ","; + } var targetDirectory = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid().ToString("N")); Directory.CreateDirectory(targetDirectory); diff --git a/MediaBrowser.Model/Configuration/TrickplayOptions.cs b/MediaBrowser.Model/Configuration/TrickplayOptions.cs new file mode 100644 index 0000000000..d527baaa46 --- /dev/null +++ b/MediaBrowser.Model/Configuration/TrickplayOptions.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using System.Diagnostics; + +namespace MediaBrowser.Model.Configuration +{ + /// <summary> + /// Class TrickplayOptions. + /// </summary> + public class TrickplayOptions + { + /// <summary> + /// Gets or sets a value indicating whether or not to use HW acceleration. + /// </summary> + public bool EnableHwAcceleration { get; set; } = false; + + /// <summary> + /// Gets or sets the behavior used by trickplay provider on library scan/update. + /// </summary> + public TrickplayScanBehavior ScanBehavior { get; set; } = TrickplayScanBehavior.NonBlocking; + + /// <summary> + /// Gets or sets the process priority for the ffmpeg process. + /// </summary> + public ProcessPriorityClass ProcessPriority { get; set; } = ProcessPriorityClass.BelowNormal; + + /// <summary> + /// Gets or sets the interval, in ms, between each new trickplay image. + /// </summary> + public int Interval { get; set; } = 10000; + + /// <summary> + /// Gets or sets the target width resolutions, in px, to generates preview images for. + /// </summary> + public HashSet<int> WidthResolutions { get; set; } = new HashSet<int> { 320 }; + + /// <summary> + /// Gets or sets number of tile images to allow in X dimension. + /// </summary> + public int TileWidth { get; set; } = 10; + + /// <summary> + /// Gets or sets number of tile images to allow in Y dimension. + /// </summary> + public int TileHeight { get; set; } = 10; + + /// <summary> + /// Gets or sets the ffmpeg output quality level. + /// </summary> + public int Qscale { get; set; } = 10; + + /// <summary> + /// Gets or sets the jpeg quality to use for image tiles. + /// </summary> + public int JpegQuality { get; set; } = 90; + + /// <summary> + /// Gets or sets the number of threads to be used by ffmpeg. + /// </summary> + public int ProcessThreads { get; set; } = 0; + } +} diff --git a/MediaBrowser.Model/Configuration/TrickplayScanBehavior.cs b/MediaBrowser.Model/Configuration/TrickplayScanBehavior.cs new file mode 100644 index 0000000000..7997941768 --- /dev/null +++ b/MediaBrowser.Model/Configuration/TrickplayScanBehavior.cs @@ -0,0 +1,18 @@ +namespace MediaBrowser.Model.Configuration +{ + /// <summary> + /// Enum TrickplayScanBehavior. + /// </summary> + public enum TrickplayScanBehavior + { + /// <summary> + /// Starts generation, only return once complete. + /// </summary> + Blocking, + + /// <summary> + /// Start generation, return immediately. + /// </summary> + NonBlocking + } +} From d448cc18ea14237c3fe9c2812a4eea1927a2d83f Mon Sep 17 00:00:00 2001 From: nicknsy <20588554+nicknsy@users.noreply.github.com> Date: Thu, 23 Feb 2023 18:03:22 -0800 Subject: [PATCH 377/858] update --- Jellyfin.Api/Controllers/TrickplayController.cs | 3 ++- MediaBrowser.Providers/Trickplay/TrickplayManager.cs | 6 +++--- MediaBrowser.Providers/Trickplay/TrickplayProvider.cs | 2 -- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Jellyfin.Api/Controllers/TrickplayController.cs b/Jellyfin.Api/Controllers/TrickplayController.cs index 389eb43ffc..46289f1701 100644 --- a/Jellyfin.Api/Controllers/TrickplayController.cs +++ b/Jellyfin.Api/Controllers/TrickplayController.cs @@ -128,6 +128,7 @@ public class TrickplayController : BaseJellyfinApiController var layout = tilesInfo.TileWidth.ToString(CultureInfo.InvariantCulture) + "x" + tilesInfo.TileHeight.ToString(CultureInfo.InvariantCulture); var tilesPerGrid = tilesInfo.TileWidth * tilesInfo.TileHeight; var tileDuration = (decimal)tilesInfo.Interval / 1000; + var infDuration = tileDuration * tilesPerGrid; var tileGridCount = (int)Math.Ceiling((decimal)tilesInfo.TileCount / tilesPerGrid); builder.AppendLine("#EXTM3U"); @@ -144,9 +145,9 @@ public class TrickplayController : BaseJellyfinApiController if (i == tileGridCount - 1) { tilesPerGrid = tilesInfo.TileCount - (i * tilesPerGrid); + infDuration = tileDuration * tilesPerGrid; } - var infDuration = tileDuration * tilesPerGrid; var url = string.Format( CultureInfo.InvariantCulture, urlFormat, diff --git a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs index 4b45148974..cb916dfdbf 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs +++ b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs @@ -127,7 +127,7 @@ namespace MediaBrowser.Providers.Trickplay // Create tiles var tilesTempDir = Path.Combine(imgTempDir, Guid.NewGuid().ToString("N")); - var tilesInfo = CreateTiles(images, width, interval, tileWidth, tileHeight, tilesTempDir, outputDir); + var tilesInfo = CreateTiles(images, width, interval, tileWidth, tileHeight, 100/* todo _config.JpegQuality*/, tilesTempDir, outputDir); // Save tiles info try @@ -166,7 +166,7 @@ namespace MediaBrowser.Providers.Trickplay } } - private TrickplayTilesInfo CreateTiles(List<FileSystemMetadata> images, int width, int interval, int tileWidth, int tileHeight, string workDir, string outputDir) + private TrickplayTilesInfo CreateTiles(List<FileSystemMetadata> images, int width, int interval, int tileWidth, int tileHeight, int quality, string workDir, string outputDir) { if (images.Count == 0) { @@ -244,7 +244,7 @@ namespace MediaBrowser.Providers.Trickplay var tileGridPath = Path.Combine(workDir, $"{imgNo}.jpg"); using (var stream = File.OpenWrite(tileGridPath)) { - tileGrid.Encode(stream, SKEncodedImageFormat.Jpeg, 100/* todo _config.JpegQuality*/); + tileGrid.Encode(stream, SKEncodedImageFormat.Jpeg, quality); } var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tileGridPath).Length * 8 / tilesInfo.TileWidth / tilesInfo.TileHeight / (tilesInfo.Interval / 1000)); diff --git a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs index 2b3879ca31..e4bd9e3c20 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs +++ b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs @@ -107,14 +107,12 @@ namespace MediaBrowser.Providers.Trickplay if (options.IsAutomated && !enableDuringScan.GetValueOrDefault(false)) { - _logger.LogDebug("exit refresh: automated - {0} enable scan - {1}", options.IsAutomated, enableDuringScan.GetValueOrDefault(false)); return ItemUpdateType.None; } // TODO: this is always blocking for metadata collection, make non-blocking option if (true) { - _logger.LogDebug("called refresh"); await _trickplayManager.RefreshTrickplayData(video, replace, cancellationToken).ConfigureAwait(false); } From 6744e712d3a4fd6f800e5499c90b247787e48cb6 Mon Sep 17 00:00:00 2001 From: nicknsy <20588554+nicknsy@users.noreply.github.com> Date: Sat, 25 Feb 2023 15:59:46 -0800 Subject: [PATCH 378/858] Use config values --- .../MediaEncoding/IMediaEncoder.cs | 13 +++-- .../Encoder/MediaEncoder.cs | 37 +++++++++----- .../Configuration/ServerConfiguration.cs | 2 + .../Trickplay/TrickplayImagesTask.cs | 12 +++-- .../Trickplay/TrickplayManager.cs | 49 +++++++++++++------ .../Trickplay/TrickplayProvider.cs | 16 +++--- 6 files changed, 89 insertions(+), 40 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index aa9faa9369..f5e3d03cbc 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.Configuration; @@ -145,10 +146,12 @@ namespace MediaBrowser.Controller.MediaEncoding /// <param name="container">Video container type.</param> /// <param name="mediaSource">Media source information.</param> /// <param name="imageStream">Media stream information.</param> - /// <param name="interval">The interval.</param> /// <param name="maxWidth">The maximum width.</param> + /// <param name="interval">The interval.</param> /// <param name="allowHwAccel">Allow for hardware acceleration.</param> - /// <param name="allowHwEncode">Allow for hardware encoding. allowHwAccel must also be true.</param> + /// <param name="threads">The input/output thread count for ffmpeg.</param> + /// <param name="qualityScale">The qscale value for ffmpeg.</param> + /// <param name="priority">The process priority for the ffmpeg process.</param> /// <param name="encodingHelper">EncodingHelper instance.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Directory where images where extracted. A given image made before another will always be named with a lower number.</returns> @@ -157,10 +160,12 @@ namespace MediaBrowser.Controller.MediaEncoding string container, MediaSourceInfo mediaSource, MediaStream imageStream, - TimeSpan interval, int maxWidth, + TimeSpan interval, bool allowHwAccel, - bool allowHwEncode, + int? threads, + int? qualityScale, + ProcessPriorityClass? priority, EncodingHelper encodingHelper, CancellationToken cancellationToken); diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 9b58f83b48..11f42c3f96 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -783,14 +783,17 @@ namespace MediaBrowser.MediaEncoding.Encoder string container, MediaSourceInfo mediaSource, MediaStream imageStream, - TimeSpan interval, int maxWidth, + TimeSpan interval, bool allowHwAccel, - bool allowHwEncode, + int? threads, + int? qualityScale, + ProcessPriorityClass? priority, EncodingHelper encodingHelper, CancellationToken cancellationToken) { var options = allowHwAccel ? _configurationManager.GetEncodingOptions() : new EncodingOptions(); + threads = threads ?? _threads; // A new EncodingOptions instance must be used as to not disable HW acceleration for all of Jellyfin. // Additionally, we must set a few fields without defaults to prevent null pointer exceptions. @@ -822,7 +825,7 @@ namespace MediaBrowser.MediaEncoding.Encoder if (!allowHwAccel) { - inputArg = "-threads " + _threads + " " + inputArg; // HW accel args set a different input thread count, only set if disabled + inputArg = "-threads " + threads + " " + inputArg; // HW accel args set a different input thread count, only set if disabled } var filterParam = encodingHelper.GetVideoProcessingFilterParam(jobState, options, jobState.OutputVideoCodec).Trim(); @@ -831,7 +834,7 @@ namespace MediaBrowser.MediaEncoding.Encoder throw new InvalidOperationException("EncodingHelper returned empty or invalid filter parameters."); } - return ExtractVideoImagesOnIntervalInternal(inputArg, filterParam, interval, vidEncoder, _threads, cancellationToken); + return ExtractVideoImagesOnIntervalInternal(inputArg, filterParam, interval, vidEncoder, threads, qualityScale, priority, cancellationToken); } private async Task<string> ExtractVideoImagesOnIntervalInternal( @@ -839,7 +842,9 @@ namespace MediaBrowser.MediaEncoding.Encoder string filterParam, TimeSpan interval, string vidEncoder, - int outputThreads, + int? outputThreads, + int? qualityScale, + ProcessPriorityClass? priority, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(inputArg)) @@ -857,10 +862,6 @@ namespace MediaBrowser.MediaEncoding.Encoder { filterParam = filterParam.Insert(filterParam.IndexOf("\"", StringComparison.Ordinal) + 1, fps + ","); } - else - { - filterParam += fps + ","; - } var targetDirectory = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid().ToString("N")); Directory.CreateDirectory(targetDirectory); @@ -869,11 +870,12 @@ namespace MediaBrowser.MediaEncoding.Encoder // Final command arguments var args = string.Format( CultureInfo.InvariantCulture, - "-loglevel error {0} -an -sn {1} -threads {2} -c:v {3} -f {4} \"{5}\"", + "-loglevel error {0} -an -sn {1} -threads {2} -c:v {3} {4}-f {5} \"{6}\"", inputArg, filterParam, - outputThreads, + outputThreads.GetValueOrDefault(_threads), vidEncoder, + qualityScale.HasValue ? "-qscale:v " + qualityScale.Value.ToString(CultureInfo.InvariantCulture) + " " : string.Empty, "image2", outputPath); @@ -904,6 +906,19 @@ namespace MediaBrowser.MediaEncoding.Encoder { StartProcess(processWrapper); + // Set process priority + if (priority.HasValue) + { + try + { + processWrapper.Process.PriorityClass = priority.Value; + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Unable to set process priority to {Priority} for {Description}", priority.Value, processDescription); + } + } + // Need to give ffmpeg enough time to make all the thumbnails, which could be a while, // but we still need to detect if the process hangs. // Making the assumption that as long as new jpegs are showing up, everything is good. diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 78a310f0b1..097eff295a 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -264,5 +264,7 @@ namespace MediaBrowser.Model.Configuration /// </summary> /// <value>The limit for parallel image encoding.</value> public int ParallelImageEncodingLimit { get; set; } + + public TrickplayOptions TrickplayOptions { get; set; } = new TrickplayOptions(); } } diff --git a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs index 87ac145d75..a364926c01 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs +++ b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs @@ -22,7 +22,6 @@ namespace MediaBrowser.Providers.Trickplay private readonly ILogger<TrickplayImagesTask> _logger; private readonly ILibraryManager _libraryManager; private readonly ILocalizationManager _localization; - private readonly IServerConfigurationManager _configurationManager; private readonly ITrickplayManager _trickplayManager; /// <summary> @@ -31,19 +30,16 @@ namespace MediaBrowser.Providers.Trickplay /// <param name="logger">The logger.</param> /// <param name="libraryManager">The library manager.</param> /// <param name="localization">The localization manager.</param> - /// <param name="configurationManager">The configuration manager.</param> /// <param name="trickplayManager">The trickplay manager.</param> public TrickplayImagesTask( ILogger<TrickplayImagesTask> logger, ILibraryManager libraryManager, ILocalizationManager localization, - IServerConfigurationManager configurationManager, ITrickplayManager trickplayManager) { _libraryManager = libraryManager; _logger = logger; _localization = localization; - _configurationManager = configurationManager; _trickplayManager = trickplayManager; } @@ -77,6 +73,14 @@ namespace MediaBrowser.Providers.Trickplay public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) { // TODO: libraryoptions dont run on libraries with trickplay disabled + /* will this still get all sub-items? should recursive be true? + * from chapterimagestask + * DtoOptions = new DtoOptions(false) + { + EnableImages = false + }, + SourceTypes = new SourceType[] { SourceType.Library }, + */ var items = _libraryManager.GetItemList(new InternalItemsQuery { MediaTypes = new[] { MediaType.Video }, diff --git a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs index cb916dfdbf..ed2c11281e 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs +++ b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs @@ -5,11 +5,13 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Trickplay; +using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; @@ -28,6 +30,7 @@ namespace MediaBrowser.Providers.Trickplay private readonly IFileSystem _fileSystem; private readonly EncodingHelper _encodingHelper; private readonly ILibraryManager _libraryManager; + private readonly IServerConfigurationManager _config; private static readonly SemaphoreSlim _resourcePool = new(1, 1); @@ -40,13 +43,15 @@ namespace MediaBrowser.Providers.Trickplay /// <param name="fileSystem">The file systen.</param> /// <param name="encodingHelper">The encoding helper.</param> /// <param name="libraryManager">The library manager.</param> + /// <param name="config">The server configuration manager.</param> public TrickplayManager( ILogger<TrickplayManager> logger, IItemRepository itemRepo, IMediaEncoder mediaEncoder, IFileSystem fileSystem, EncodingHelper encodingHelper, - ILibraryManager libraryManager) + ILibraryManager libraryManager, + IServerConfigurationManager config) { _logger = logger; _itemRepo = itemRepo; @@ -54,6 +59,7 @@ namespace MediaBrowser.Providers.Trickplay _fileSystem = fileSystem; _encodingHelper = encodingHelper; _libraryManager = libraryManager; + _config = config; } /// <inheritdoc /> @@ -61,16 +67,27 @@ namespace MediaBrowser.Providers.Trickplay { _logger.LogDebug("Trickplay refresh for {ItemId} (replace existing: {Replace})", video.Id, replace); - foreach (var width in new int[] { 320 } /*todo conf*/) + var options = _config.Configuration.TrickplayOptions; + foreach (var width in options.WidthResolutions) { cancellationToken.ThrowIfCancellationRequested(); - await RefreshTrickplayData(video, replace, width, 10000/*todo conf*/, 10/*todo conf*/, 10/*todo conf*/, true/*todo conf*/, true/*todo conf*/, cancellationToken).ConfigureAwait(false); + await RefreshTrickplayDataInternal( + video, + replace, + width, + options, + cancellationToken).ConfigureAwait(false); } } - private async Task RefreshTrickplayData(Video video, bool replace, int width, int interval, int tileWidth, int tileHeight, bool doHwAccel, bool doHwEncode, CancellationToken cancellationToken) + private async Task RefreshTrickplayDataInternal( + Video video, + bool replace, + int width, + TrickplayOptions options, + CancellationToken cancellationToken) { - if (!CanGenerateTrickplay(video, interval)) + if (!CanGenerateTrickplay(video, options.Interval)) { return; } @@ -108,10 +125,12 @@ namespace MediaBrowser.Providers.Trickplay container, mediaSource, mediaStream, - TimeSpan.FromMilliseconds(interval), width, - doHwAccel, - doHwEncode, + TimeSpan.FromMilliseconds(options.Interval), + options.EnableHwAcceleration, + options.ProcessThreads, + options.Qscale, + options.ProcessPriority, _encodingHelper, cancellationToken).ConfigureAwait(false); @@ -127,7 +146,7 @@ namespace MediaBrowser.Providers.Trickplay // Create tiles var tilesTempDir = Path.Combine(imgTempDir, Guid.NewGuid().ToString("N")); - var tilesInfo = CreateTiles(images, width, interval, tileWidth, tileHeight, 100/* todo _config.JpegQuality*/, tilesTempDir, outputDir); + var tilesInfo = CreateTiles(images, width, options, tilesTempDir, outputDir); // Save tiles info try @@ -166,7 +185,7 @@ namespace MediaBrowser.Providers.Trickplay } } - private TrickplayTilesInfo CreateTiles(List<FileSystemMetadata> images, int width, int interval, int tileWidth, int tileHeight, int quality, string workDir, string outputDir) + private TrickplayTilesInfo CreateTiles(List<FileSystemMetadata> images, int width, TrickplayOptions options, string workDir, string outputDir) { if (images.Count == 0) { @@ -178,9 +197,9 @@ namespace MediaBrowser.Providers.Trickplay var tilesInfo = new TrickplayTilesInfo { Width = width, - Interval = interval, - TileWidth = tileWidth, - TileHeight = tileHeight, + Interval = options.Interval, + TileWidth = options.TileWidth, + TileHeight = options.TileHeight, TileCount = 0, Bandwidth = 0 }; @@ -244,7 +263,7 @@ namespace MediaBrowser.Providers.Trickplay var tileGridPath = Path.Combine(workDir, $"{imgNo}.jpg"); using (var stream = File.OpenWrite(tileGridPath)) { - tileGrid.Encode(stream, SKEncodedImageFormat.Jpeg, quality); + tileGrid.Encode(stream, SKEncodedImageFormat.Jpeg, options.JpegQuality); } var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tileGridPath).Length * 8 / tilesInfo.TileWidth / tilesInfo.TileHeight / (tilesInfo.Interval / 1000)); @@ -351,7 +370,7 @@ namespace MediaBrowser.Providers.Trickplay { Directory.Move(source, destination); } - catch (System.IO.IOException) + catch (IOException) { // Cross device move requires a copy Directory.CreateDirectory(destination); diff --git a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs index e4bd9e3c20..e296467253 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs +++ b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs @@ -7,6 +7,7 @@ using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Trickplay; +using MediaBrowser.Model.Configuration; using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.Trickplay @@ -25,7 +26,7 @@ namespace MediaBrowser.Providers.Trickplay IForcedProvider { private readonly ILogger<TrickplayProvider> _logger; - private readonly IServerConfigurationManager _configurationManager; + private readonly IServerConfigurationManager _config; private readonly ITrickplayManager _trickplayManager; private readonly ILibraryManager _libraryManager; @@ -33,17 +34,17 @@ namespace MediaBrowser.Providers.Trickplay /// Initializes a new instance of the <see cref="TrickplayProvider"/> class. /// </summary> /// <param name="logger">The logger.</param> - /// <param name="configurationManager">The configuration manager.</param> + /// <param name="config">The configuration manager.</param> /// <param name="trickplayManager">The trickplay manager.</param> /// <param name="libraryManager">The library manager.</param> public TrickplayProvider( ILogger<TrickplayProvider> logger, - IServerConfigurationManager configurationManager, + IServerConfigurationManager config, ITrickplayManager trickplayManager, ILibraryManager libraryManager) { _logger = logger; - _configurationManager = configurationManager; + _config = config; _trickplayManager = trickplayManager; _libraryManager = libraryManager; } @@ -110,11 +111,14 @@ namespace MediaBrowser.Providers.Trickplay return ItemUpdateType.None; } - // TODO: this is always blocking for metadata collection, make non-blocking option - if (true) + if (_config.Configuration.TrickplayOptions.ScanBehavior == TrickplayScanBehavior.Blocking) { await _trickplayManager.RefreshTrickplayData(video, replace, cancellationToken).ConfigureAwait(false); } + else + { + _ = _trickplayManager.RefreshTrickplayData(video, replace, cancellationToken).ConfigureAwait(false); + } // The core doesn't need to trigger any save operations over this return ItemUpdateType.None; From d84370a6f72111b544f1bbcaa8f3b0339148b5f8 Mon Sep 17 00:00:00 2001 From: nicknsy <20588554+nicknsy@users.noreply.github.com> Date: Sat, 25 Feb 2023 20:36:38 -0800 Subject: [PATCH 379/858] Make trickplay response ids have no dashes --- Emby.Server.Implementations/Dto/DtoService.cs | 4 +++- MediaBrowser.Model/Dto/BaseItemDto.cs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 10352b6ff8..1687fa442b 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -1060,7 +1060,9 @@ namespace Emby.Server.Implementations.Dto if (options.ContainsField(ItemFields.Trickplay)) { - dto.Trickplay = _itemRepo.GetTrickplayManifest(item); + // To stay consistent with other fields, this must go from a Guid to a non-dashed string. + // This does not seem to occur automatically to dictionaries like it does with other Guid fields. + dto.Trickplay = _itemRepo.GetTrickplayManifest(item).ToDictionary(x => x.Key.ToString("N", CultureInfo.InvariantCulture), y => y.Value); } if (video.ExtraType.HasValue) diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs index ab424c6f5c..3db9cb08b5 100644 --- a/MediaBrowser.Model/Dto/BaseItemDto.cs +++ b/MediaBrowser.Model/Dto/BaseItemDto.cs @@ -572,7 +572,7 @@ namespace MediaBrowser.Model.Dto /// Gets or sets the trickplay manifest. /// </summary> /// <value>The trickplay manifest.</value> - public Dictionary<Guid, Dictionary<int, TrickplayTilesInfo>> Trickplay { get; set; } + public Dictionary<string, Dictionary<int, TrickplayTilesInfo>> Trickplay { get; set; } /// <summary> /// Gets or sets the type of the location. From 2e2085a212312282e20dcb6578503b6daff9b72e Mon Sep 17 00:00:00 2001 From: nicknsy <20588554+nicknsy@users.noreply.github.com> Date: Sat, 4 Mar 2023 16:19:09 -0800 Subject: [PATCH 380/858] HashSet datatype was causing default values to always be added on server start --- MediaBrowser.Model/Configuration/TrickplayOptions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Model/Configuration/TrickplayOptions.cs b/MediaBrowser.Model/Configuration/TrickplayOptions.cs index d527baaa46..f85259a14b 100644 --- a/MediaBrowser.Model/Configuration/TrickplayOptions.cs +++ b/MediaBrowser.Model/Configuration/TrickplayOptions.cs @@ -31,7 +31,7 @@ namespace MediaBrowser.Model.Configuration /// <summary> /// Gets or sets the target width resolutions, in px, to generates preview images for. /// </summary> - public HashSet<int> WidthResolutions { get; set; } = new HashSet<int> { 320 }; + public int[] WidthResolutions { get; set; } = new[] { 320 }; /// <summary> /// Gets or sets number of tile images to allow in X dimension. @@ -51,7 +51,7 @@ namespace MediaBrowser.Model.Configuration /// <summary> /// Gets or sets the jpeg quality to use for image tiles. /// </summary> - public int JpegQuality { get; set; } = 90; + public int JpegQuality { get; set; } = 80; /// <summary> /// Gets or sets the number of threads to be used by ffmpeg. From 79a0e36b90c9e195606cc21aad3967ae4335dcc0 Mon Sep 17 00:00:00 2001 From: nicknsy <20588554+nicknsy@users.noreply.github.com> Date: Mon, 13 Mar 2023 00:21:36 -0700 Subject: [PATCH 381/858] Remove max runtime --- MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs index a364926c01..fa50f80b8c 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs +++ b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs @@ -63,8 +63,7 @@ namespace MediaBrowser.Providers.Trickplay new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerDaily, - TimeOfDayTicks = TimeSpan.FromHours(3).Ticks, - MaxRuntimeTicks = TimeSpan.FromHours(5).Ticks + TimeOfDayTicks = TimeSpan.FromHours(3).Ticks } }; } From 0f053f0fe0970e30a253f9b3d2db264eee6a6ae5 Mon Sep 17 00:00:00 2001 From: nicknsy <20588554+nicknsy@users.noreply.github.com> Date: Mon, 13 Mar 2023 00:32:48 -0700 Subject: [PATCH 382/858] Change generation task to search recursively --- MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs index fa50f80b8c..50166ca39f 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs +++ b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs @@ -85,7 +85,7 @@ namespace MediaBrowser.Providers.Trickplay MediaTypes = new[] { MediaType.Video }, IsVirtualItem = false, IsFolder = false, - Recursive = false + Recursive = true }).OfType<Video>().ToList(); var numComplete = 0; From fe1c9d43ce350c76ab6afcd5f54e260487c0ff22 Mon Sep 17 00:00:00 2001 From: Nick <20588554+nicknsy@users.noreply.github.com> Date: Tue, 28 Mar 2023 12:34:49 -0700 Subject: [PATCH 383/858] Fix using order --- Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index 6508dd8cf9..b1657aeae2 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -9,8 +9,8 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Extensions; using Jellyfin.Api.Models.StreamingDtos; -using Jellyfin.Extensions; using Jellyfin.Data.Entities; +using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; From d7fd54197c98346844b53860b280f0a6e65f6180 Mon Sep 17 00:00:00 2001 From: Nick <20588554+nicknsy@users.noreply.github.com> Date: Tue, 28 Mar 2023 12:45:41 -0700 Subject: [PATCH 384/858] Task localization --- Emby.Server.Implementations/Localization/Core/en-US.json | 2 ++ MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs | 9 --------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/en-US.json b/Emby.Server.Implementations/Localization/Core/en-US.json index 15088384cc..496ecabd37 100644 --- a/Emby.Server.Implementations/Localization/Core/en-US.json +++ b/Emby.Server.Implementations/Localization/Core/en-US.json @@ -112,6 +112,8 @@ "TaskCleanLogsDescription": "Deletes log files that are more than {0} days old.", "TaskRefreshPeople": "Refresh People", "TaskRefreshPeopleDescription": "Updates metadata for actors and directors in your media library.", + "TaskRefreshTrickplayImages": "Generate Trickplay Images", + "TaskRefreshTrickplayImagesDescription": "Creates trickplay previews for videos in enabled libraries.", "TaskUpdatePlugins": "Update Plugins", "TaskUpdatePluginsDescription": "Downloads and installs updates for plugins that are configured to update automatically.", "TaskCleanTranscode": "Clean Transcode Directory", diff --git a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs index 50166ca39f..8d0d9d5a34 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs +++ b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs @@ -71,15 +71,6 @@ namespace MediaBrowser.Providers.Trickplay /// <inheritdoc /> public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) { - // TODO: libraryoptions dont run on libraries with trickplay disabled - /* will this still get all sub-items? should recursive be true? - * from chapterimagestask - * DtoOptions = new DtoOptions(false) - { - EnableImages = false - }, - SourceTypes = new SourceType[] { SourceType.Library }, - */ var items = _libraryManager.GetItemList(new InternalItemsQuery { MediaTypes = new[] { MediaType.Video }, From e8ca278266df095884172787c5611642c5a88db3 Mon Sep 17 00:00:00 2001 From: Nick <20588554+nicknsy@users.noreply.github.com> Date: Tue, 28 Mar 2023 12:52:18 -0700 Subject: [PATCH 385/858] Add names to contributors --- CONTRIBUTORS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index dfb61df0a1..009610c41d 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -57,6 +57,7 @@ - [hawken93](https://github.com/hawken93) - [HelloWorld017](https://github.com/HelloWorld017) - [ikomhoog](https://github.com/ikomhoog) + - [iwalton3](https://github.com/iwalton3) - [jftuga](https://github.com/jftuga) - [jmshrv](https://github.com/jmshrv) - [joern-h](https://github.com/joern-h) @@ -88,6 +89,7 @@ - [neilsb](https://github.com/neilsb) - [nevado](https://github.com/nevado) - [Nickbert7](https://github.com/Nickbert7) + - [nicknsy](https://github.com/nicknsy) - [nvllsvm](https://github.com/nvllsvm) - [nyanmisaka](https://github.com/nyanmisaka) - [OancaAndrei](https://github.com/OancaAndrei) From b89bf5d735de144495b3dc9acbcf70c17fb24c15 Mon Sep 17 00:00:00 2001 From: Nick <20588554+nicknsy@users.noreply.github.com> Date: Tue, 28 Mar 2023 13:09:34 -0700 Subject: [PATCH 386/858] Change defaults --- MediaBrowser.Model/Configuration/TrickplayOptions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Model/Configuration/TrickplayOptions.cs b/MediaBrowser.Model/Configuration/TrickplayOptions.cs index f85259a14b..d89e5f590c 100644 --- a/MediaBrowser.Model/Configuration/TrickplayOptions.cs +++ b/MediaBrowser.Model/Configuration/TrickplayOptions.cs @@ -46,12 +46,12 @@ namespace MediaBrowser.Model.Configuration /// <summary> /// Gets or sets the ffmpeg output quality level. /// </summary> - public int Qscale { get; set; } = 10; + public int Qscale { get; set; } = 4; /// <summary> /// Gets or sets the jpeg quality to use for image tiles. /// </summary> - public int JpegQuality { get; set; } = 80; + public int JpegQuality { get; set; } = 90; /// <summary> /// Gets or sets the number of threads to be used by ffmpeg. From dd8ef08592830236b31307e2424b491e974f024a Mon Sep 17 00:00:00 2001 From: Nick <20588554+nicknsy@users.noreply.github.com> Date: Wed, 29 Mar 2023 16:43:17 -0700 Subject: [PATCH 387/858] Move fps filter to GetVideoProcessingFilterParam --- .../MediaEncoding/EncodingHelper.cs | 9 +++++++++ .../Encoder/MediaEncoder.cs | 19 ++++--------------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 0889a90f48..bcdf2934a2 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -4806,6 +4806,15 @@ namespace MediaBrowser.Controller.MediaEncoding subFilters?.RemoveAll(filter => string.IsNullOrEmpty(filter)); overlayFilters?.RemoveAll(filter => string.IsNullOrEmpty(filter)); + var framerate = GetFramerateParam(state); + if (framerate.HasValue) + { + mainFilters.Insert(0, string.Format( + CultureInfo.InvariantCulture, + "fps={0}", + framerate.Value)); + } + var mainStr = string.Empty; if (mainFilters?.Count > 0) { diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 11f42c3f96..4692bf5048 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -804,7 +804,7 @@ namespace MediaBrowser.MediaEncoding.Encoder options.EnableTonemapping = false; } - var baseRequest = new BaseEncodingJobOptions { MaxWidth = maxWidth }; + var baseRequest = new BaseEncodingJobOptions { MaxWidth = maxWidth, MaxFramerate = (float)(1.0 / interval.TotalSeconds) }; var jobState = new EncodingJobInfo(TranscodingJobType.Progressive) { IsVideoRequest = true, // must be true for InputVideoHwaccelArgs to return non-empty value @@ -829,18 +829,17 @@ namespace MediaBrowser.MediaEncoding.Encoder } var filterParam = encodingHelper.GetVideoProcessingFilterParam(jobState, options, jobState.OutputVideoCodec).Trim(); - if (string.IsNullOrWhiteSpace(filterParam) || filterParam.IndexOf("\"", StringComparison.Ordinal) == -1) + if (string.IsNullOrWhiteSpace(filterParam)) { throw new InvalidOperationException("EncodingHelper returned empty or invalid filter parameters."); } - return ExtractVideoImagesOnIntervalInternal(inputArg, filterParam, interval, vidEncoder, threads, qualityScale, priority, cancellationToken); + return ExtractVideoImagesOnIntervalInternal(inputArg, filterParam, vidEncoder, threads, qualityScale, priority, cancellationToken); } private async Task<string> ExtractVideoImagesOnIntervalInternal( string inputArg, string filterParam, - TimeSpan interval, string vidEncoder, int? outputThreads, int? qualityScale, @@ -853,16 +852,6 @@ namespace MediaBrowser.MediaEncoding.Encoder } // Output arguments - string fps = "fps=1/" + interval.TotalSeconds.ToString(CultureInfo.InvariantCulture); - if (string.IsNullOrWhiteSpace(filterParam)) - { - filterParam = "-vf \"" + fps + "\""; - } - else if (filterParam.IndexOf("\"", StringComparison.Ordinal) != -1) - { - filterParam = filterParam.Insert(filterParam.IndexOf("\"", StringComparison.Ordinal) + 1, fps + ","); - } - var targetDirectory = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid().ToString("N")); Directory.CreateDirectory(targetDirectory); var outputPath = Path.Combine(targetDirectory, "%08d.jpg"); @@ -895,7 +884,7 @@ namespace MediaBrowser.MediaEncoding.Encoder }; var processDescription = string.Format(CultureInfo.InvariantCulture, "{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments); - _logger.LogDebug("{ProcessDescription}", processDescription); + _logger.LogInformation("Trickplay generation: {ProcessDescription}", processDescription); using (var processWrapper = new ProcessWrapper(process, this)) { From 33770322282326304b4b8073f583d6fed2354c0b Mon Sep 17 00:00:00 2001 From: Nick <20588554+nicknsy@users.noreply.github.com> Date: Mon, 1 May 2023 12:51:05 -0700 Subject: [PATCH 388/858] crobibero styling, format, code suggestions --- .../MediaEncoding/EncodingHelper.cs | 27 +- .../Trickplay/ITrickplayManager.cs | 77 +- .../Encoder/MediaEncoder.cs | 2 +- .../Configuration/TrickplayOptions.cs | 89 ++- .../Configuration/TrickplayScanBehavior.cs | 25 +- .../Entities/TrickplayTilesInfo.cs | 79 ++- .../Trickplay/TrickplayImagesTask.cs | 167 +++-- .../Trickplay/TrickplayManager.cs | 657 +++++++++--------- .../Trickplay/TrickplayProvider.cs | 199 +++--- 9 files changed, 655 insertions(+), 667 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index bcdf2934a2..01b6e31e9a 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -90,6 +90,13 @@ namespace MediaBrowser.Controller.MediaEncoding { "truehd", 6 }, }; + private static readonly string _defaultMjpegEncoder = "mjpeg"; + private static readonly Dictionary<string, string> _mjpegCodecMap = new(StringComparer.OrdinalIgnoreCase) + { + { "vaapi", _defaultMjpegEncoder + "_vaapi" }, + { "qsv", _defaultMjpegEncoder + "_qsv" } + }; + public static readonly string[] LosslessAudioCodecs = new string[] { "alac", @@ -151,32 +158,20 @@ namespace MediaBrowser.Controller.MediaEncoding private string GetMjpegEncoder(EncodingJobInfo state, EncodingOptions encodingOptions) { - var defaultEncoder = "mjpeg"; - if (state.VideoType == VideoType.VideoFile) { var hwType = encodingOptions.HardwareAccelerationType; - var codecMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) - { - { "vaapi", defaultEncoder + "_vaapi" }, - { "qsv", defaultEncoder + "_qsv" } - }; - if (!string.IsNullOrEmpty(hwType) && encodingOptions.EnableHardwareEncoding - && codecMap.ContainsKey(hwType)) + && _mjpegCodecMap.TryGetValue(hwType, out var preferredEncoder) + && _mediaEncoder.SupportsEncoder(preferredEncoder)) { - var preferredEncoder = codecMap[hwType]; - - if (_mediaEncoder.SupportsEncoder(preferredEncoder)) - { - return preferredEncoder; - } + return preferredEncoder; } } - return defaultEncoder; + return _defaultMjpegEncoder; } private bool IsVaapiSupported(EncodingJobInfo state) diff --git a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs index bae458f98c..8e82c57d4c 100644 --- a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs +++ b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs @@ -5,50 +5,49 @@ using System.Threading.Tasks; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Entities; -namespace MediaBrowser.Controller.Trickplay +namespace MediaBrowser.Controller.Trickplay; + +/// <summary> +/// Interface ITrickplayManager. +/// </summary> +public interface ITrickplayManager { /// <summary> - /// Interface ITrickplayManager. + /// Generate or replace trickplay data. /// </summary> - public interface ITrickplayManager - { - /// <summary> - /// Generate or replace trickplay data. - /// </summary> - /// <param name="video">The video.</param> - /// <param name="replace">Whether or not existing data should be replaced.</param> - /// <param name="cancellationToken">CancellationToken to use for operation.</param> - /// <returns>Task.</returns> - Task RefreshTrickplayData(Video video, bool replace, CancellationToken cancellationToken); + /// <param name="video">The video.</param> + /// <param name="replace">Whether or not existing data should be replaced.</param> + /// <param name="cancellationToken">CancellationToken to use for operation.</param> + /// <returns>Task.</returns> + Task RefreshTrickplayDataAsync(Video video, bool replace, CancellationToken cancellationToken); - /// <summary> - /// Get available trickplay resolutions and corresponding info. - /// </summary> - /// <param name="itemId">The item.</param> - /// <returns>Map of width resolutions to trickplay tiles info.</returns> - Dictionary<int, TrickplayTilesInfo> GetTilesResolutions(Guid itemId); + /// <summary> + /// Get available trickplay resolutions and corresponding info. + /// </summary> + /// <param name="itemId">The item.</param> + /// <returns>Map of width resolutions to trickplay tiles info.</returns> + Dictionary<int, TrickplayTilesInfo> GetTilesResolutions(Guid itemId); - /// <summary> - /// Saves trickplay tiles info. - /// </summary> - /// <param name="itemId">The item.</param> - /// <param name="tilesInfo">The trickplay tiles info.</param> - void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo); + /// <summary> + /// Saves trickplay tiles info. + /// </summary> + /// <param name="itemId">The item.</param> + /// <param name="tilesInfo">The trickplay tiles info.</param> + void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo); - /// <summary> - /// Gets the trickplay manifest. - /// </summary> - /// <param name="item">The item.</param> - /// <returns>A map of media source id to a map of tile width to tile info.</returns> - Dictionary<Guid, Dictionary<int, TrickplayTilesInfo>> GetTrickplayManifest(BaseItem item); + /// <summary> + /// Gets the trickplay manifest. + /// </summary> + /// <param name="item">The item.</param> + /// <returns>A map of media source id to a map of tile width to tile info.</returns> + Dictionary<Guid, Dictionary<int, TrickplayTilesInfo>> GetTrickplayManifest(BaseItem item); - /// <summary> - /// Gets the path to a trickplay tiles image. - /// </summary> - /// <param name="item">The item.</param> - /// <param name="width">The width of a single tile.</param> - /// <param name="index">The tile grid's index.</param> - /// <returns>The absolute path.</returns> - string GetTrickplayTilePath(BaseItem item, int width, int index); - } + /// <summary> + /// Gets the path to a trickplay tiles image. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="width">The width of a single tile.</param> + /// <param name="index">The tile grid's index.</param> + /// <returns>The absolute path.</returns> + string GetTrickplayTilePath(BaseItem item, int width, int index); } diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 4692bf5048..000831fe2d 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -793,7 +793,7 @@ namespace MediaBrowser.MediaEncoding.Encoder CancellationToken cancellationToken) { var options = allowHwAccel ? _configurationManager.GetEncodingOptions() : new EncodingOptions(); - threads = threads ?? _threads; + threads ??= _threads; // A new EncodingOptions instance must be used as to not disable HW acceleration for all of Jellyfin. // Additionally, we must set a few fields without defaults to prevent null pointer exceptions. diff --git a/MediaBrowser.Model/Configuration/TrickplayOptions.cs b/MediaBrowser.Model/Configuration/TrickplayOptions.cs index d89e5f590c..1fff1a5ed5 100644 --- a/MediaBrowser.Model/Configuration/TrickplayOptions.cs +++ b/MediaBrowser.Model/Configuration/TrickplayOptions.cs @@ -1,61 +1,60 @@ using System.Collections.Generic; using System.Diagnostics; -namespace MediaBrowser.Model.Configuration +namespace MediaBrowser.Model.Configuration; + +/// <summary> +/// Class TrickplayOptions. +/// </summary> +public class TrickplayOptions { /// <summary> - /// Class TrickplayOptions. + /// Gets or sets a value indicating whether or not to use HW acceleration. /// </summary> - public class TrickplayOptions - { - /// <summary> - /// Gets or sets a value indicating whether or not to use HW acceleration. - /// </summary> - public bool EnableHwAcceleration { get; set; } = false; + public bool EnableHwAcceleration { get; set; } = false; - /// <summary> - /// Gets or sets the behavior used by trickplay provider on library scan/update. - /// </summary> - public TrickplayScanBehavior ScanBehavior { get; set; } = TrickplayScanBehavior.NonBlocking; + /// <summary> + /// Gets or sets the behavior used by trickplay provider on library scan/update. + /// </summary> + public TrickplayScanBehavior ScanBehavior { get; set; } = TrickplayScanBehavior.NonBlocking; - /// <summary> - /// Gets or sets the process priority for the ffmpeg process. - /// </summary> - public ProcessPriorityClass ProcessPriority { get; set; } = ProcessPriorityClass.BelowNormal; + /// <summary> + /// Gets or sets the process priority for the ffmpeg process. + /// </summary> + public ProcessPriorityClass ProcessPriority { get; set; } = ProcessPriorityClass.BelowNormal; - /// <summary> - /// Gets or sets the interval, in ms, between each new trickplay image. - /// </summary> - public int Interval { get; set; } = 10000; + /// <summary> + /// Gets or sets the interval, in ms, between each new trickplay image. + /// </summary> + public int Interval { get; set; } = 10000; - /// <summary> - /// Gets or sets the target width resolutions, in px, to generates preview images for. - /// </summary> - public int[] WidthResolutions { get; set; } = new[] { 320 }; + /// <summary> + /// Gets or sets the target width resolutions, in px, to generates preview images for. + /// </summary> + public int[] WidthResolutions { get; set; } = new[] { 320 }; - /// <summary> - /// Gets or sets number of tile images to allow in X dimension. - /// </summary> - public int TileWidth { get; set; } = 10; + /// <summary> + /// Gets or sets number of tile images to allow in X dimension. + /// </summary> + public int TileWidth { get; set; } = 10; - /// <summary> - /// Gets or sets number of tile images to allow in Y dimension. - /// </summary> - public int TileHeight { get; set; } = 10; + /// <summary> + /// Gets or sets number of tile images to allow in Y dimension. + /// </summary> + public int TileHeight { get; set; } = 10; - /// <summary> - /// Gets or sets the ffmpeg output quality level. - /// </summary> - public int Qscale { get; set; } = 4; + /// <summary> + /// Gets or sets the ffmpeg output quality level. + /// </summary> + public int Qscale { get; set; } = 4; - /// <summary> - /// Gets or sets the jpeg quality to use for image tiles. - /// </summary> - public int JpegQuality { get; set; } = 90; + /// <summary> + /// Gets or sets the jpeg quality to use for image tiles. + /// </summary> + public int JpegQuality { get; set; } = 90; - /// <summary> - /// Gets or sets the number of threads to be used by ffmpeg. - /// </summary> - public int ProcessThreads { get; set; } = 0; - } + /// <summary> + /// Gets or sets the number of threads to be used by ffmpeg. + /// </summary> + public int ProcessThreads { get; set; } = 0; } diff --git a/MediaBrowser.Model/Configuration/TrickplayScanBehavior.cs b/MediaBrowser.Model/Configuration/TrickplayScanBehavior.cs index 7997941768..d0db532185 100644 --- a/MediaBrowser.Model/Configuration/TrickplayScanBehavior.cs +++ b/MediaBrowser.Model/Configuration/TrickplayScanBehavior.cs @@ -1,18 +1,17 @@ -namespace MediaBrowser.Model.Configuration +namespace MediaBrowser.Model.Configuration; + +/// <summary> +/// Enum TrickplayScanBehavior. +/// </summary> +public enum TrickplayScanBehavior { /// <summary> - /// Enum TrickplayScanBehavior. + /// Starts generation, only return once complete. /// </summary> - public enum TrickplayScanBehavior - { - /// <summary> - /// Starts generation, only return once complete. - /// </summary> - Blocking, + Blocking, - /// <summary> - /// Start generation, return immediately. - /// </summary> - NonBlocking - } + /// <summary> + /// Start generation, return immediately. + /// </summary> + NonBlocking } diff --git a/MediaBrowser.Model/Entities/TrickplayTilesInfo.cs b/MediaBrowser.Model/Entities/TrickplayTilesInfo.cs index 84b6b03228..86d37787f3 100644 --- a/MediaBrowser.Model/Entities/TrickplayTilesInfo.cs +++ b/MediaBrowser.Model/Entities/TrickplayTilesInfo.cs @@ -1,50 +1,49 @@ -namespace MediaBrowser.Model.Entities +namespace MediaBrowser.Model.Entities; + +/// <summary> +/// Class TrickplayTilesInfo. +/// </summary> +public class TrickplayTilesInfo { /// <summary> - /// Class TrickplayTilesInfo. + /// Gets or sets width of an individual tile. /// </summary> - public class TrickplayTilesInfo - { - /// <summary> - /// Gets or sets width of an individual tile. - /// </summary> - /// <value>The width.</value> - public int Width { get; set; } + /// <value>The width.</value> + public int Width { get; set; } - /// <summary> - /// Gets or sets height of an individual tile. - /// </summary> - /// <value>The height.</value> - public int Height { get; set; } + /// <summary> + /// Gets or sets height of an individual tile. + /// </summary> + /// <value>The height.</value> + public int Height { get; set; } - /// <summary> - /// Gets or sets amount of tiles per row. - /// </summary> - /// <value>The tile grid's width.</value> - public int TileWidth { get; set; } + /// <summary> + /// Gets or sets amount of tiles per row. + /// </summary> + /// <value>The tile grid's width.</value> + public int TileWidth { get; set; } - /// <summary> - /// Gets or sets amount of tiles per column. - /// </summary> - /// <value>The tile grid's height.</value> - public int TileHeight { get; set; } + /// <summary> + /// Gets or sets amount of tiles per column. + /// </summary> + /// <value>The tile grid's height.</value> + public int TileHeight { get; set; } - /// <summary> - /// Gets or sets total amount of non-black tiles. - /// </summary> - /// <value>The tile count.</value> - public int TileCount { get; set; } + /// <summary> + /// Gets or sets total amount of non-black tiles. + /// </summary> + /// <value>The tile count.</value> + public int TileCount { get; set; } - /// <summary> - /// Gets or sets interval in milliseconds between each trickplay tile. - /// </summary> - /// <value>The interval.</value> - public int Interval { get; set; } + /// <summary> + /// Gets or sets interval in milliseconds between each trickplay tile. + /// </summary> + /// <value>The interval.</value> + public int Interval { get; set; } - /// <summary> - /// Gets or sets peak bandwith usage in bits per second. - /// </summary> - /// <value>The bandwidth.</value> - public int Bandwidth { get; set; } - } + /// <summary> + /// Gets or sets peak bandwith usage in bits per second. + /// </summary> + /// <value>The bandwidth.</value> + public int Bandwidth { get; set; } } diff --git a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs index 8d0d9d5a34..f32557cd12 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs +++ b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs @@ -12,98 +12,97 @@ using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; -namespace MediaBrowser.Providers.Trickplay +namespace MediaBrowser.Providers.Trickplay; + +/// <summary> +/// Class TrickplayImagesTask. +/// </summary> +public class TrickplayImagesTask : IScheduledTask { + private readonly ILogger<TrickplayImagesTask> _logger; + private readonly ILibraryManager _libraryManager; + private readonly ILocalizationManager _localization; + private readonly ITrickplayManager _trickplayManager; + /// <summary> - /// Class TrickplayImagesTask. + /// Initializes a new instance of the <see cref="TrickplayImagesTask"/> class. /// </summary> - public class TrickplayImagesTask : IScheduledTask + /// <param name="logger">The logger.</param> + /// <param name="libraryManager">The library manager.</param> + /// <param name="localization">The localization manager.</param> + /// <param name="trickplayManager">The trickplay manager.</param> + public TrickplayImagesTask( + ILogger<TrickplayImagesTask> logger, + ILibraryManager libraryManager, + ILocalizationManager localization, + ITrickplayManager trickplayManager) { - private readonly ILogger<TrickplayImagesTask> _logger; - private readonly ILibraryManager _libraryManager; - private readonly ILocalizationManager _localization; - private readonly ITrickplayManager _trickplayManager; + _libraryManager = libraryManager; + _logger = logger; + _localization = localization; + _trickplayManager = trickplayManager; + } - /// <summary> - /// Initializes a new instance of the <see cref="TrickplayImagesTask"/> class. - /// </summary> - /// <param name="logger">The logger.</param> - /// <param name="libraryManager">The library manager.</param> - /// <param name="localization">The localization manager.</param> - /// <param name="trickplayManager">The trickplay manager.</param> - public TrickplayImagesTask( - ILogger<TrickplayImagesTask> logger, - ILibraryManager libraryManager, - ILocalizationManager localization, - ITrickplayManager trickplayManager) + /// <inheritdoc /> + public string Name => _localization.GetLocalizedString("TaskRefreshTrickplayImages"); + + /// <inheritdoc /> + public string Description => _localization.GetLocalizedString("TaskRefreshTrickplayImagesDescription"); + + /// <inheritdoc /> + public string Key => "RefreshTrickplayImages"; + + /// <inheritdoc /> + public string Category => _localization.GetLocalizedString("TasksLibraryCategory"); + + /// <inheritdoc /> + public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() + { + return new[] { - _libraryManager = libraryManager; - _logger = logger; - _localization = localization; - _trickplayManager = trickplayManager; - } - - /// <inheritdoc /> - public string Name => _localization.GetLocalizedString("TaskRefreshTrickplayImages"); - - /// <inheritdoc /> - public string Description => _localization.GetLocalizedString("TaskRefreshTrickplayImagesDescription"); - - /// <inheritdoc /> - public string Key => "RefreshTrickplayImages"; - - /// <inheritdoc /> - public string Category => _localization.GetLocalizedString("TasksLibraryCategory"); - - /// <inheritdoc /> - public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() - { - return new[] + new TaskTriggerInfo { - new TaskTriggerInfo - { - Type = TaskTriggerInfo.TriggerDaily, - TimeOfDayTicks = TimeSpan.FromHours(3).Ticks - } - }; - } - - /// <inheritdoc /> - public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) - { - var items = _libraryManager.GetItemList(new InternalItemsQuery - { - MediaTypes = new[] { MediaType.Video }, - IsVirtualItem = false, - IsFolder = false, - Recursive = true - }).OfType<Video>().ToList(); - - var numComplete = 0; - - foreach (var item in items) - { - try - { - cancellationToken.ThrowIfCancellationRequested(); - await _trickplayManager.RefreshTrickplayData(item, false, cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - break; - } - catch (Exception ex) - { - _logger.LogError("Error creating trickplay files for {ItemName}: {Msg}", item.Name, ex); - } - - numComplete++; - double percent = numComplete; - percent /= items.Count; - percent *= 100; - - progress.Report(percent); + Type = TaskTriggerInfo.TriggerDaily, + TimeOfDayTicks = TimeSpan.FromHours(3).Ticks } + }; + } + + /// <inheritdoc /> + public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) + { + var items = _libraryManager.GetItemList(new InternalItemsQuery + { + MediaTypes = new[] { MediaType.Video }, + IsVirtualItem = false, + IsFolder = false, + Recursive = true + }).OfType<Video>().ToList(); + + var numComplete = 0; + + foreach (var item in items) + { + try + { + cancellationToken.ThrowIfCancellationRequested(); + await _trickplayManager.RefreshTrickplayDataAsync(item, false, cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + break; + } + catch (Exception ex) + { + _logger.LogError("Error creating trickplay files for {ItemName}: {Msg}", item.Name, ex); + } + + numComplete++; + double percent = numComplete; + percent /= items.Count; + percent *= 100; + + progress.Report(percent); } } } diff --git a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs index ed2c11281e..9b8eb81501 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs +++ b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs @@ -17,370 +17,369 @@ using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; using SkiaSharp; -namespace MediaBrowser.Providers.Trickplay +namespace MediaBrowser.Providers.Trickplay; + +/// <summary> +/// ITrickplayManager implementation. +/// </summary> +public class TrickplayManager : ITrickplayManager { + private readonly ILogger<TrickplayManager> _logger; + private readonly IItemRepository _itemRepo; + private readonly IMediaEncoder _mediaEncoder; + private readonly IFileSystem _fileSystem; + private readonly EncodingHelper _encodingHelper; + private readonly ILibraryManager _libraryManager; + private readonly IServerConfigurationManager _config; + + private static readonly SemaphoreSlim _resourcePool = new(1, 1); + /// <summary> - /// ITrickplayManager implementation. + /// Initializes a new instance of the <see cref="TrickplayManager"/> class. /// </summary> - public class TrickplayManager : ITrickplayManager + /// <param name="logger">The logger.</param> + /// <param name="itemRepo">The item repository.</param> + /// <param name="mediaEncoder">The media encoder.</param> + /// <param name="fileSystem">The file systen.</param> + /// <param name="encodingHelper">The encoding helper.</param> + /// <param name="libraryManager">The library manager.</param> + /// <param name="config">The server configuration manager.</param> + public TrickplayManager( + ILogger<TrickplayManager> logger, + IItemRepository itemRepo, + IMediaEncoder mediaEncoder, + IFileSystem fileSystem, + EncodingHelper encodingHelper, + ILibraryManager libraryManager, + IServerConfigurationManager config) { - private readonly ILogger<TrickplayManager> _logger; - private readonly IItemRepository _itemRepo; - private readonly IMediaEncoder _mediaEncoder; - private readonly IFileSystem _fileSystem; - private readonly EncodingHelper _encodingHelper; - private readonly ILibraryManager _libraryManager; - private readonly IServerConfigurationManager _config; + _logger = logger; + _itemRepo = itemRepo; + _mediaEncoder = mediaEncoder; + _fileSystem = fileSystem; + _encodingHelper = encodingHelper; + _libraryManager = libraryManager; + _config = config; + } - private static readonly SemaphoreSlim _resourcePool = new(1, 1); + /// <inheritdoc /> + public async Task RefreshTrickplayDataAsync(Video video, bool replace, CancellationToken cancellationToken) + { + _logger.LogDebug("Trickplay refresh for {ItemId} (replace existing: {Replace})", video.Id, replace); - /// <summary> - /// Initializes a new instance of the <see cref="TrickplayManager"/> class. - /// </summary> - /// <param name="logger">The logger.</param> - /// <param name="itemRepo">The item repository.</param> - /// <param name="mediaEncoder">The media encoder.</param> - /// <param name="fileSystem">The file systen.</param> - /// <param name="encodingHelper">The encoding helper.</param> - /// <param name="libraryManager">The library manager.</param> - /// <param name="config">The server configuration manager.</param> - public TrickplayManager( - ILogger<TrickplayManager> logger, - IItemRepository itemRepo, - IMediaEncoder mediaEncoder, - IFileSystem fileSystem, - EncodingHelper encodingHelper, - ILibraryManager libraryManager, - IServerConfigurationManager config) + var options = _config.Configuration.TrickplayOptions; + foreach (var width in options.WidthResolutions) { - _logger = logger; - _itemRepo = itemRepo; - _mediaEncoder = mediaEncoder; - _fileSystem = fileSystem; - _encodingHelper = encodingHelper; - _libraryManager = libraryManager; - _config = config; + cancellationToken.ThrowIfCancellationRequested(); + await RefreshTrickplayDataInternal( + video, + replace, + width, + options, + cancellationToken).ConfigureAwait(false); + } + } + + private async Task RefreshTrickplayDataInternal( + Video video, + bool replace, + int width, + TrickplayOptions options, + CancellationToken cancellationToken) + { + if (!CanGenerateTrickplay(video, options.Interval)) + { + return; } - /// <inheritdoc /> - public async Task RefreshTrickplayData(Video video, bool replace, CancellationToken cancellationToken) - { - _logger.LogDebug("Trickplay refresh for {ItemId} (replace existing: {Replace})", video.Id, replace); + var imgTempDir = string.Empty; + var outputDir = GetTrickplayDirectory(video, width); - var options = _config.Configuration.TrickplayOptions; - foreach (var width in options.WidthResolutions) - { - cancellationToken.ThrowIfCancellationRequested(); - await RefreshTrickplayDataInternal( - video, - replace, - width, - options, - cancellationToken).ConfigureAwait(false); - } - } - - private async Task RefreshTrickplayDataInternal( - Video video, - bool replace, - int width, - TrickplayOptions options, - CancellationToken cancellationToken) + try { - if (!CanGenerateTrickplay(video, options.Interval)) + await _resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); + + if (!replace && Directory.Exists(outputDir) && GetTilesResolutions(video.Id).ContainsKey(width)) { + _logger.LogDebug("Found existing trickplay files for {ItemId}. Exiting.", video.Id); return; } - var imgTempDir = string.Empty; - var outputDir = GetTrickplayDirectory(video, width); + // Extract images + // Note: Media sources under parent items exist as their own video/item as well. Only use this video stream for trickplay. + var mediaSource = video.GetMediaSources(false).Find(source => Guid.Parse(source.Id).Equals(video.Id)); + if (mediaSource is null) + { + _logger.LogDebug("Found no matching media source for item {ItemId}", video.Id); + return; + } + + var mediaPath = mediaSource.Path; + var mediaStream = mediaSource.VideoStream; + var container = mediaSource.Container; + + _logger.LogInformation("Creating trickplay files at {Width} width, for {Path} [ID: {ItemId}]", width, mediaPath, video.Id); + imgTempDir = await _mediaEncoder.ExtractVideoImagesOnIntervalAccelerated( + mediaPath, + container, + mediaSource, + mediaStream, + width, + TimeSpan.FromMilliseconds(options.Interval), + options.EnableHwAcceleration, + options.ProcessThreads, + options.Qscale, + options.ProcessPriority, + _encodingHelper, + cancellationToken).ConfigureAwait(false); + + if (string.IsNullOrEmpty(imgTempDir) || !Directory.Exists(imgTempDir)) + { + throw new InvalidOperationException("Null or invalid directory from media encoder."); + } + + var images = _fileSystem.GetFiles(imgTempDir, new string[] { ".jpg" }, false, false) + .Where(img => string.Equals(img.Extension, ".jpg", StringComparison.Ordinal)) + .OrderBy(i => i.FullName) + .ToList(); + + // Create tiles + var tilesTempDir = Path.Combine(imgTempDir, Guid.NewGuid().ToString("N")); + var tilesInfo = CreateTiles(images, width, options, tilesTempDir, outputDir); + + // Save tiles info try { - await _resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); - - if (!replace && Directory.Exists(outputDir) && GetTilesResolutions(video.Id).ContainsKey(width)) + if (tilesInfo is not null) { - _logger.LogDebug("Found existing trickplay files for {ItemId}. Exiting.", video.Id); - return; + SaveTilesInfo(video.Id, tilesInfo); + _logger.LogInformation("Finished creation of trickplay files for {0}", mediaPath); } - - // Extract images - // Note: Media sources under parent items exist as their own video/item as well. Only use this video stream for trickplay. - var mediaSource = video.GetMediaSources(false).Find(source => Guid.Parse(source.Id).Equals(video.Id)); - - if (mediaSource is null) + else { - _logger.LogDebug("Found no matching media source for item {ItemId}", video.Id); - return; - } - - var mediaPath = mediaSource.Path; - var mediaStream = mediaSource.VideoStream; - var container = mediaSource.Container; - - _logger.LogInformation("Creating trickplay files at {Width} width, for {Path} [ID: {ItemId}]", width, mediaPath, video.Id); - imgTempDir = await _mediaEncoder.ExtractVideoImagesOnIntervalAccelerated( - mediaPath, - container, - mediaSource, - mediaStream, - width, - TimeSpan.FromMilliseconds(options.Interval), - options.EnableHwAcceleration, - options.ProcessThreads, - options.Qscale, - options.ProcessPriority, - _encodingHelper, - cancellationToken).ConfigureAwait(false); - - if (string.IsNullOrEmpty(imgTempDir) || !Directory.Exists(imgTempDir)) - { - throw new InvalidOperationException("Null or invalid directory from media encoder."); - } - - var images = _fileSystem.GetFiles(imgTempDir, new string[] { ".jpg" }, false, false) - .Where(img => string.Equals(img.Extension, ".jpg", StringComparison.Ordinal)) - .OrderBy(i => i.FullName) - .ToList(); - - // Create tiles - var tilesTempDir = Path.Combine(imgTempDir, Guid.NewGuid().ToString("N")); - var tilesInfo = CreateTiles(images, width, options, tilesTempDir, outputDir); - - // Save tiles info - try - { - if (tilesInfo is not null) - { - SaveTilesInfo(video.Id, tilesInfo); - _logger.LogInformation("Finished creation of trickplay files for {0}", mediaPath); - } - else - { - throw new InvalidOperationException("Null trickplay tiles info from CreateTiles."); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error while saving trickplay tiles info."); - - // Make sure no files stay in metadata folders on failure - // if tiles info wasn't saved. - Directory.Delete(outputDir, true); + throw new InvalidOperationException("Null trickplay tiles info from CreateTiles."); } } catch (Exception ex) { - _logger.LogError(ex, "Error creating trickplay images."); - } - finally - { - _resourcePool.Release(); + _logger.LogError(ex, "Error while saving trickplay tiles info."); - if (!string.IsNullOrEmpty(imgTempDir)) - { - Directory.Delete(imgTempDir, true); - } - } - } - - private TrickplayTilesInfo CreateTiles(List<FileSystemMetadata> images, int width, TrickplayOptions options, string workDir, string outputDir) - { - if (images.Count == 0) - { - throw new InvalidOperationException("Can't create trickplay from 0 images."); - } - - Directory.CreateDirectory(workDir); - - var tilesInfo = new TrickplayTilesInfo - { - Width = width, - Interval = options.Interval, - TileWidth = options.TileWidth, - TileHeight = options.TileHeight, - TileCount = 0, - Bandwidth = 0 - }; - - var firstImg = SKBitmap.Decode(images[0].FullName); - if (firstImg == null) - { - throw new InvalidDataException("Could not decode image data."); - } - - tilesInfo.Height = firstImg.Height; - if (tilesInfo.Width != firstImg.Width) - { - throw new InvalidOperationException("Image width does not match config width."); - } - - /* - * Generate grids of trickplay image tiles - */ - var imgNo = 0; - var i = 0; - while (i < images.Count) - { - var tileGrid = new SKBitmap(tilesInfo.Width * tilesInfo.TileWidth, tilesInfo.Height * tilesInfo.TileHeight); - - using (var canvas = new SKCanvas(tileGrid)) - { - for (var y = 0; y < tilesInfo.TileHeight; y++) - { - for (var x = 0; x < tilesInfo.TileWidth; x++) - { - if (i >= images.Count) - { - break; - } - - var img = SKBitmap.Decode(images[i].FullName); - if (img == null) - { - throw new InvalidDataException("Could not decode image data."); - } - - if (tilesInfo.Width != img.Width) - { - throw new InvalidOperationException("Image width does not match config width."); - } - - if (tilesInfo.Height != img.Height) - { - throw new InvalidOperationException("Image height does not match first image height."); - } - - canvas.DrawBitmap(img, x * tilesInfo.Width, y * tilesInfo.Height); - tilesInfo.TileCount++; - i++; - } - } - } - - // Output each tile grid to singular file - var tileGridPath = Path.Combine(workDir, $"{imgNo}.jpg"); - using (var stream = File.OpenWrite(tileGridPath)) - { - tileGrid.Encode(stream, SKEncodedImageFormat.Jpeg, options.JpegQuality); - } - - var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tileGridPath).Length * 8 / tilesInfo.TileWidth / tilesInfo.TileHeight / (tilesInfo.Interval / 1000)); - tilesInfo.Bandwidth = Math.Max(tilesInfo.Bandwidth, bitrate); - - imgNo++; - } - - /* - * Move trickplay tiles to output directory - */ - Directory.CreateDirectory(outputDir); - - // Replace existing tile grids if they already exist - if (Directory.Exists(outputDir)) - { + // Make sure no files stay in metadata folders on failure + // if tiles info wasn't saved. Directory.Delete(outputDir, true); } - - MoveDirectory(workDir, outputDir); - - return tilesInfo; } - - private bool CanGenerateTrickplay(Video video, int interval) + catch (Exception ex) { - var videoType = video.VideoType; - if (videoType == VideoType.Iso || videoType == VideoType.Dvd || videoType == VideoType.BluRay) - { - return false; - } - - if (video.IsPlaceHolder) - { - return false; - } - - if (video.IsShortcut) - { - return false; - } - - if (!video.IsCompleteMedia) - { - return false; - } - - if (!video.RunTimeTicks.HasValue || video.RunTimeTicks.Value < TimeSpan.FromMilliseconds(interval).Ticks) - { - return false; - } - - var libraryOptions = _libraryManager.GetLibraryOptions(video); - if (libraryOptions is not null) - { - if (!libraryOptions.EnableTrickplayImageExtraction) - { - return false; - } - } - else - { - return false; - } - - // Can't extract images if there are no video streams - return video.GetMediaStreams().Count > 0; + _logger.LogError(ex, "Error creating trickplay images."); } - - /// <inheritdoc /> - public Dictionary<int, TrickplayTilesInfo> GetTilesResolutions(Guid itemId) + finally { - return _itemRepo.GetTilesResolutions(itemId); - } + _resourcePool.Release(); - /// <inheritdoc /> - public void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo) - { - _itemRepo.SaveTilesInfo(itemId, tilesInfo); - } - - /// <inheritdoc /> - public Dictionary<Guid, Dictionary<int, TrickplayTilesInfo>> GetTrickplayManifest(BaseItem item) - { - return _itemRepo.GetTrickplayManifest(item); - } - - /// <inheritdoc /> - public string GetTrickplayTilePath(BaseItem item, int width, int index) - { - return Path.Combine(GetTrickplayDirectory(item, width), index + ".jpg"); - } - - private string GetTrickplayDirectory(BaseItem item, int? width = null) - { - var path = Path.Combine(item.GetInternalMetadataPath(), "trickplay"); - - return width.HasValue ? Path.Combine(path, width.Value.ToString(CultureInfo.InvariantCulture)) : path; - } - - private void MoveDirectory(string source, string destination) - { - try + if (!string.IsNullOrEmpty(imgTempDir)) { - Directory.Move(source, destination); - } - catch (IOException) - { - // Cross device move requires a copy - Directory.CreateDirectory(destination); - foreach (string file in Directory.GetFiles(source)) - { - File.Copy(file, Path.Join(destination, Path.GetFileName(file)), true); - } - - Directory.Delete(source, true); + Directory.Delete(imgTempDir, true); } } } + + private TrickplayTilesInfo CreateTiles(List<FileSystemMetadata> images, int width, TrickplayOptions options, string workDir, string outputDir) + { + if (images.Count == 0) + { + throw new InvalidOperationException("Can't create trickplay from 0 images."); + } + + Directory.CreateDirectory(workDir); + + var tilesInfo = new TrickplayTilesInfo + { + Width = width, + Interval = options.Interval, + TileWidth = options.TileWidth, + TileHeight = options.TileHeight, + TileCount = 0, + Bandwidth = 0 + }; + + var firstImg = SKBitmap.Decode(images[0].FullName); + if (firstImg == null) + { + throw new InvalidDataException("Could not decode image data."); + } + + tilesInfo.Height = firstImg.Height; + if (tilesInfo.Width != firstImg.Width) + { + throw new InvalidOperationException("Image width does not match config width."); + } + + /* + * Generate grids of trickplay image tiles + */ + var imgNo = 0; + var i = 0; + while (i < images.Count) + { + var tileGrid = new SKBitmap(tilesInfo.Width * tilesInfo.TileWidth, tilesInfo.Height * tilesInfo.TileHeight); + + using (var canvas = new SKCanvas(tileGrid)) + { + for (var y = 0; y < tilesInfo.TileHeight; y++) + { + for (var x = 0; x < tilesInfo.TileWidth; x++) + { + if (i >= images.Count) + { + break; + } + + var img = SKBitmap.Decode(images[i].FullName); + if (img == null) + { + throw new InvalidDataException("Could not decode image data."); + } + + if (tilesInfo.Width != img.Width) + { + throw new InvalidOperationException("Image width does not match config width."); + } + + if (tilesInfo.Height != img.Height) + { + throw new InvalidOperationException("Image height does not match first image height."); + } + + canvas.DrawBitmap(img, x * tilesInfo.Width, y * tilesInfo.Height); + tilesInfo.TileCount++; + i++; + } + } + } + + // Output each tile grid to singular file + var tileGridPath = Path.Combine(workDir, $"{imgNo}.jpg"); + using (var stream = File.OpenWrite(tileGridPath)) + { + tileGrid.Encode(stream, SKEncodedImageFormat.Jpeg, options.JpegQuality); + } + + var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tileGridPath).Length * 8 / tilesInfo.TileWidth / tilesInfo.TileHeight / (tilesInfo.Interval / 1000)); + tilesInfo.Bandwidth = Math.Max(tilesInfo.Bandwidth, bitrate); + + imgNo++; + } + + /* + * Move trickplay tiles to output directory + */ + Directory.CreateDirectory(outputDir); + + // Replace existing tile grids if they already exist + if (Directory.Exists(outputDir)) + { + Directory.Delete(outputDir, true); + } + + MoveDirectory(workDir, outputDir); + + return tilesInfo; + } + + private bool CanGenerateTrickplay(Video video, int interval) + { + var videoType = video.VideoType; + if (videoType == VideoType.Iso || videoType == VideoType.Dvd || videoType == VideoType.BluRay) + { + return false; + } + + if (video.IsPlaceHolder) + { + return false; + } + + if (video.IsShortcut) + { + return false; + } + + if (!video.IsCompleteMedia) + { + return false; + } + + if (!video.RunTimeTicks.HasValue || video.RunTimeTicks.Value < TimeSpan.FromMilliseconds(interval).Ticks) + { + return false; + } + + var libraryOptions = _libraryManager.GetLibraryOptions(video); + if (libraryOptions is not null) + { + if (!libraryOptions.EnableTrickplayImageExtraction) + { + return false; + } + } + else + { + return false; + } + + // Can't extract images if there are no video streams + return video.GetMediaStreams().Count > 0; + } + + /// <inheritdoc /> + public Dictionary<int, TrickplayTilesInfo> GetTilesResolutions(Guid itemId) + { + return _itemRepo.GetTilesResolutions(itemId); + } + + /// <inheritdoc /> + public void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo) + { + _itemRepo.SaveTilesInfo(itemId, tilesInfo); + } + + /// <inheritdoc /> + public Dictionary<Guid, Dictionary<int, TrickplayTilesInfo>> GetTrickplayManifest(BaseItem item) + { + return _itemRepo.GetTrickplayManifest(item); + } + + /// <inheritdoc /> + public string GetTrickplayTilePath(BaseItem item, int width, int index) + { + return Path.Combine(GetTrickplayDirectory(item, width), index + ".jpg"); + } + + private string GetTrickplayDirectory(BaseItem item, int? width = null) + { + var path = Path.Combine(item.GetInternalMetadataPath(), "trickplay"); + + return width.HasValue ? Path.Combine(path, width.Value.ToString(CultureInfo.InvariantCulture)) : path; + } + + private void MoveDirectory(string source, string destination) + { + try + { + Directory.Move(source, destination); + } + catch (IOException) + { + // Cross device move requires a copy + Directory.CreateDirectory(destination); + foreach (string file in Directory.GetFiles(source)) + { + File.Copy(file, Path.Join(destination, Path.GetFileName(file)), true); + } + + Directory.Delete(source, true); + } + } } diff --git a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs index e296467253..d467c480ea 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs +++ b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs @@ -10,118 +10,117 @@ using MediaBrowser.Controller.Trickplay; using MediaBrowser.Model.Configuration; using Microsoft.Extensions.Logging; -namespace MediaBrowser.Providers.Trickplay +namespace MediaBrowser.Providers.Trickplay; + +/// <summary> +/// Class TrickplayProvider. Provides images and metadata for trickplay +/// scrubbing previews. +/// </summary> +public class TrickplayProvider : ICustomMetadataProvider<Episode>, + ICustomMetadataProvider<MusicVideo>, + ICustomMetadataProvider<Movie>, + ICustomMetadataProvider<Trailer>, + ICustomMetadataProvider<Video>, + IHasItemChangeMonitor, + IHasOrder, + IForcedProvider { + private readonly ILogger<TrickplayProvider> _logger; + private readonly IServerConfigurationManager _config; + private readonly ITrickplayManager _trickplayManager; + private readonly ILibraryManager _libraryManager; + /// <summary> - /// Class TrickplayProvider. Provides images and metadata for trickplay - /// scrubbing previews. + /// Initializes a new instance of the <see cref="TrickplayProvider"/> class. /// </summary> - public class TrickplayProvider : ICustomMetadataProvider<Episode>, - ICustomMetadataProvider<MusicVideo>, - ICustomMetadataProvider<Movie>, - ICustomMetadataProvider<Trailer>, - ICustomMetadataProvider<Video>, - IHasItemChangeMonitor, - IHasOrder, - IForcedProvider + /// <param name="logger">The logger.</param> + /// <param name="config">The configuration manager.</param> + /// <param name="trickplayManager">The trickplay manager.</param> + /// <param name="libraryManager">The library manager.</param> + public TrickplayProvider( + ILogger<TrickplayProvider> logger, + IServerConfigurationManager config, + ITrickplayManager trickplayManager, + ILibraryManager libraryManager) { - private readonly ILogger<TrickplayProvider> _logger; - private readonly IServerConfigurationManager _config; - private readonly ITrickplayManager _trickplayManager; - private readonly ILibraryManager _libraryManager; + _logger = logger; + _config = config; + _trickplayManager = trickplayManager; + _libraryManager = libraryManager; + } - /// <summary> - /// Initializes a new instance of the <see cref="TrickplayProvider"/> class. - /// </summary> - /// <param name="logger">The logger.</param> - /// <param name="config">The configuration manager.</param> - /// <param name="trickplayManager">The trickplay manager.</param> - /// <param name="libraryManager">The library manager.</param> - public TrickplayProvider( - ILogger<TrickplayProvider> logger, - IServerConfigurationManager config, - ITrickplayManager trickplayManager, - ILibraryManager libraryManager) + /// <inheritdoc /> + public string Name => "Trickplay Provider"; + + /// <inheritdoc /> + public int Order => 100; + + /// <inheritdoc /> + public bool HasChanged(BaseItem item, IDirectoryService directoryService) + { + if (item.IsFileProtocol) { - _logger = logger; - _config = config; - _trickplayManager = trickplayManager; - _libraryManager = libraryManager; - } - - /// <inheritdoc /> - public string Name => "Trickplay Provider"; - - /// <inheritdoc /> - public int Order => 100; - - /// <inheritdoc /> - public bool HasChanged(BaseItem item, IDirectoryService directoryService) - { - if (item.IsFileProtocol) + var file = directoryService.GetFile(item.Path); + if (file is not null && item.DateModified != file.LastWriteTimeUtc) { - var file = directoryService.GetFile(item.Path); - if (file is not null && item.DateModified != file.LastWriteTimeUtc) - { - return true; - } + return true; } - - return false; } - /// <inheritdoc /> - public Task<ItemUpdateType> FetchAsync(Episode item, MetadataRefreshOptions options, CancellationToken cancellationToken) + return false; + } + + /// <inheritdoc /> + public Task<ItemUpdateType> FetchAsync(Episode item, MetadataRefreshOptions options, CancellationToken cancellationToken) + { + return FetchInternal(item, options, cancellationToken); + } + + /// <inheritdoc /> + public Task<ItemUpdateType> FetchAsync(MusicVideo item, MetadataRefreshOptions options, CancellationToken cancellationToken) + { + return FetchInternal(item, options, cancellationToken); + } + + /// <inheritdoc /> + public Task<ItemUpdateType> FetchAsync(Movie item, MetadataRefreshOptions options, CancellationToken cancellationToken) + { + return FetchInternal(item, options, cancellationToken); + } + + /// <inheritdoc /> + public Task<ItemUpdateType> FetchAsync(Trailer item, MetadataRefreshOptions options, CancellationToken cancellationToken) + { + return FetchInternal(item, options, cancellationToken); + } + + /// <inheritdoc /> + public Task<ItemUpdateType> FetchAsync(Video item, MetadataRefreshOptions options, CancellationToken cancellationToken) + { + return FetchInternal(item, options, cancellationToken); + } + + private async Task<ItemUpdateType> FetchInternal(Video video, MetadataRefreshOptions options, CancellationToken cancellationToken) + { + var libraryOptions = _libraryManager.GetLibraryOptions(video); + bool? enableDuringScan = libraryOptions?.ExtractTrickplayImagesDuringLibraryScan; + bool replace = options.ReplaceAllImages; + + if (options.IsAutomated && !enableDuringScan.GetValueOrDefault(false)) { - return FetchInternal(item, options, cancellationToken); - } - - /// <inheritdoc /> - public Task<ItemUpdateType> FetchAsync(MusicVideo item, MetadataRefreshOptions options, CancellationToken cancellationToken) - { - return FetchInternal(item, options, cancellationToken); - } - - /// <inheritdoc /> - public Task<ItemUpdateType> FetchAsync(Movie item, MetadataRefreshOptions options, CancellationToken cancellationToken) - { - return FetchInternal(item, options, cancellationToken); - } - - /// <inheritdoc /> - public Task<ItemUpdateType> FetchAsync(Trailer item, MetadataRefreshOptions options, CancellationToken cancellationToken) - { - return FetchInternal(item, options, cancellationToken); - } - - /// <inheritdoc /> - public Task<ItemUpdateType> FetchAsync(Video item, MetadataRefreshOptions options, CancellationToken cancellationToken) - { - return FetchInternal(item, options, cancellationToken); - } - - private async Task<ItemUpdateType> FetchInternal(Video video, MetadataRefreshOptions options, CancellationToken cancellationToken) - { - var libraryOptions = _libraryManager.GetLibraryOptions(video); - bool? enableDuringScan = libraryOptions?.ExtractTrickplayImagesDuringLibraryScan; - bool replace = options.ReplaceAllImages; - - if (options.IsAutomated && !enableDuringScan.GetValueOrDefault(false)) - { - return ItemUpdateType.None; - } - - if (_config.Configuration.TrickplayOptions.ScanBehavior == TrickplayScanBehavior.Blocking) - { - await _trickplayManager.RefreshTrickplayData(video, replace, cancellationToken).ConfigureAwait(false); - } - else - { - _ = _trickplayManager.RefreshTrickplayData(video, replace, cancellationToken).ConfigureAwait(false); - } - - // The core doesn't need to trigger any save operations over this return ItemUpdateType.None; } + + if (_config.Configuration.TrickplayOptions.ScanBehavior == TrickplayScanBehavior.Blocking) + { + await _trickplayManager.RefreshTrickplayDataAsync(video, replace, cancellationToken).ConfigureAwait(false); + } + else + { + _ = _trickplayManager.RefreshTrickplayDataAsync(video, replace, cancellationToken).ConfigureAwait(false); + } + + // The core doesn't need to trigger any save operations over this + return ItemUpdateType.None; } } From 07e6804f7a892514487334066a24661b395aab3d Mon Sep 17 00:00:00 2001 From: Nick <20588554+nicknsy@users.noreply.github.com> Date: Wed, 17 May 2023 22:46:30 -0700 Subject: [PATCH 389/858] Change default threads to 1 --- MediaBrowser.Model/Configuration/TrickplayOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Model/Configuration/TrickplayOptions.cs b/MediaBrowser.Model/Configuration/TrickplayOptions.cs index 1fff1a5ed5..92c16ee84f 100644 --- a/MediaBrowser.Model/Configuration/TrickplayOptions.cs +++ b/MediaBrowser.Model/Configuration/TrickplayOptions.cs @@ -56,5 +56,5 @@ public class TrickplayOptions /// <summary> /// Gets or sets the number of threads to be used by ffmpeg. /// </summary> - public int ProcessThreads { get; set; } = 0; + public int ProcessThreads { get; set; } = 1; } From 98e41d5a14a579113f354ae3cb32a9ff6bc41958 Mon Sep 17 00:00:00 2001 From: Nick <20588554+nicknsy@users.noreply.github.com> Date: Wed, 17 May 2023 23:25:52 -0700 Subject: [PATCH 390/858] Styling, format, minor code changes (crobibero) --- .../Controllers/TrickplayController.cs | 53 ++++++++----------- .../Trickplay/TrickplayImagesTask.cs | 2 +- .../Trickplay/TrickplayManager.cs | 8 +-- .../Trickplay/TrickplayProvider.cs | 4 -- 4 files changed, 26 insertions(+), 41 deletions(-) diff --git a/Jellyfin.Api/Controllers/TrickplayController.cs b/Jellyfin.Api/Controllers/TrickplayController.cs index 46289f1701..6dee023427 100644 --- a/Jellyfin.Api/Controllers/TrickplayController.cs +++ b/Jellyfin.Api/Controllers/TrickplayController.cs @@ -6,18 +6,14 @@ using System.IO; using System.Linq; using System.Net.Mime; using System.Text; -using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Extensions; -using Jellyfin.Api.Helpers; -using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Trickplay; using MediaBrowser.Model; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; namespace Jellyfin.Api.Controllers; @@ -28,26 +24,18 @@ namespace Jellyfin.Api.Controllers; [Authorize] public class TrickplayController : BaseJellyfinApiController { - private readonly ILogger<TrickplayController> _logger; - private readonly IHttpContextAccessor _httpContextAccessor; private readonly ILibraryManager _libraryManager; private readonly ITrickplayManager _trickplayManager; /// <summary> /// Initializes a new instance of the <see cref="TrickplayController"/> class. /// </summary> - /// <param name="logger">Instance of the <see cref="ILogger{TrickplayController}"/> interface.</param> - /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param> /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/>.</param> /// <param name="trickplayManager">Instance of <see cref="ITrickplayManager"/>.</param> public TrickplayController( - ILogger<TrickplayController> logger, - IHttpContextAccessor httpContextAccessor, ILibraryManager libraryManager, ITrickplayManager trickplayManager) { - _logger = logger; - _httpContextAccessor = httpContextAccessor; _libraryManager = libraryManager; _trickplayManager = trickplayManager; } @@ -66,9 +54,9 @@ public class TrickplayController : BaseJellyfinApiController public ActionResult GetTrickplayHlsPlaylist( [FromRoute, Required] Guid itemId, [FromRoute, Required] int width, - [FromQuery] string? mediaSourceId) + [FromQuery] Guid? mediaSourceId) { - return GetTrickplayPlaylistInternal(width, mediaSourceId ?? itemId.ToString("N")); + return GetTrickplayPlaylistInternal(width, mediaSourceId ?? itemId); } /// <summary> @@ -89,9 +77,9 @@ public class TrickplayController : BaseJellyfinApiController [FromRoute, Required] Guid itemId, [FromRoute, Required] int width, [FromRoute, Required] int index, - [FromQuery] string? mediaSourceId) + [FromQuery] Guid? mediaSourceId) { - var item = _libraryManager.GetItemById(mediaSourceId ?? itemId.ToString("N")); + var item = _libraryManager.GetItemById(mediaSourceId ?? itemId); if (item is null) { return NotFound(); @@ -106,28 +94,22 @@ public class TrickplayController : BaseJellyfinApiController return NotFound(); } - private ActionResult GetTrickplayPlaylistInternal(int width, string mediaSourceId) + private ActionResult GetTrickplayPlaylistInternal(int width, Guid mediaSourceId) { - if (_httpContextAccessor.HttpContext is null) - { - throw new ResourceNotFoundException(nameof(_httpContextAccessor.HttpContext)); - } - - var tilesResolutions = _trickplayManager.GetTilesResolutions(Guid.Parse(mediaSourceId)); - if (tilesResolutions is not null && tilesResolutions.ContainsKey(width)) + var tilesResolutions = _trickplayManager.GetTilesResolutions(mediaSourceId); + if (tilesResolutions is not null && tilesResolutions.TryGetValue(width, out var tilesInfo)) { var builder = new StringBuilder(128); - var tilesInfo = tilesResolutions[width]; if (tilesInfo.TileCount > 0) { const string urlFormat = "Trickplay/{0}/{1}.jpg?MediaSourceId={2}&api_key={3}"; const string decimalFormat = "{0:0.###}"; - var resolution = tilesInfo.Width.ToString(CultureInfo.InvariantCulture) + "x" + tilesInfo.Height.ToString(CultureInfo.InvariantCulture); - var layout = tilesInfo.TileWidth.ToString(CultureInfo.InvariantCulture) + "x" + tilesInfo.TileHeight.ToString(CultureInfo.InvariantCulture); + var resolution = $"{tilesInfo.Width}x{tilesInfo.Height}"; + var layout = $"{tilesInfo.TileWidth}x{tilesInfo.TileHeight}"; var tilesPerGrid = tilesInfo.TileWidth * tilesInfo.TileHeight; - var tileDuration = (decimal)tilesInfo.Interval / 1000; + var tileDuration = tilesInfo.Interval / 1000m; var infDuration = tileDuration * tilesPerGrid; var tileGridCount = (int)Math.Ceiling((decimal)tilesInfo.TileCount / tilesPerGrid); @@ -153,15 +135,22 @@ public class TrickplayController : BaseJellyfinApiController urlFormat, width.ToString(CultureInfo.InvariantCulture), i.ToString(CultureInfo.InvariantCulture), - mediaSourceId, - _httpContextAccessor.HttpContext.User.GetToken()); + mediaSourceId.ToString("N"), + User.GetToken()); // EXTINF - builder.Append("#EXTINF:").Append(string.Format(CultureInfo.InvariantCulture, decimalFormat, infDuration)) + builder + .Append("#EXTINF:") + .Append(string.Format(CultureInfo.InvariantCulture, decimalFormat, infDuration)) .AppendLine(","); // EXT-X-TILES - builder.Append("#EXT-X-TILES:RESOLUTION=").Append(resolution).Append(",LAYOUT=").Append(layout).Append(",DURATION=") + builder + .Append("#EXT-X-TILES:RESOLUTION=") + .Append(resolution) + .Append(",LAYOUT=") + .Append(layout) + .Append(",DURATION=") .AppendLine(string.Format(CultureInfo.InvariantCulture, decimalFormat, tileDuration)); // URL diff --git a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs index f32557cd12..8ac7641aae 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs +++ b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs @@ -94,7 +94,7 @@ public class TrickplayImagesTask : IScheduledTask } catch (Exception ex) { - _logger.LogError("Error creating trickplay files for {ItemName}: {Msg}", item.Name, ex); + _logger.LogError(ex, "Error creating trickplay files for {ItemName}", item.Name); } numComplete++; diff --git a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs index 9b8eb81501..d377d2d802 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs +++ b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs @@ -33,6 +33,7 @@ public class TrickplayManager : ITrickplayManager private readonly IServerConfigurationManager _config; private static readonly SemaphoreSlim _resourcePool = new(1, 1); + private static readonly string[] _trickplayImgExtensions = { ".jpg" }; /// <summary> /// Initializes a new instance of the <see cref="TrickplayManager"/> class. @@ -95,10 +96,10 @@ public class TrickplayManager : ITrickplayManager var imgTempDir = string.Empty; var outputDir = GetTrickplayDirectory(video, width); + await _resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); + try { - await _resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); - if (!replace && Directory.Exists(outputDir) && GetTilesResolutions(video.Id).ContainsKey(width)) { _logger.LogDebug("Found existing trickplay files for {ItemId}. Exiting.", video.Id); @@ -139,8 +140,7 @@ public class TrickplayManager : ITrickplayManager throw new InvalidOperationException("Null or invalid directory from media encoder."); } - var images = _fileSystem.GetFiles(imgTempDir, new string[] { ".jpg" }, false, false) - .Where(img => string.Equals(img.Extension, ".jpg", StringComparison.Ordinal)) + var images = _fileSystem.GetFiles(imgTempDir, _trickplayImgExtensions, false, false) .OrderBy(i => i.FullName) .ToList(); diff --git a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs index d467c480ea..17e9efdde5 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs +++ b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs @@ -25,7 +25,6 @@ public class TrickplayProvider : ICustomMetadataProvider<Episode>, IHasOrder, IForcedProvider { - private readonly ILogger<TrickplayProvider> _logger; private readonly IServerConfigurationManager _config; private readonly ITrickplayManager _trickplayManager; private readonly ILibraryManager _libraryManager; @@ -33,17 +32,14 @@ public class TrickplayProvider : ICustomMetadataProvider<Episode>, /// <summary> /// Initializes a new instance of the <see cref="TrickplayProvider"/> class. /// </summary> - /// <param name="logger">The logger.</param> /// <param name="config">The configuration manager.</param> /// <param name="trickplayManager">The trickplay manager.</param> /// <param name="libraryManager">The library manager.</param> public TrickplayProvider( - ILogger<TrickplayProvider> logger, IServerConfigurationManager config, ITrickplayManager trickplayManager, ILibraryManager libraryManager) { - _logger = logger; _config = config; _trickplayManager = trickplayManager; _libraryManager = libraryManager; From d338253242f3b7996e735b94eacfa9d67ed0913a Mon Sep 17 00:00:00 2001 From: Nick <20588554+nicknsy@users.noreply.github.com> Date: Thu, 18 May 2023 00:11:08 -0700 Subject: [PATCH 391/858] Fix styling for string builder --- Jellyfin.Api/Controllers/TrickplayController.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Jellyfin.Api/Controllers/TrickplayController.cs b/Jellyfin.Api/Controllers/TrickplayController.cs index 6dee023427..e9ec6a6f48 100644 --- a/Jellyfin.Api/Controllers/TrickplayController.cs +++ b/Jellyfin.Api/Controllers/TrickplayController.cs @@ -113,12 +113,14 @@ public class TrickplayController : BaseJellyfinApiController var infDuration = tileDuration * tilesPerGrid; var tileGridCount = (int)Math.Ceiling((decimal)tilesInfo.TileCount / tilesPerGrid); - builder.AppendLine("#EXTM3U"); - builder.Append("#EXT-X-TARGETDURATION:").AppendLine(tileGridCount.ToString(CultureInfo.InvariantCulture)); - builder.AppendLine("#EXT-X-VERSION:7"); - builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:1"); - builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD"); - builder.AppendLine("#EXT-X-IMAGES-ONLY"); + builder + .AppendLine("#EXTM3U") + .Append("#EXT-X-TARGETDURATION:") + .AppendLine(tileGridCount.ToString(CultureInfo.InvariantCulture)) + .AppendLine("#EXT-X-VERSION:7") + .AppendLine("#EXT-X-MEDIA-SEQUENCE:1") + .AppendLine("#EXT-X-PLAYLIST-TYPE:VOD") + .AppendLine("#EXT-X-IMAGES-ONLY"); for (int i = 0; i < tileGridCount; i++) { From f82af0478122d668294affecd21f89fcec69a61d Mon Sep 17 00:00:00 2001 From: Nick <20588554+nicknsy@users.noreply.github.com> Date: Thu, 18 May 2023 00:45:12 -0700 Subject: [PATCH 392/858] Trickplay task pagination --- .../Trickplay/TrickplayImagesTask.cs | 52 +++++++++++-------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs index 8ac7641aae..b090745bf1 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs +++ b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Trickplay; @@ -11,6 +10,7 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; +using TagLib.Ape; namespace MediaBrowser.Providers.Trickplay; @@ -19,6 +19,8 @@ namespace MediaBrowser.Providers.Trickplay; /// </summary> public class TrickplayImagesTask : IScheduledTask { + private const int QueryPageLimit = 100; + private readonly ILogger<TrickplayImagesTask> _logger; private readonly ILibraryManager _libraryManager; private readonly ILocalizationManager _localization; @@ -71,38 +73,46 @@ public class TrickplayImagesTask : IScheduledTask /// <inheritdoc /> public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) { - var items = _libraryManager.GetItemList(new InternalItemsQuery + var query = new InternalItemsQuery { MediaTypes = new[] { MediaType.Video }, + SourceTypes = new[] { SourceType.Library }, IsVirtualItem = false, IsFolder = false, - Recursive = true - }).OfType<Video>().ToList(); + Recursive = true, + Limit = QueryPageLimit + }; + var numberOfVideos = _libraryManager.GetCount(query); + + var startIndex = 0; var numComplete = 0; - foreach (var item in items) + while (startIndex < numberOfVideos) { - try + query.StartIndex = startIndex; + var videos = _libraryManager.GetItemList(query).OfType<Video>().ToList(); + + foreach (var video in videos) { cancellationToken.ThrowIfCancellationRequested(); - await _trickplayManager.RefreshTrickplayDataAsync(item, false, cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - break; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error creating trickplay files for {ItemName}", item.Name); + + try + { + await _trickplayManager.RefreshTrickplayDataAsync(video, false, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating trickplay files for {ItemName}", video.Name); + } + + numComplete++; + progress.Report(100d * numComplete / numberOfVideos); } - numComplete++; - double percent = numComplete; - percent /= items.Count; - percent *= 100; - - progress.Report(percent); + startIndex += QueryPageLimit; } + + progress.Report(100); } } From a9594cd8b47bece2e9732cc7addb6b118dd130a4 Mon Sep 17 00:00:00 2001 From: Nick <20588554+nicknsy@users.noreply.github.com> Date: Thu, 18 May 2023 15:32:15 -0700 Subject: [PATCH 393/858] Minor code change --- MediaBrowser.Providers/Trickplay/TrickplayManager.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs index d377d2d802..2304f803ec 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs +++ b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs @@ -317,14 +317,7 @@ public class TrickplayManager : ITrickplayManager } var libraryOptions = _libraryManager.GetLibraryOptions(video); - if (libraryOptions is not null) - { - if (!libraryOptions.EnableTrickplayImageExtraction) - { - return false; - } - } - else + if (libraryOptions is null || !libraryOptions.EnableTrickplayImageExtraction) { return false; } From 049361b66cefe7cb26364f9c39ac57abc7826752 Mon Sep 17 00:00:00 2001 From: Nick <20588554+nicknsy@users.noreply.github.com> Date: Wed, 24 May 2023 14:41:38 -0700 Subject: [PATCH 394/858] TrickplayController return 404 if playlist doesn't exist. Minor code style/format changes (crobibero) --- .../Controllers/TrickplayController.cs | 31 +++++++++---------- .../Trickplay/TrickplayProvider.cs | 1 - 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/Jellyfin.Api/Controllers/TrickplayController.cs b/Jellyfin.Api/Controllers/TrickplayController.cs index e9ec6a6f48..ac71eff199 100644 --- a/Jellyfin.Api/Controllers/TrickplayController.cs +++ b/Jellyfin.Api/Controllers/TrickplayController.cs @@ -1,9 +1,6 @@ using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Globalization; -using System.IO; -using System.Linq; using System.Net.Mime; using System.Text; using Jellyfin.Api.Attributes; @@ -50,6 +47,7 @@ public class TrickplayController : BaseJellyfinApiController /// <returns>A <see cref="FileResult"/> containing the trickplay tiles file.</returns> [HttpGet("Videos/{itemId}/Trickplay/{width}/tiles.m3u8")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesPlaylistFile] public ActionResult GetTrickplayHlsPlaylist( [FromRoute, Required] Guid itemId, @@ -109,7 +107,7 @@ public class TrickplayController : BaseJellyfinApiController var resolution = $"{tilesInfo.Width}x{tilesInfo.Height}"; var layout = $"{tilesInfo.TileWidth}x{tilesInfo.TileHeight}"; var tilesPerGrid = tilesInfo.TileWidth * tilesInfo.TileHeight; - var tileDuration = tilesInfo.Interval / 1000m; + var tileDuration = tilesInfo.Interval / 1000d; var infDuration = tileDuration * tilesPerGrid; var tileGridCount = (int)Math.Ceiling((decimal)tilesInfo.TileCount / tilesPerGrid); @@ -132,18 +130,10 @@ public class TrickplayController : BaseJellyfinApiController infDuration = tileDuration * tilesPerGrid; } - var url = string.Format( - CultureInfo.InvariantCulture, - urlFormat, - width.ToString(CultureInfo.InvariantCulture), - i.ToString(CultureInfo.InvariantCulture), - mediaSourceId.ToString("N"), - User.GetToken()); - // EXTINF builder .Append("#EXTINF:") - .Append(string.Format(CultureInfo.InvariantCulture, decimalFormat, infDuration)) + .AppendFormat(CultureInfo.InvariantCulture, decimalFormat, infDuration) .AppendLine(","); // EXT-X-TILES @@ -153,10 +143,19 @@ public class TrickplayController : BaseJellyfinApiController .Append(",LAYOUT=") .Append(layout) .Append(",DURATION=") - .AppendLine(string.Format(CultureInfo.InvariantCulture, decimalFormat, tileDuration)); + .AppendFormat(CultureInfo.InvariantCulture, decimalFormat, tileDuration) + .AppendLine(); // URL - builder.AppendLine(url); + builder + .AppendFormat( + CultureInfo.InvariantCulture, + urlFormat, + width.ToString(CultureInfo.InvariantCulture), + i.ToString(CultureInfo.InvariantCulture), + mediaSourceId.ToString("N"), + User.GetToken()) + .AppendLine(); } builder.AppendLine("#EXT-X-ENDLIST"); @@ -164,6 +163,6 @@ public class TrickplayController : BaseJellyfinApiController } } - return new FileContentResult(Array.Empty<byte>(), MimeTypes.GetMimeType("playlist.m3u8")); + return NotFound(); } } diff --git a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs index 17e9efdde5..f6dcde4f68 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs +++ b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs @@ -8,7 +8,6 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Trickplay; using MediaBrowser.Model.Configuration; -using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.Trickplay; From 0e2c362078c5b0babaa0fd254106452e6d67ebe8 Mon Sep 17 00:00:00 2001 From: Nick <20588554+nicknsy@users.noreply.github.com> Date: Tue, 30 May 2023 14:23:02 -0700 Subject: [PATCH 395/858] Move SkiaSharp related code to Jellyfin.Drawing and IImageEncoder --- .../Drawing/IImageEncoder.cs | 11 +++ .../MediaBrowser.Providers.csproj | 3 +- .../Trickplay/TrickplayManager.cs | 97 +++++++------------ src/Jellyfin.Drawing.Skia/SkiaEncoder.cs | 79 +++++++++++++++ src/Jellyfin.Drawing/NullImageEncoder.cs | 6 ++ 5 files changed, 131 insertions(+), 65 deletions(-) diff --git a/MediaBrowser.Controller/Drawing/IImageEncoder.cs b/MediaBrowser.Controller/Drawing/IImageEncoder.cs index e5c8ebfaf9..42c680761d 100644 --- a/MediaBrowser.Controller/Drawing/IImageEncoder.cs +++ b/MediaBrowser.Controller/Drawing/IImageEncoder.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Drawing; namespace MediaBrowser.Controller.Drawing @@ -81,5 +82,15 @@ namespace MediaBrowser.Controller.Drawing /// <param name="posters">The list of poster paths.</param> /// <param name="backdrops">The list of backdrop paths.</param> void CreateSplashscreen(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops); + + /// <summary> + /// Creates a new jpeg trickplay grid image. + /// </summary> + /// <param name="options">The options to use when creating the image. Width and Height are a quantity of tiles in this case, not pixels.</param> + /// <param name="quality">The image encode quality.</param> + /// <param name="imgWidth">The width of a single trickplay image.</param> + /// <param name="imgHeight">Optional height of a single trickplay image, if it is known.</param> + /// <returns>Height of single decoded trickplay image.</returns> + int CreateTrickplayGrid(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight); } } diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index c836c8ed53..7ef70f4b08 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -1,4 +1,4 @@ -<Project Sdk="Microsoft.NET.Sdk"> +<Project Sdk="Microsoft.NET.Sdk"> <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> <PropertyGroup> @@ -22,7 +22,6 @@ <PackageReference Include="Microsoft.Extensions.Http" /> <PackageReference Include="Newtonsoft.Json" /> <PackageReference Include="PlaylistsNET" /> - <PackageReference Include="SkiaSharp" /> <PackageReference Include="TagLibSharp" /> <PackageReference Include="TMDbLib" /> </ItemGroup> diff --git a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs index 2304f803ec..419adc4b03 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs +++ b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; @@ -15,7 +16,6 @@ using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; -using SkiaSharp; namespace MediaBrowser.Providers.Trickplay; @@ -31,6 +31,7 @@ public class TrickplayManager : ITrickplayManager private readonly EncodingHelper _encodingHelper; private readonly ILibraryManager _libraryManager; private readonly IServerConfigurationManager _config; + private readonly IImageEncoder _imageEncoder; private static readonly SemaphoreSlim _resourcePool = new(1, 1); private static readonly string[] _trickplayImgExtensions = { ".jpg" }; @@ -45,6 +46,7 @@ public class TrickplayManager : ITrickplayManager /// <param name="encodingHelper">The encoding helper.</param> /// <param name="libraryManager">The library manager.</param> /// <param name="config">The server configuration manager.</param> + /// <param name="imageEncoder">The image encoder.</param> public TrickplayManager( ILogger<TrickplayManager> logger, IItemRepository itemRepo, @@ -52,7 +54,8 @@ public class TrickplayManager : ITrickplayManager IFileSystem fileSystem, EncodingHelper encodingHelper, ILibraryManager libraryManager, - IServerConfigurationManager config) + IServerConfigurationManager config, + IImageEncoder imageEncoder) { _logger = logger; _itemRepo = itemRepo; @@ -61,6 +64,7 @@ public class TrickplayManager : ITrickplayManager _encodingHelper = encodingHelper; _libraryManager = libraryManager; _config = config; + _imageEncoder = imageEncoder; } /// <inheritdoc /> @@ -141,7 +145,8 @@ public class TrickplayManager : ITrickplayManager } var images = _fileSystem.GetFiles(imgTempDir, _trickplayImgExtensions, false, false) - .OrderBy(i => i.FullName) + .Select(i => i.FullName) + .OrderBy(i => i) .ToList(); // Create tiles @@ -185,11 +190,11 @@ public class TrickplayManager : ITrickplayManager } } - private TrickplayTilesInfo CreateTiles(List<FileSystemMetadata> images, int width, TrickplayOptions options, string workDir, string outputDir) + private TrickplayTilesInfo CreateTiles(List<string> images, int width, TrickplayOptions options, string workDir, string outputDir) { if (images.Count == 0) { - throw new InvalidOperationException("Can't create trickplay from 0 images."); + throw new ArgumentException("Can't create trickplay from 0 images."); } Directory.CreateDirectory(workDir); @@ -200,76 +205,42 @@ public class TrickplayManager : ITrickplayManager Interval = options.Interval, TileWidth = options.TileWidth, TileHeight = options.TileHeight, - TileCount = 0, + TileCount = images.Count, + // Set during image generation + Height = 0, Bandwidth = 0 }; - var firstImg = SKBitmap.Decode(images[0].FullName); - if (firstImg == null) - { - throw new InvalidDataException("Could not decode image data."); - } - - tilesInfo.Height = firstImg.Height; - if (tilesInfo.Width != firstImg.Width) - { - throw new InvalidOperationException("Image width does not match config width."); - } - /* - * Generate grids of trickplay image tiles + * Generate trickplay tile grids from sets of images */ - var imgNo = 0; - var i = 0; - while (i < images.Count) + var imageOptions = new ImageCollageOptions { - var tileGrid = new SKBitmap(tilesInfo.Width * tilesInfo.TileWidth, tilesInfo.Height * tilesInfo.TileHeight); + Width = tilesInfo.TileWidth, + Height = tilesInfo.TileHeight + }; - using (var canvas = new SKCanvas(tileGrid)) + var tilesPerGrid = tilesInfo.TileWidth * tilesInfo.TileHeight; + var requiredTileGrids = (int)Math.Ceiling((double)images.Count / tilesPerGrid); + + for (int i = 0; i < requiredTileGrids; i++) + { + // Set output/input paths + var tileGridPath = Path.Combine(workDir, $"{i}.jpg"); + + imageOptions.OutputPath = tileGridPath; + imageOptions.InputPaths = images.Skip(i * tilesPerGrid).Take(tilesPerGrid).ToList(); + + // Generate image and use returned height for tiles info + var height = _imageEncoder.CreateTrickplayGrid(imageOptions, options.JpegQuality, tilesInfo.Width, tilesInfo.Height != 0 ? tilesInfo.Height : null); + if (tilesInfo.Height == 0) { - for (var y = 0; y < tilesInfo.TileHeight; y++) - { - for (var x = 0; x < tilesInfo.TileWidth; x++) - { - if (i >= images.Count) - { - break; - } - - var img = SKBitmap.Decode(images[i].FullName); - if (img == null) - { - throw new InvalidDataException("Could not decode image data."); - } - - if (tilesInfo.Width != img.Width) - { - throw new InvalidOperationException("Image width does not match config width."); - } - - if (tilesInfo.Height != img.Height) - { - throw new InvalidOperationException("Image height does not match first image height."); - } - - canvas.DrawBitmap(img, x * tilesInfo.Width, y * tilesInfo.Height); - tilesInfo.TileCount++; - i++; - } - } - } - - // Output each tile grid to singular file - var tileGridPath = Path.Combine(workDir, $"{imgNo}.jpg"); - using (var stream = File.OpenWrite(tileGridPath)) - { - tileGrid.Encode(stream, SKEncodedImageFormat.Jpeg, options.JpegQuality); + tilesInfo.Height = height; } + // Update bitrate var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tileGridPath).Length * 8 / tilesInfo.TileWidth / tilesInfo.TileHeight / (tilesInfo.Interval / 1000)); tilesInfo.Bandwidth = Math.Max(tilesInfo.Bandwidth, bitrate); - - imgNo++; } /* diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 2d980db181..2facf0f370 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -2,14 +2,18 @@ using System; using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Linq; +using System.Security.Cryptography.Xml; using BlurHashSharp.SkiaSharp; using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.LiveTv; using MediaBrowser.Model.Drawing; using Microsoft.Extensions.Logging; using SkiaSharp; +using static System.Net.Mime.MediaTypeNames; using SKSvg = SkiaSharp.Extended.Svg.SKSvg; namespace Jellyfin.Drawing.Skia; @@ -526,6 +530,81 @@ public class SkiaEncoder : IImageEncoder splashBuilder.GenerateSplash(posters, backdrops, outputPath); } + /// <inheritdoc /> + public int CreateTrickplayGrid(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight) + { + var paths = options.InputPaths; + var tileWidth = options.Width; + var tileHeight = options.Height; + + if (paths.Count < 1) + { + throw new ArgumentException("InputPaths cannot be empty."); + } + else if (paths.Count > tileWidth * tileHeight) + { + throw new ArgumentException($"InputPaths contains more images than would fit on {tileWidth}x{tileHeight} grid."); + } + + // If no height provided, use height of first image. + if (!imgHeight.HasValue) + { + using var firstImg = Decode(paths[0], false, null, out _); + + if (firstImg is null) + { + throw new InvalidDataException("Could not decode image data."); + } + + if (firstImg.Width != imgWidth) + { + throw new InvalidOperationException("Image width does not match provided width."); + } + + imgHeight = firstImg.Height; + } + + // Make horizontal strips using every provided image. + using var tileGrid = new SKBitmap(imgWidth * tileWidth, imgHeight.Value * tileHeight); + using var canvas = new SKCanvas(tileGrid); + + var imgIndex = 0; + for (var y = 0; y < tileHeight; y++) + { + for (var x = 0; x < tileWidth; x++) + { + if (imgIndex >= paths.Count) + { + break; + } + + using var img = Decode(paths[imgIndex++], false, null, out _); + + if (img is null) + { + throw new InvalidDataException("Could not decode image data."); + } + + if (img.Width != imgWidth) + { + throw new InvalidOperationException("Image width does not match provided width."); + } + + if (img.Height != imgHeight) + { + throw new InvalidOperationException("Image height does not match first image height."); + } + + canvas.DrawBitmap(img, x * imgWidth, y * imgHeight.Value); + } + } + + using var outputStream = new SKFileWStream(options.OutputPath); + tileGrid.Encode(outputStream, SKEncodedImageFormat.Jpeg, quality); + + return imgHeight.Value; + } + private void DrawIndicator(SKCanvas canvas, int imageWidth, int imageHeight, ImageProcessingOptions options) { try diff --git a/src/Jellyfin.Drawing/NullImageEncoder.cs b/src/Jellyfin.Drawing/NullImageEncoder.cs index 171128bed3..15345e1bc8 100644 --- a/src/Jellyfin.Drawing/NullImageEncoder.cs +++ b/src/Jellyfin.Drawing/NullImageEncoder.cs @@ -49,6 +49,12 @@ public class NullImageEncoder : IImageEncoder throw new NotImplementedException(); } + /// <inheritdoc /> + public int CreateTrickplayGrid(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight) + { + throw new NotImplementedException(); + } + /// <inheritdoc /> public string GetImageBlurHash(int xComp, int yComp, string path) { From 6de56f05186b77042a611112d82208b8fa8675fb Mon Sep 17 00:00:00 2001 From: Niels van Velzen <git@ndat.nl> Date: Tue, 20 Jun 2023 16:51:07 +0200 Subject: [PATCH 396/858] Add support for lyric provider plugins --- Jellyfin.Server/CoreAppHost.cs | 6 ++ .../Lyrics/ILyricParser.cs | 28 ++++++++ MediaBrowser.Controller/Lyrics/LyricFile.cs | 28 ++++++++ MediaBrowser.Controller/Lyrics/LyricInfo.cs | 49 -------------- .../Lyric/DefaultLyricProvider.cs | 66 +++++++++++++++++++ .../Lyric}/ILyricProvider.cs | 12 ++-- ...{LrcLyricProvider.cs => LrcLyricParser.cs} | 53 +++++---------- MediaBrowser.Providers/Lyric/LyricManager.cs | 22 +++++-- .../Lyric/TxtLyricParser.cs | 49 ++++++++++++++ .../Lyric/TxtLyricProvider.cs | 60 ----------------- 10 files changed, 215 insertions(+), 158 deletions(-) create mode 100644 MediaBrowser.Controller/Lyrics/ILyricParser.cs create mode 100644 MediaBrowser.Controller/Lyrics/LyricFile.cs delete mode 100644 MediaBrowser.Controller/Lyrics/LyricInfo.cs create mode 100644 MediaBrowser.Providers/Lyric/DefaultLyricProvider.cs rename {MediaBrowser.Controller/Lyrics => MediaBrowser.Providers/Lyric}/ILyricProvider.cs (69%) rename MediaBrowser.Providers/Lyric/{LrcLyricProvider.cs => LrcLyricParser.cs} (76%) create mode 100644 MediaBrowser.Providers/Lyric/TxtLyricParser.cs delete mode 100644 MediaBrowser.Providers/Lyric/TxtLyricProvider.cs diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index 939376dd8d..0c6315c667 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -22,6 +22,7 @@ using MediaBrowser.Controller.Lyrics; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Security; using MediaBrowser.Model.Activity; +using MediaBrowser.Providers.Lyric; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -93,6 +94,11 @@ namespace Jellyfin.Server serviceCollection.AddSingleton(typeof(ILyricProvider), type); } + foreach (var type in GetExportTypes<ILyricParser>()) + { + serviceCollection.AddSingleton(typeof(ILyricParser), type); + } + base.RegisterServices(serviceCollection); } diff --git a/MediaBrowser.Controller/Lyrics/ILyricParser.cs b/MediaBrowser.Controller/Lyrics/ILyricParser.cs new file mode 100644 index 0000000000..65a9471a3b --- /dev/null +++ b/MediaBrowser.Controller/Lyrics/ILyricParser.cs @@ -0,0 +1,28 @@ +using MediaBrowser.Controller.Resolvers; +using MediaBrowser.Providers.Lyric; + +namespace MediaBrowser.Controller.Lyrics; + +/// <summary> +/// Interface ILyricParser. +/// </summary> +public interface ILyricParser +{ + /// <summary> + /// Gets a value indicating the provider name. + /// </summary> + string Name { get; } + + /// <summary> + /// Gets the priority. + /// </summary> + /// <value>The priority.</value> + ResolverPriority Priority { get; } + + /// <summary> + /// Parses the raw lyrics into a response. + /// </summary> + /// <param name="lyrics">The raw lyrics content.</param> + /// <returns>The parsed lyrics or null if invalid.</returns> + LyricResponse? ParseLyrics(LyricFile lyrics); +} diff --git a/MediaBrowser.Controller/Lyrics/LyricFile.cs b/MediaBrowser.Controller/Lyrics/LyricFile.cs new file mode 100644 index 0000000000..21096797ac --- /dev/null +++ b/MediaBrowser.Controller/Lyrics/LyricFile.cs @@ -0,0 +1,28 @@ +namespace MediaBrowser.Providers.Lyric; + +/// <summary> +/// The information for a raw lyrics file before parsing. +/// </summary> +public class LyricFile +{ + /// <summary> + /// Initializes a new instance of the <see cref="LyricFile"/> class. + /// </summary> + /// <param name="name">The name.</param> + /// <param name="content">The content.</param> + public LyricFile(string name, string content) + { + Name = name; + Content = content; + } + + /// <summary> + /// Gets or sets the name of the lyrics file. This must include the file extension. + /// </summary> + public string Name { get; set; } + + /// <summary> + /// Gets or sets the contents of the file. + /// </summary> + public string Content { get; set; } +} diff --git a/MediaBrowser.Controller/Lyrics/LyricInfo.cs b/MediaBrowser.Controller/Lyrics/LyricInfo.cs deleted file mode 100644 index 6ec6df5825..0000000000 --- a/MediaBrowser.Controller/Lyrics/LyricInfo.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.IO; -using Jellyfin.Extensions; - -namespace MediaBrowser.Controller.Lyrics; - -/// <summary> -/// Lyric helper methods. -/// </summary> -public static class LyricInfo -{ - /// <summary> - /// Gets matching lyric file for a requested item. - /// </summary> - /// <param name="lyricProvider">The lyricProvider interface to use.</param> - /// <param name="itemPath">Path of requested item.</param> - /// <returns>Lyric file path if passed lyric provider's supported media type is found; otherwise, null.</returns> - public static string? GetLyricFilePath(this ILyricProvider lyricProvider, string itemPath) - { - // Ensure we have a provider - if (lyricProvider is null) - { - return null; - } - - // Ensure the path to the item is not null - string? itemDirectoryPath = Path.GetDirectoryName(itemPath); - if (itemDirectoryPath is null) - { - return null; - } - - // Ensure the directory path exists - if (!Directory.Exists(itemDirectoryPath)) - { - return null; - } - - foreach (var lyricFilePath in Directory.GetFiles(itemDirectoryPath, $"{Path.GetFileNameWithoutExtension(itemPath)}.*")) - { - if (lyricProvider.SupportedMediaTypes.Contains(Path.GetExtension(lyricFilePath.AsSpan())[1..], StringComparison.OrdinalIgnoreCase)) - { - return lyricFilePath; - } - } - - return null; - } -} diff --git a/MediaBrowser.Providers/Lyric/DefaultLyricProvider.cs b/MediaBrowser.Providers/Lyric/DefaultLyricProvider.cs new file mode 100644 index 0000000000..f828ec26b9 --- /dev/null +++ b/MediaBrowser.Providers/Lyric/DefaultLyricProvider.cs @@ -0,0 +1,66 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Jellyfin.Extensions; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Resolvers; + +namespace MediaBrowser.Providers.Lyric; + +/// <inheritdoc /> +public class DefaultLyricProvider : ILyricProvider +{ + private static readonly string[] _lyricExtensions = { "lrc", "elrc", "txt", "elrc" }; + + /// <inheritdoc /> + public string Name => "DefaultLyricProvider"; + + /// <inheritdoc /> + public ResolverPriority Priority => ResolverPriority.First; + + /// <inheritdoc /> + public bool HasLyrics(BaseItem item) + { + var path = GetLyricsPath(item); + return path is not null; + } + + /// <inheritdoc /> + public async Task<LyricFile?> GetLyrics(BaseItem item) + { + var path = GetLyricsPath(item); + if (path is not null) + { + var content = await File.ReadAllTextAsync(path).ConfigureAwait(false); + return new LyricFile(path, content); + } + + return null; + } + + private string? GetLyricsPath(BaseItem item) + { + // Ensure the path to the item is not null + string? itemDirectoryPath = Path.GetDirectoryName(item.Path); + if (itemDirectoryPath is null) + { + return null; + } + + // Ensure the directory path exists + if (!Directory.Exists(itemDirectoryPath)) + { + return null; + } + + foreach (var lyricFilePath in Directory.GetFiles(itemDirectoryPath, $"{Path.GetFileNameWithoutExtension(item.Path)}.*")) + { + if (_lyricExtensions.Contains(Path.GetExtension(lyricFilePath.AsSpan())[1..], StringComparison.OrdinalIgnoreCase)) + { + return lyricFilePath; + } + } + + return null; + } +} diff --git a/MediaBrowser.Controller/Lyrics/ILyricProvider.cs b/MediaBrowser.Providers/Lyric/ILyricProvider.cs similarity index 69% rename from MediaBrowser.Controller/Lyrics/ILyricProvider.cs rename to MediaBrowser.Providers/Lyric/ILyricProvider.cs index 2a04c61520..27ceba72bf 100644 --- a/MediaBrowser.Controller/Lyrics/ILyricProvider.cs +++ b/MediaBrowser.Providers/Lyric/ILyricProvider.cs @@ -1,9 +1,8 @@ -using System.Collections.Generic; using System.Threading.Tasks; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Resolvers; -namespace MediaBrowser.Controller.Lyrics; +namespace MediaBrowser.Providers.Lyric; /// <summary> /// Interface ILyricsProvider. @@ -22,15 +21,16 @@ public interface ILyricProvider ResolverPriority Priority { get; } /// <summary> - /// Gets the supported media types for this provider. + /// Checks if an item has lyrics available. /// </summary> - /// <value>The supported media types.</value> - IReadOnlyCollection<string> SupportedMediaTypes { get; } + /// <param name="item">The media item.</param> + /// <returns>Whether lyrics where found or not.</returns> + bool HasLyrics(BaseItem item); /// <summary> /// Gets the lyrics. /// </summary> /// <param name="item">The media item.</param> /// <returns>A task representing found lyrics.</returns> - Task<LyricResponse?> GetLyrics(BaseItem item); + Task<LyricFile?> GetLyrics(BaseItem item); } diff --git a/MediaBrowser.Providers/Lyric/LrcLyricProvider.cs b/MediaBrowser.Providers/Lyric/LrcLyricParser.cs similarity index 76% rename from MediaBrowser.Providers/Lyric/LrcLyricProvider.cs rename to MediaBrowser.Providers/Lyric/LrcLyricParser.cs index 7b108921b3..01a0dddf1f 100644 --- a/MediaBrowser.Providers/Lyric/LrcLyricProvider.cs +++ b/MediaBrowser.Providers/Lyric/LrcLyricParser.cs @@ -3,34 +3,29 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; -using System.Threading.Tasks; +using Jellyfin.Extensions; using LrcParser.Model; using LrcParser.Parser; -using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Lyrics; using MediaBrowser.Controller.Resolvers; -using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.Lyric; /// <summary> -/// LRC Lyric Provider. +/// LRC Lyric Parser. /// </summary> -public class LrcLyricProvider : ILyricProvider +public class LrcLyricParser : ILyricParser { - private readonly ILogger<LrcLyricProvider> _logger; - private readonly LyricParser _lrcLyricParser; + private static readonly string[] _supportedMediaTypes = { "lrc", "elrc" }; private static readonly string[] _acceptedTimeFormats = { "HH:mm:ss", "H:mm:ss", "mm:ss", "m:ss" }; /// <summary> - /// Initializes a new instance of the <see cref="LrcLyricProvider"/> class. + /// Initializes a new instance of the <see cref="LrcLyricParser"/> class. /// </summary> - /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param> - public LrcLyricProvider(ILogger<LrcLyricProvider> logger) + public LrcLyricParser() { - _logger = logger; _lrcLyricParser = new LrcParser.Parser.Lrc.LrcParser(); } @@ -41,37 +36,25 @@ public class LrcLyricProvider : ILyricProvider /// Gets the priority. /// </summary> /// <value>The priority.</value> - public ResolverPriority Priority => ResolverPriority.First; + public ResolverPriority Priority => ResolverPriority.Fourth; /// <inheritdoc /> - public IReadOnlyCollection<string> SupportedMediaTypes { get; } = new[] { "lrc", "elrc" }; - - /// <summary> - /// Opens lyric file for the requested item, and processes it for API return. - /// </summary> - /// <param name="item">The item to to process.</param> - /// <returns>If provider can determine lyrics, returns a <see cref="LyricResponse"/> with or without metadata; otherwise, null.</returns> - public async Task<LyricResponse?> GetLyrics(BaseItem item) + public LyricResponse? ParseLyrics(LyricFile lyrics) { - string? lyricFilePath = this.GetLyricFilePath(item.Path); - - if (string.IsNullOrEmpty(lyricFilePath)) + if (!_supportedMediaTypes.Contains(Path.GetExtension(lyrics.Name.AsSpan())[1..], StringComparison.OrdinalIgnoreCase)) { return null; } - var fileMetaData = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - string lrcFileContent = await File.ReadAllTextAsync(lyricFilePath).ConfigureAwait(false); - Song lyricData; try { - lyricData = _lrcLyricParser.Decode(lrcFileContent); + lyricData = _lrcLyricParser.Decode(lyrics.Content); } - catch (Exception ex) + catch (Exception) { - _logger.LogError(ex, "Error parsing lyric file {LyricFilePath} from {Provider}", lyricFilePath, Name); + // Failed to parse, return null so the next parser will be tried return null; } @@ -84,6 +67,7 @@ public class LrcLyricProvider : ILyricProvider .Select(x => x.Text) .ToList(); + var fileMetaData = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); foreach (string metaDataRow in metaDataRows) { var index = metaDataRow.IndexOf(':', StringComparison.OrdinalIgnoreCase); @@ -130,17 +114,10 @@ public class LrcLyricProvider : ILyricProvider // Map metaData values from LRC file to LyricMetadata properties LyricMetadata lyricMetadata = MapMetadataValues(fileMetaData); - return new LyricResponse - { - Metadata = lyricMetadata, - Lyrics = lyricList - }; + return new LyricResponse { Metadata = lyricMetadata, Lyrics = lyricList }; } - return new LyricResponse - { - Lyrics = lyricList - }; + return new LyricResponse { Lyrics = lyricList }; } /// <summary> diff --git a/MediaBrowser.Providers/Lyric/LyricManager.cs b/MediaBrowser.Providers/Lyric/LyricManager.cs index f9547e0f05..6da8119275 100644 --- a/MediaBrowser.Providers/Lyric/LyricManager.cs +++ b/MediaBrowser.Providers/Lyric/LyricManager.cs @@ -12,14 +12,17 @@ namespace MediaBrowser.Providers.Lyric; public class LyricManager : ILyricManager { private readonly ILyricProvider[] _lyricProviders; + private readonly ILyricParser[] _lyricParsers; /// <summary> /// Initializes a new instance of the <see cref="LyricManager"/> class. /// </summary> /// <param name="lyricProviders">All found lyricProviders.</param> - public LyricManager(IEnumerable<ILyricProvider> lyricProviders) + /// <param name="lyricParsers">All found lyricParsers.</param> + public LyricManager(IEnumerable<ILyricProvider> lyricProviders, IEnumerable<ILyricParser> lyricParsers) { _lyricProviders = lyricProviders.OrderBy(i => i.Priority).ToArray(); + _lyricParsers = lyricParsers.OrderBy(i => i.Priority).ToArray(); } /// <inheritdoc /> @@ -27,10 +30,19 @@ public class LyricManager : ILyricManager { foreach (ILyricProvider provider in _lyricProviders) { - var results = await provider.GetLyrics(item).ConfigureAwait(false); - if (results is not null) + var lyrics = await provider.GetLyrics(item).ConfigureAwait(false); + if (lyrics is null) { - return results; + continue; + } + + foreach (ILyricParser parser in _lyricParsers) + { + var result = parser.ParseLyrics(lyrics); + if (result is not null) + { + return result; + } } } @@ -47,7 +59,7 @@ public class LyricManager : ILyricManager continue; } - if (provider.GetLyricFilePath(item.Path) is not null) + if (provider.HasLyrics(item)) { return true; } diff --git a/MediaBrowser.Providers/Lyric/TxtLyricParser.cs b/MediaBrowser.Providers/Lyric/TxtLyricParser.cs new file mode 100644 index 0000000000..2ed0a6d8a6 --- /dev/null +++ b/MediaBrowser.Providers/Lyric/TxtLyricParser.cs @@ -0,0 +1,49 @@ +using System; +using System.IO; +using Jellyfin.Extensions; +using MediaBrowser.Controller.Lyrics; +using MediaBrowser.Controller.Resolvers; + +namespace MediaBrowser.Providers.Lyric; + +/// <summary> +/// TXT Lyric Parser. +/// </summary> +public class TxtLyricParser : ILyricParser +{ + private static readonly string[] _supportedMediaTypes = { "lrc", "elrc", "txt" }; + + /// <inheritdoc /> + public string Name => "TxtLyricProvider"; + + /// <summary> + /// Gets the priority. + /// </summary> + /// <value>The priority.</value> + public ResolverPriority Priority => ResolverPriority.Fifth; + + /// <inheritdoc /> + public LyricResponse? ParseLyrics(LyricFile lyrics) + { + if (!_supportedMediaTypes.Contains(Path.GetExtension(lyrics.Name.AsSpan())[1..], StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + string[] lyricTextLines = lyrics.Content.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); + + if (lyricTextLines.Length == 0) + { + return null; + } + + LyricLine[] lyricList = new LyricLine[lyricTextLines.Length]; + + for (int lyricLineIndex = 0; lyricLineIndex < lyricTextLines.Length; lyricLineIndex++) + { + lyricList[lyricLineIndex] = new LyricLine(lyricTextLines[lyricLineIndex]); + } + + return new LyricResponse { Lyrics = lyricList }; + } +} diff --git a/MediaBrowser.Providers/Lyric/TxtLyricProvider.cs b/MediaBrowser.Providers/Lyric/TxtLyricProvider.cs deleted file mode 100644 index a9099d1927..0000000000 --- a/MediaBrowser.Providers/Lyric/TxtLyricProvider.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Lyrics; -using MediaBrowser.Controller.Resolvers; - -namespace MediaBrowser.Providers.Lyric; - -/// <summary> -/// TXT Lyric Provider. -/// </summary> -public class TxtLyricProvider : ILyricProvider -{ - /// <inheritdoc /> - public string Name => "TxtLyricProvider"; - - /// <summary> - /// Gets the priority. - /// </summary> - /// <value>The priority.</value> - public ResolverPriority Priority => ResolverPriority.Second; - - /// <inheritdoc /> - public IReadOnlyCollection<string> SupportedMediaTypes { get; } = new[] { "lrc", "elrc", "txt" }; - - /// <summary> - /// Opens lyric file for the requested item, and processes it for API return. - /// </summary> - /// <param name="item">The item to to process.</param> - /// <returns>If provider can determine lyrics, returns a <see cref="LyricResponse"/>; otherwise, null.</returns> - public async Task<LyricResponse?> GetLyrics(BaseItem item) - { - string? lyricFilePath = this.GetLyricFilePath(item.Path); - - if (string.IsNullOrEmpty(lyricFilePath)) - { - return null; - } - - string[] lyricTextLines = await File.ReadAllLinesAsync(lyricFilePath).ConfigureAwait(false); - - if (lyricTextLines.Length == 0) - { - return null; - } - - LyricLine[] lyricList = new LyricLine[lyricTextLines.Length]; - - for (int lyricLineIndex = 0; lyricLineIndex < lyricTextLines.Length; lyricLineIndex++) - { - lyricList[lyricLineIndex] = new LyricLine(lyricTextLines[lyricLineIndex]); - } - - return new LyricResponse - { - Lyrics = lyricList - }; - } -} From b27153fd3103fef078b4088bd8721033114f966e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 23 Jun 2023 14:25:37 -0600 Subject: [PATCH 397/858] chore(deps): update dotnet monorepo to v7.0.8 (#9919) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Cody Robibero <cody@robibe.ro> --- Directory.Packages.props | 16 ++++++++-------- deployment/Dockerfile.centos.amd64 | 2 +- deployment/Dockerfile.fedora.amd64 | 2 +- deployment/Dockerfile.ubuntu.amd64 | 2 +- deployment/Dockerfile.ubuntu.arm64 | 2 +- deployment/Dockerfile.ubuntu.armhf | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 28059fe845..b9c1a8dcb4 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -23,13 +23,13 @@ <PackageVersion Include="libse" Version="3.6.13" /> <PackageVersion Include="LrcParser" Version="2023.524.0" /> <PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" /> - <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.7" /> - <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.7" /> + <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.8" /> + <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.8" /> <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.7" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.7" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.7" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.7" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.8" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.8" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.8" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.8" /> <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" /> @@ -38,8 +38,8 @@ <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" /> - <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.7" /> - <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.7" /> + <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.8" /> + <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.8" /> <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Http" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" /> diff --git a/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64 index ee1bc911bb..771675519d 100644 --- a/deployment/Dockerfile.centos.amd64 +++ b/deployment/Dockerfile.centos.amd64 @@ -13,7 +13,7 @@ RUN yum update -yq \ && yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget # Install DotNET SDK -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/9c86d7b4-acb2-4be4-8a89-d13bc3c3f28f/1d044c7c29df018e8f2837bb343e8a84/dotnet-sdk-7.0.304-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/87a55ae3-917d-449e-a4e8-776f82976e91/03380e598c326c2f9465d262c6a88c45/dotnet-sdk-7.0.305-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.fedora.amd64 b/deployment/Dockerfile.fedora.amd64 index 0a2ee7b3f3..c552f06b0b 100644 --- a/deployment/Dockerfile.fedora.amd64 +++ b/deployment/Dockerfile.fedora.amd64 @@ -12,7 +12,7 @@ RUN dnf update -yq \ && dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd wget make # Install DotNET SDK -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/9c86d7b4-acb2-4be4-8a89-d13bc3c3f28f/1d044c7c29df018e8f2837bb343e8a84/dotnet-sdk-7.0.304-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/87a55ae3-917d-449e-a4e8-776f82976e91/03380e598c326c2f9465d262c6a88c45/dotnet-sdk-7.0.305-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64 index 457f28f50c..30100d20d9 100644 --- a/deployment/Dockerfile.ubuntu.amd64 +++ b/deployment/Dockerfile.ubuntu.amd64 @@ -17,7 +17,7 @@ RUN apt-get update -yqq \ libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/9c86d7b4-acb2-4be4-8a89-d13bc3c3f28f/1d044c7c29df018e8f2837bb343e8a84/dotnet-sdk-7.0.304-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/87a55ae3-917d-449e-a4e8-776f82976e91/03380e598c326c2f9465d262c6a88c45/dotnet-sdk-7.0.305-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.arm64 b/deployment/Dockerfile.ubuntu.arm64 index e3f1032a82..bac2adfafb 100644 --- a/deployment/Dockerfile.ubuntu.arm64 +++ b/deployment/Dockerfile.ubuntu.arm64 @@ -16,7 +16,7 @@ RUN apt-get update -yqq \ mmv build-essential lsb-release # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/9c86d7b4-acb2-4be4-8a89-d13bc3c3f28f/1d044c7c29df018e8f2837bb343e8a84/dotnet-sdk-7.0.304-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/87a55ae3-917d-449e-a4e8-776f82976e91/03380e598c326c2f9465d262c6a88c45/dotnet-sdk-7.0.305-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.armhf b/deployment/Dockerfile.ubuntu.armhf index aebcaeccff..37a1ed5ff3 100644 --- a/deployment/Dockerfile.ubuntu.armhf +++ b/deployment/Dockerfile.ubuntu.armhf @@ -16,7 +16,7 @@ RUN apt-get update -yqq \ mmv build-essential lsb-release # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/9c86d7b4-acb2-4be4-8a89-d13bc3c3f28f/1d044c7c29df018e8f2837bb343e8a84/dotnet-sdk-7.0.304-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/87a55ae3-917d-449e-a4e8-776f82976e91/03380e598c326c2f9465d262c6a88c45/dotnet-sdk-7.0.305-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet From 619d1d47f27e3ca2f2f249fa81fe23f8019ec0e7 Mon Sep 17 00:00:00 2001 From: Nick <20588554+nicknsy@users.noreply.github.com> Date: Fri, 23 Jun 2023 14:22:00 -0700 Subject: [PATCH 398/858] Move GetHlsPlaylist to ITrickplayManager --- .../Controllers/TrickplayController.cs | 86 ++----------------- .../Trickplay/ITrickplayManager.cs | 9 ++ .../Trickplay/TrickplayManager.cs | 76 ++++++++++++++++ 3 files changed, 94 insertions(+), 77 deletions(-) diff --git a/Jellyfin.Api/Controllers/TrickplayController.cs b/Jellyfin.Api/Controllers/TrickplayController.cs index ac71eff199..36464d726e 100644 --- a/Jellyfin.Api/Controllers/TrickplayController.cs +++ b/Jellyfin.Api/Controllers/TrickplayController.cs @@ -1,6 +1,5 @@ using System; using System.ComponentModel.DataAnnotations; -using System.Globalization; using System.Net.Mime; using System.Text; using Jellyfin.Api.Attributes; @@ -54,7 +53,14 @@ public class TrickplayController : BaseJellyfinApiController [FromRoute, Required] int width, [FromQuery] Guid? mediaSourceId) { - return GetTrickplayPlaylistInternal(width, mediaSourceId ?? itemId); + string? playlist = _trickplayManager.GetHlsPlaylist(mediaSourceId ?? itemId, width, User.GetToken()); + + if (string.IsNullOrEmpty(playlist)) + { + return NotFound(); + } + + return new FileContentResult(Encoding.UTF8.GetBytes(playlist), MimeTypes.GetMimeType("playlist.m3u8")); } /// <summary> @@ -71,7 +77,7 @@ public class TrickplayController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesImageFile] - public ActionResult GetTrickplayHlsPlaylist( + public ActionResult GetTrickplayGridImage( [FromRoute, Required] Guid itemId, [FromRoute, Required] int width, [FromRoute, Required] int index, @@ -91,78 +97,4 @@ public class TrickplayController : BaseJellyfinApiController return NotFound(); } - - private ActionResult GetTrickplayPlaylistInternal(int width, Guid mediaSourceId) - { - var tilesResolutions = _trickplayManager.GetTilesResolutions(mediaSourceId); - if (tilesResolutions is not null && tilesResolutions.TryGetValue(width, out var tilesInfo)) - { - var builder = new StringBuilder(128); - - if (tilesInfo.TileCount > 0) - { - const string urlFormat = "Trickplay/{0}/{1}.jpg?MediaSourceId={2}&api_key={3}"; - const string decimalFormat = "{0:0.###}"; - - var resolution = $"{tilesInfo.Width}x{tilesInfo.Height}"; - var layout = $"{tilesInfo.TileWidth}x{tilesInfo.TileHeight}"; - var tilesPerGrid = tilesInfo.TileWidth * tilesInfo.TileHeight; - var tileDuration = tilesInfo.Interval / 1000d; - var infDuration = tileDuration * tilesPerGrid; - var tileGridCount = (int)Math.Ceiling((decimal)tilesInfo.TileCount / tilesPerGrid); - - builder - .AppendLine("#EXTM3U") - .Append("#EXT-X-TARGETDURATION:") - .AppendLine(tileGridCount.ToString(CultureInfo.InvariantCulture)) - .AppendLine("#EXT-X-VERSION:7") - .AppendLine("#EXT-X-MEDIA-SEQUENCE:1") - .AppendLine("#EXT-X-PLAYLIST-TYPE:VOD") - .AppendLine("#EXT-X-IMAGES-ONLY"); - - for (int i = 0; i < tileGridCount; i++) - { - // All tile grids before the last one must contain full amount of tiles. - // The final grid will be 0 < count <= maxTiles - if (i == tileGridCount - 1) - { - tilesPerGrid = tilesInfo.TileCount - (i * tilesPerGrid); - infDuration = tileDuration * tilesPerGrid; - } - - // EXTINF - builder - .Append("#EXTINF:") - .AppendFormat(CultureInfo.InvariantCulture, decimalFormat, infDuration) - .AppendLine(","); - - // EXT-X-TILES - builder - .Append("#EXT-X-TILES:RESOLUTION=") - .Append(resolution) - .Append(",LAYOUT=") - .Append(layout) - .Append(",DURATION=") - .AppendFormat(CultureInfo.InvariantCulture, decimalFormat, tileDuration) - .AppendLine(); - - // URL - builder - .AppendFormat( - CultureInfo.InvariantCulture, - urlFormat, - width.ToString(CultureInfo.InvariantCulture), - i.ToString(CultureInfo.InvariantCulture), - mediaSourceId.ToString("N"), - User.GetToken()) - .AppendLine(); - } - - builder.AppendLine("#EXT-X-ENDLIST"); - return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8")); - } - } - - return NotFound(); - } } diff --git a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs index 8e82c57d4c..8d36fc3ff9 100644 --- a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs +++ b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs @@ -50,4 +50,13 @@ public interface ITrickplayManager /// <param name="index">The tile grid's index.</param> /// <returns>The absolute path.</returns> string GetTrickplayTilePath(BaseItem item, int width, int index); + + /// <summary> + /// Gets the trickplay HLS playlist. + /// </summary> + /// <param name="itemId">The item.</param> + /// <param name="width">The width of a single tile.</param> + /// <param name="apiKey">Optional api key of the requesting user.</param> + /// <returns>The text content of the .m3u8 playlist.</returns> + string? GetHlsPlaylist(Guid itemId, int width, string? apiKey); } diff --git a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs index 419adc4b03..9fe3a330a0 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs +++ b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Text; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Configuration; @@ -321,6 +322,81 @@ public class TrickplayManager : ITrickplayManager return Path.Combine(GetTrickplayDirectory(item, width), index + ".jpg"); } + /// <inheritdoc /> + public string? GetHlsPlaylist(Guid itemId, int width, string? apiKey) + { + var tilesResolutions = GetTilesResolutions(itemId); + if (tilesResolutions is not null && tilesResolutions.TryGetValue(width, out var tilesInfo)) + { + var builder = new StringBuilder(128); + + if (tilesInfo.TileCount > 0) + { + const string urlFormat = "Trickplay/{0}/{1}.jpg?MediaSourceId={2}&api_key={3}"; + const string decimalFormat = "{0:0.###}"; + + var resolution = $"{tilesInfo.Width}x{tilesInfo.Height}"; + var layout = $"{tilesInfo.TileWidth}x{tilesInfo.TileHeight}"; + var tilesPerGrid = tilesInfo.TileWidth * tilesInfo.TileHeight; + var tileDuration = tilesInfo.Interval / 1000d; + var infDuration = tileDuration * tilesPerGrid; + var tileGridCount = (int)Math.Ceiling((decimal)tilesInfo.TileCount / tilesPerGrid); + + builder + .AppendLine("#EXTM3U") + .Append("#EXT-X-TARGETDURATION:") + .AppendLine(tileGridCount.ToString(CultureInfo.InvariantCulture)) + .AppendLine("#EXT-X-VERSION:7") + .AppendLine("#EXT-X-MEDIA-SEQUENCE:1") + .AppendLine("#EXT-X-PLAYLIST-TYPE:VOD") + .AppendLine("#EXT-X-IMAGES-ONLY"); + + for (int i = 0; i < tileGridCount; i++) + { + // All tile grids before the last one must contain full amount of tiles. + // The final grid will be 0 < count <= maxTiles + if (i == tileGridCount - 1) + { + tilesPerGrid = tilesInfo.TileCount - (i * tilesPerGrid); + infDuration = tileDuration * tilesPerGrid; + } + + // EXTINF + builder + .Append("#EXTINF:") + .AppendFormat(CultureInfo.InvariantCulture, decimalFormat, infDuration) + .AppendLine(","); + + // EXT-X-TILES + builder + .Append("#EXT-X-TILES:RESOLUTION=") + .Append(resolution) + .Append(",LAYOUT=") + .Append(layout) + .Append(",DURATION=") + .AppendFormat(CultureInfo.InvariantCulture, decimalFormat, tileDuration) + .AppendLine(); + + // URL + builder + .AppendFormat( + CultureInfo.InvariantCulture, + urlFormat, + width.ToString(CultureInfo.InvariantCulture), + i.ToString(CultureInfo.InvariantCulture), + itemId.ToString("N"), + apiKey) + .AppendLine(); + } + + builder.AppendLine("#EXT-X-ENDLIST"); + return builder.ToString(); + } + } + + return null; + } + private string GetTrickplayDirectory(BaseItem item, int? width = null) { var path = Path.Combine(item.GetInternalMetadataPath(), "trickplay"); From a2a144869d38b295fac0c3ab5bf0ee66b84e4eb0 Mon Sep 17 00:00:00 2001 From: Nick <20588554+nicknsy@users.noreply.github.com> Date: Fri, 23 Jun 2023 14:30:55 -0700 Subject: [PATCH 399/858] Minor code fixes (cvium) --- MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs | 2 +- MediaBrowser.Providers/Trickplay/TrickplayManager.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs index b090745bf1..69f10b43bd 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs +++ b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs @@ -91,7 +91,7 @@ public class TrickplayImagesTask : IScheduledTask while (startIndex < numberOfVideos) { query.StartIndex = startIndex; - var videos = _libraryManager.GetItemList(query).OfType<Video>().ToList(); + var videos = _libraryManager.GetItemList(query).OfType<Video>(); foreach (var video in videos) { diff --git a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs index 9fe3a330a0..3ea7c00d07 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs +++ b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs @@ -247,7 +247,7 @@ public class TrickplayManager : ITrickplayManager /* * Move trickplay tiles to output directory */ - Directory.CreateDirectory(outputDir); + Directory.CreateDirectory(Directory.GetParent(outputDir)!.FullName); // Replace existing tile grids if they already exist if (Directory.Exists(outputDir)) From 1ed5f0a624abeebef16a960ed7a52942bce47502 Mon Sep 17 00:00:00 2001 From: Niels van Velzen <git@ndat.nl> Date: Sat, 24 Jun 2023 09:25:25 +0200 Subject: [PATCH 400/858] Move line break characters to static readonly string array in TxtLyricParser --- MediaBrowser.Providers/Lyric/TxtLyricParser.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Providers/Lyric/TxtLyricParser.cs b/MediaBrowser.Providers/Lyric/TxtLyricParser.cs index 2ed0a6d8a6..7e029ee42a 100644 --- a/MediaBrowser.Providers/Lyric/TxtLyricParser.cs +++ b/MediaBrowser.Providers/Lyric/TxtLyricParser.cs @@ -12,6 +12,7 @@ namespace MediaBrowser.Providers.Lyric; public class TxtLyricParser : ILyricParser { private static readonly string[] _supportedMediaTypes = { "lrc", "elrc", "txt" }; + private static readonly string[] _lineBreakCharacters = { "\r\n", "\r", "\n" }; /// <inheritdoc /> public string Name => "TxtLyricProvider"; @@ -30,7 +31,7 @@ public class TxtLyricParser : ILyricParser return null; } - string[] lyricTextLines = lyrics.Content.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); + string[] lyricTextLines = lyrics.Content.Split(_lineBreakCharacters, StringSplitOptions.None); if (lyricTextLines.Length == 0) { From 45ed1d9f880774031532a0fbb5a2a9e846afe066 Mon Sep 17 00:00:00 2001 From: Sushil Shrestha <sushilshrestha940@gmail.com> Date: Fri, 23 Jun 2023 14:29:41 +0000 Subject: [PATCH 401/858] Translated using Weblate (Nepali) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ne/ --- .../Localization/Core/ne.json | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/ne.json b/Emby.Server.Implementations/Localization/Core/ne.json index 4c8e820a5c..7c6b08fb36 100644 --- a/Emby.Server.Implementations/Localization/Core/ne.json +++ b/Emby.Server.Implementations/Localization/Core/ne.json @@ -109,5 +109,19 @@ "Sync": "समकालीन", "SubtitleDownloadFailureFromForItem": "उपशीर्षकहरू {0} बाट {1} को लागि डाउनलोड गर्न असफल", "PluginUpdatedWithName": "{0} अद्यावधिक गरिएको थियो", - "PluginUninstalledWithName": "{0} को स्थापना रद्द गरिएको थियो" + "PluginUninstalledWithName": "{0} को स्थापना रद्द गरिएको थियो", + "HearingImpaired": "सुन्न नसक्ने", + "TaskUpdatePluginsDescription": "स्वचालित रूपमा अद्यावधिक गर्न कन्फिगर गरिएका प्लगइनहरूका लागि अद्यावधिकहरू डाउनलोड र स्थापना गर्दछ।", + "TaskCleanTranscode": "सफा ट्रान्सकोड निर्देशिका", + "TaskCleanTranscodeDescription": "एक दिन भन्दा पुराना ट्रान्सकोड फाइलहरू मेटाउँछ।", + "TaskRefreshChannels": "च्यानलहरू ताजा गर्नुहोस्", + "TaskDownloadMissingSubtitlesDescription": "मेटाडेटा कन्फिगरेसनमा आधारित हराइरहेको उपशीर्षकहरूको लागि इन्टरनेट खोज्छ।", + "TaskOptimizeDatabase": "डेटाबेस अप्टिमाइज गर्नुहोस्", + "TaskOptimizeDatabaseDescription": "डाटाबेस कम्प्याक्ट र खाली ठाउँ काट्छ। पुस्तकालय स्क्यान गरेपछि वा डाटाबेस परिमार्जनलाई संकेत गर्ने अन्य परिवर्तनहरू गरेपछि यो कार्य चलाउँदा कार्यसम्पादनमा सुधार हुन सक्छ।", + "TaskKeyframeExtractorDescription": "थप सटीक एचएलएस प्लेलिस्टहरू सिर्जना गर्न भिडियो फाइलहरूबाट कीफ्रेमहरू निकाल्छ। यो कार्य लामो समय सम्म चल्न सक्छ।", + "TaskUpdatePlugins": "प्लगइनहरू अपडेट गर्नुहोस्", + "TaskRefreshPeopleDescription": "तपाईंको मिडिया लाइब्रेरीमा अभिनेता र निर्देशकहरूको लागि मेटाडेटा अपडेट गर्दछ।", + "TaskRefreshChannelsDescription": "इन्टरनेट च्यानल जानकारी ताजा गर्दछ।", + "TaskDownloadMissingSubtitles": "छुटेका उपशीर्षकहरू डाउनलोड गर्नुहोस्", + "TaskKeyframeExtractor": "कीफ्रेम एक्स्ट्रक्टर" } From df880ff785287714e9517ee70ed4a114b867ff0a Mon Sep 17 00:00:00 2001 From: nyanmisaka <nst799610810@gmail.com> Date: Sun, 25 Jun 2023 21:39:25 +0800 Subject: [PATCH 402/858] Apply suggestions from code review Signed-off-by: nyanmisaka <nst799610810@gmail.com> --- Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index b66aa69350..480267b512 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -597,12 +597,8 @@ public class DynamicHlsHelper } if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase) - || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)) - { - profileString ??= "main"; - } - - if (string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase)) + || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase) + || string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase)) { profileString ??= "main"; } From d2e88c92cf2ff2ba6b23f7a882ec4a6ae00315ba Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 26 Jun 2023 19:55:55 +0000 Subject: [PATCH 403/858] chore(deps): update dependency sharpfuzz to v2.1.0 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index b9c1a8dcb4..c335839a6c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -64,7 +64,7 @@ <PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" /> <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.0.1" /> <PackageVersion Include="SerilogAnalyzer" Version="0.15.0" /> - <PackageVersion Include="SharpFuzz" Version="2.0.2" /> + <PackageVersion Include="SharpFuzz" Version="2.1.0" /> <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.3" /> <PackageVersion Include="SkiaSharp.Svg" Version="1.60.0" /> <PackageVersion Include="SkiaSharp" Version="2.88.3" /> From ab20ceaad65b2e72fe6e823aa6086e2c6ac36844 Mon Sep 17 00:00:00 2001 From: Nick <20588554+nicknsy@users.noreply.github.com> Date: Mon, 26 Jun 2023 17:40:10 -0700 Subject: [PATCH 404/858] Migrate to trickplay table to EF. Rename vars/methods/members to have consistent use of tile and thumbnail --- .../ApplicationHost.cs | 3 - .../Data/SqliteItemRepository.cs | 124 ---- Emby.Server.Implementations/Dto/DtoService.cs | 10 +- .../Controllers/TrickplayController.cs | 19 +- Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 18 +- Jellyfin.Data/Entities/TrickplayInfo.cs | 75 ++ .../JellyfinDbContext.cs | 5 + ...230626233818_AddTrickplayInfos.Designer.cs | 681 ++++++++++++++++++ .../20230626233818_AddTrickplayInfos.cs | 40 + .../Migrations/JellyfinDbModelSnapshot.cs | 35 +- .../TrickplayInfoConfiguration.cs | 18 + .../Trickplay/TrickplayManager.cs | 151 ++-- Jellyfin.Server/CoreAppHost.cs | 3 + .../Drawing/IImageEncoder.cs | 13 +- .../Persistence/IItemRepository.cs | 21 - .../Trickplay/ITrickplayManager.cs | 30 +- MediaBrowser.Model/Dto/BaseItemDto.cs | 3 +- .../Entities/TrickplayTilesInfo.cs | 49 -- src/Jellyfin.Drawing.Skia/SkiaEncoder.cs | 2 +- src/Jellyfin.Drawing/NullImageEncoder.cs | 2 +- 20 files changed, 1004 insertions(+), 298 deletions(-) create mode 100644 Jellyfin.Data/Entities/TrickplayInfo.cs create mode 100644 Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.Designer.cs create mode 100644 Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.cs create mode 100644 Jellyfin.Server.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs rename {MediaBrowser.Providers => Jellyfin.Server.Implementations}/Trickplay/TrickplayManager.cs (68%) delete mode 100644 MediaBrowser.Model/Entities/TrickplayTilesInfo.cs diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 1e0bb0cd6f..7969577bc0 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -78,7 +78,6 @@ using MediaBrowser.Controller.Session; using MediaBrowser.Controller.Sorting; using MediaBrowser.Controller.Subtitles; using MediaBrowser.Controller.SyncPlay; -using MediaBrowser.Controller.Trickplay; using MediaBrowser.Controller.TV; using MediaBrowser.LocalMetadata.Savers; using MediaBrowser.MediaEncoding.BdInfo; @@ -97,7 +96,6 @@ using MediaBrowser.Providers.Lyric; using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Plugins.Tmdb; using MediaBrowser.Providers.Subtitles; -using MediaBrowser.Providers.Trickplay; using MediaBrowser.XbmcMetadata.Providers; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -593,7 +591,6 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>(); serviceCollection.AddSingleton<IChapterManager, ChapterManager>(); - serviceCollection.AddSingleton<ITrickplayManager, TrickplayManager>(); serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>(); diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 8ec24522b0..d1fbea95ac 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -26,7 +26,6 @@ using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Extensions; -using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Playlists; @@ -48,7 +47,6 @@ namespace Emby.Server.Implementations.Data { private const string FromText = " from TypedBaseItems A"; private const string ChaptersTableName = "Chapters2"; - private const string TrickplayTableName = "Trickplay"; private const string SaveItemCommandText = @"replace into TypedBaseItems @@ -384,8 +382,6 @@ namespace Emby.Server.Implementations.Data "create table if not exists " + ChaptersTableName + " (ItemId GUID, ChapterIndex INT NOT NULL, StartPositionTicks BIGINT NOT NULL, Name TEXT, ImagePath TEXT, PRIMARY KEY (ItemId, ChapterIndex))", - "create table if not exists " + TrickplayTableName + " (ItemId GUID, Width INT NOT NULL, Height INT NOT NULL, TileWidth INT NOT NULL, TileHeight INT NOT NULL, TileCount INT NOT NULL, Interval INT NOT NULL, Bandwidth INT NOT NULL, PRIMARY KEY (ItemId, Width))", - CreateMediaStreamsTableCommand, CreateMediaAttachmentsTableCommand, @@ -2138,126 +2134,6 @@ namespace Emby.Server.Implementations.Data } } - /// <inheritdoc /> - public Dictionary<int, TrickplayTilesInfo> GetTilesResolutions(Guid itemId) - { - CheckDisposed(); - - var tilesResolutions = new Dictionary<int, TrickplayTilesInfo>(); - using (var connection = GetConnection(true)) - { - using (var statement = PrepareStatement(connection, "select Width,Height,TileWidth,TileHeight,TileCount,Interval,Bandwidth from " + TrickplayTableName + " where ItemId = @ItemId order by Width asc")) - { - statement.TryBind("@ItemId", itemId); - - foreach (var row in statement.ExecuteQuery()) - { - TrickplayTilesInfo tilesInfo = GetTrickplayTilesInfo(row); - tilesResolutions[tilesInfo.Width] = tilesInfo; - } - } - } - - return tilesResolutions; - } - - /// <inheritdoc /> - public void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo) - { - CheckDisposed(); - - ArgumentNullException.ThrowIfNull(tilesInfo); - - var idBlob = itemId.ToByteArray(); - using (var connection = GetConnection(false)) - { - connection.RunInTransaction( - db => - { - // Delete old tiles info - db.Execute("delete from " + TrickplayTableName + " where ItemId=@ItemId and Width=@Width", idBlob, tilesInfo.Width); - db.Execute( - "insert into " + TrickplayTableName + " values (@ItemId, @Width, @Height, @TileWidth, @TileHeight, @TileCount, @Interval, @Bandwidth)", - idBlob, - tilesInfo.Width, - tilesInfo.Height, - tilesInfo.TileWidth, - tilesInfo.TileHeight, - tilesInfo.TileCount, - tilesInfo.Interval, - tilesInfo.Bandwidth); - }, - TransactionMode); - } - } - - /// <inheritdoc /> - public Dictionary<Guid, Dictionary<int, TrickplayTilesInfo>> GetTrickplayManifest(BaseItem item) - { - CheckDisposed(); - - var trickplayManifest = new Dictionary<Guid, Dictionary<int, TrickplayTilesInfo>>(); - foreach (var mediaSource in item.GetMediaSources(false)) - { - var mediaSourceId = Guid.Parse(mediaSource.Id); - var tilesResolutions = GetTilesResolutions(mediaSourceId); - - if (tilesResolutions.Count > 0) - { - trickplayManifest[mediaSourceId] = tilesResolutions; - } - } - - return trickplayManifest; - } - - /// <summary> - /// Gets the trickplay tiles info. - /// </summary> - /// <param name="reader">The reader.</param> - /// <returns>TrickplayTilesInfo.</returns> - private TrickplayTilesInfo GetTrickplayTilesInfo(IReadOnlyList<ResultSetValue> reader) - { - var tilesInfo = new TrickplayTilesInfo(); - - if (reader.TryGetInt32(0, out var width)) - { - tilesInfo.Width = width; - } - - if (reader.TryGetInt32(1, out var height)) - { - tilesInfo.Height = height; - } - - if (reader.TryGetInt32(2, out var tileWidth)) - { - tilesInfo.TileWidth = tileWidth; - } - - if (reader.TryGetInt32(3, out var tileHeight)) - { - tilesInfo.TileHeight = tileHeight; - } - - if (reader.TryGetInt32(4, out var tileCount)) - { - tilesInfo.TileCount = tileCount; - } - - if (reader.TryGetInt32(5, out var interval)) - { - tilesInfo.Interval = interval; - } - - if (reader.TryGetInt32(6, out var bandwidth)) - { - tilesInfo.Bandwidth = bandwidth; - } - - return tilesInfo; - } - private static bool EnableJoinUserData(InternalItemsQuery query) { if (query.User is null) diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 1687fa442b..933b95df04 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -22,6 +22,7 @@ using MediaBrowser.Controller.Lyrics; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Providers; +using MediaBrowser.Controller.Trickplay; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; @@ -52,6 +53,7 @@ namespace Emby.Server.Implementations.Dto private readonly Lazy<ILiveTvManager> _livetvManagerFactory; private readonly ILyricManager _lyricManager; + private readonly ITrickplayManager _trickplayManager; public DtoService( ILogger<DtoService> logger, @@ -63,7 +65,8 @@ namespace Emby.Server.Implementations.Dto IApplicationHost appHost, IMediaSourceManager mediaSourceManager, Lazy<ILiveTvManager> livetvManagerFactory, - ILyricManager lyricManager) + ILyricManager lyricManager, + ITrickplayManager trickplayManager) { _logger = logger; _libraryManager = libraryManager; @@ -75,6 +78,7 @@ namespace Emby.Server.Implementations.Dto _mediaSourceManager = mediaSourceManager; _livetvManagerFactory = livetvManagerFactory; _lyricManager = lyricManager; + _trickplayManager = trickplayManager; } private ILiveTvManager LivetvManager => _livetvManagerFactory.Value; @@ -1060,9 +1064,11 @@ namespace Emby.Server.Implementations.Dto if (options.ContainsField(ItemFields.Trickplay)) { + var manifest = _trickplayManager.GetTrickplayManifest(item).ConfigureAwait(false).GetAwaiter().GetResult(); + // To stay consistent with other fields, this must go from a Guid to a non-dashed string. // This does not seem to occur automatically to dictionaries like it does with other Guid fields. - dto.Trickplay = _itemRepo.GetTrickplayManifest(item).ToDictionary(x => x.Key.ToString("N", CultureInfo.InvariantCulture), y => y.Value); + dto.Trickplay = manifest.ToDictionary(x => x.Key.ToString("N", CultureInfo.InvariantCulture), y => y.Value); } if (video.ExtraType.HasValue) diff --git a/Jellyfin.Api/Controllers/TrickplayController.cs b/Jellyfin.Api/Controllers/TrickplayController.cs index 36464d726e..e4f8f076e4 100644 --- a/Jellyfin.Api/Controllers/TrickplayController.cs +++ b/Jellyfin.Api/Controllers/TrickplayController.cs @@ -2,6 +2,7 @@ using System; using System.ComponentModel.DataAnnotations; using System.Net.Mime; using System.Text; +using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Extensions; using MediaBrowser.Controller.Library; @@ -42,18 +43,18 @@ public class TrickplayController : BaseJellyfinApiController /// <param name="itemId">The item id.</param> /// <param name="width">The width of a single tile.</param> /// <param name="mediaSourceId">The media version id, if using an alternate version.</param> - /// <response code="200">Tiles stream returned.</response> - /// <returns>A <see cref="FileResult"/> containing the trickplay tiles file.</returns> + /// <response code="200">Tiles playlist returned.</response> + /// <returns>A <see cref="FileResult"/> containing the trickplay playlist file.</returns> [HttpGet("Videos/{itemId}/Trickplay/{width}/tiles.m3u8")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesPlaylistFile] - public ActionResult GetTrickplayHlsPlaylist( + public async Task<ActionResult> GetTrickplayHlsPlaylist( [FromRoute, Required] Guid itemId, [FromRoute, Required] int width, [FromQuery] Guid? mediaSourceId) { - string? playlist = _trickplayManager.GetHlsPlaylist(mediaSourceId ?? itemId, width, User.GetToken()); + string? playlist = await _trickplayManager.GetHlsPlaylist(mediaSourceId ?? itemId, width, User.GetToken()).ConfigureAwait(false); if (string.IsNullOrEmpty(playlist)) { @@ -64,20 +65,20 @@ public class TrickplayController : BaseJellyfinApiController } /// <summary> - /// Gets a trickplay tile grid image. + /// Gets a trickplay tile image. /// </summary> /// <param name="itemId">The item id.</param> /// <param name="width">The width of a single tile.</param> - /// <param name="index">The index of the desired tile grid.</param> + /// <param name="index">The index of the desired tile.</param> /// <param name="mediaSourceId">The media version id, if using an alternate version.</param> - /// <response code="200">Tiles image returned.</response> - /// <response code="200">Tiles image not found at specified index.</response> + /// <response code="200">Tile image returned.</response> + /// <response code="200">Tile image not found at specified index.</response> /// <returns>A <see cref="FileResult"/> containing the trickplay tiles image.</returns> [HttpGet("Videos/{itemId}/Trickplay/{width}/{index}.jpg")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesImageFile] - public ActionResult GetTrickplayGridImage( + public ActionResult GetTrickplayTileImage( [FromRoute, Required] Guid itemId, [FromRoute, Required] int width, [FromRoute, Required] int index, diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index b1657aeae2..bd30910558 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -308,8 +308,8 @@ public class DynamicHlsHelper if (!isLiveStream && (state.VideoRequest?.EnableTrickplay).GetValueOrDefault(false)) { var sourceId = Guid.Parse(state.Request.MediaSourceId); - var tilesResolutions = _trickplayManager.GetTilesResolutions(sourceId); - AddTrickplay(state, tilesResolutions, builder, _httpContextAccessor.HttpContext.User); + var trickplayResolutions = await _trickplayManager.GetTrickplayResolutions(sourceId).ConfigureAwait(false); + AddTrickplay(state, trickplayResolutions, builder, _httpContextAccessor.HttpContext.User); } return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8")); @@ -544,17 +544,17 @@ public class DynamicHlsHelper /// Appends EXT-X-IMAGE-STREAM-INF playlists for each available trickplay resolution. /// </summary> /// <param name="state">StreamState of the current stream.</param> - /// <param name="tilesResolutions">Dictionary of widths to corresponding tiles info.</param> + /// <param name="trickplayResolutions">Dictionary of widths to corresponding tiles info.</param> /// <param name="builder">StringBuilder to append the field to.</param> /// <param name="user">Http user context.</param> - private void AddTrickplay(StreamState state, Dictionary<int, TrickplayTilesInfo> tilesResolutions, StringBuilder builder, ClaimsPrincipal user) + private void AddTrickplay(StreamState state, Dictionary<int, TrickplayInfo> trickplayResolutions, StringBuilder builder, ClaimsPrincipal user) { const string playlistFormat = "#EXT-X-IMAGE-STREAM-INF:BANDWIDTH={0},RESOLUTION={1}x{2},CODECS=\"jpeg\",URI=\"{3}\""; - foreach (var resolution in tilesResolutions) + foreach (var resolution in trickplayResolutions) { var width = resolution.Key; - var tilesInfo = resolution.Value; + var trickplayInfo = resolution.Value; var url = string.Format( CultureInfo.InvariantCulture, @@ -566,9 +566,9 @@ public class DynamicHlsHelper var line = string.Format( CultureInfo.InvariantCulture, playlistFormat, - tilesInfo.Bandwidth.ToString(CultureInfo.InvariantCulture), - tilesInfo.Width.ToString(CultureInfo.InvariantCulture), - tilesInfo.Height.ToString(CultureInfo.InvariantCulture), + trickplayInfo.Bandwidth.ToString(CultureInfo.InvariantCulture), + trickplayInfo.Width.ToString(CultureInfo.InvariantCulture), + trickplayInfo.Height.ToString(CultureInfo.InvariantCulture), url); builder.AppendLine(line); diff --git a/Jellyfin.Data/Entities/TrickplayInfo.cs b/Jellyfin.Data/Entities/TrickplayInfo.cs new file mode 100644 index 0000000000..64e7da1b5d --- /dev/null +++ b/Jellyfin.Data/Entities/TrickplayInfo.cs @@ -0,0 +1,75 @@ +using System; +using System.Text.Json.Serialization; + +namespace Jellyfin.Data.Entities; + +/// <summary> +/// An entity representing the metadata for a group of trickplay tiles. +/// </summary> +public class TrickplayInfo +{ + /// <summary> + /// Gets or sets the id of the associated item. + /// </summary> + /// <remarks> + /// Required. + /// </remarks> + [JsonIgnore] + public Guid ItemId { get; set; } + + /// <summary> + /// Gets or sets width of an individual thumbnail. + /// </summary> + /// <remarks> + /// Required. + /// </remarks> + public int Width { get; set; } + + /// <summary> + /// Gets or sets height of an individual thumbnail. + /// </summary> + /// <remarks> + /// Required. + /// </remarks> + public int Height { get; set; } + + /// <summary> + /// Gets or sets amount of thumbnails per row. + /// </summary> + /// <remarks> + /// Required. + /// </remarks> + public int TileWidth { get; set; } + + /// <summary> + /// Gets or sets amount of thumbnails per column. + /// </summary> + /// <remarks> + /// Required. + /// </remarks> + public int TileHeight { get; set; } + + /// <summary> + /// Gets or sets total amount of non-black thumbnails. + /// </summary> + /// <remarks> + /// Required. + /// </remarks> + public int ThumbnailCount { get; set; } + + /// <summary> + /// Gets or sets interval in milliseconds between each trickplay thumbnail. + /// </summary> + /// <remarks> + /// Required. + /// </remarks> + public int Interval { get; set; } + + /// <summary> + /// Gets or sets peak bandwith usage in bits per second. + /// </summary> + /// <remarks> + /// Required. + /// </remarks> + public int Bandwidth { get; set; } +} diff --git a/Jellyfin.Server.Implementations/JellyfinDbContext.cs b/Jellyfin.Server.Implementations/JellyfinDbContext.cs index 0d91707e3e..ea99af0047 100644 --- a/Jellyfin.Server.Implementations/JellyfinDbContext.cs +++ b/Jellyfin.Server.Implementations/JellyfinDbContext.cs @@ -78,6 +78,11 @@ public class JellyfinDbContext : DbContext /// </summary> public DbSet<User> Users => Set<User>(); + /// <summary> + /// Gets the <see cref="DbSet{TEntity}"/> containing the trickplay metadata. + /// </summary> + public DbSet<TrickplayInfo> TrickplayInfos => Set<TrickplayInfo>(); + /*public DbSet<Artwork> Artwork => Set<Artwork>(); public DbSet<Book> Books => Set<Book>(); diff --git a/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.Designer.cs new file mode 100644 index 0000000000..28baf19925 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.Designer.cs @@ -0,0 +1,681 @@ +// <auto-generated /> +using System; +using Jellyfin.Server.Implementations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + [DbContext(typeof(JellyfinDbContext))] + [Migration("20230626233818_AddTrickplayInfos")] + partial class AddTrickplayInfos + { + /// <inheritdoc /> + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.7"); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<int>("DayOfWeek") + .HasColumnType("INTEGER"); + + b.Property<double>("EndHour") + .HasColumnType("REAL"); + + b.Property<double>("StartHour") + .HasColumnType("REAL"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccessSchedules"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<DateTime>("DateCreated") + .HasColumnType("TEXT"); + + b.Property<string>("ItemId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property<int>("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property<string>("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property<string>("Overview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property<uint>("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property<string>("ShortOverview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property<string>("Type") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DateCreated"); + + b.ToTable("ActivityLogs"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<string>("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<Guid>("ItemId") + .HasColumnType("TEXT"); + + b.Property<string>("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.Property<string>("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client", "Key") + .IsUnique(); + + b.ToTable("CustomItemDisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<int>("ChromecastVersion") + .HasColumnType("INTEGER"); + + b.Property<string>("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<string>("DashboardTheme") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<bool>("EnableNextVideoInfoOverlay") + .HasColumnType("INTEGER"); + + b.Property<int?>("IndexBy") + .HasColumnType("INTEGER"); + + b.Property<Guid>("ItemId") + .HasColumnType("TEXT"); + + b.Property<int>("ScrollDirection") + .HasColumnType("INTEGER"); + + b.Property<bool>("ShowBackdrop") + .HasColumnType("INTEGER"); + + b.Property<bool>("ShowSidebar") + .HasColumnType("INTEGER"); + + b.Property<int>("SkipBackwardLength") + .HasColumnType("INTEGER"); + + b.Property<int>("SkipForwardLength") + .HasColumnType("INTEGER"); + + b.Property<string>("TvHome") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client") + .IsUnique(); + + b.ToTable("DisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<int>("DisplayPreferencesId") + .HasColumnType("INTEGER"); + + b.Property<int>("Order") + .HasColumnType("INTEGER"); + + b.Property<int>("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DisplayPreferencesId"); + + b.ToTable("HomeSection"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<DateTime>("LastModified") + .HasColumnType("TEXT"); + + b.Property<string>("Path") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property<Guid?>("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("ImageInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<string>("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<int?>("IndexBy") + .HasColumnType("INTEGER"); + + b.Property<Guid>("ItemId") + .HasColumnType("TEXT"); + + b.Property<bool>("RememberIndexing") + .HasColumnType("INTEGER"); + + b.Property<bool>("RememberSorting") + .HasColumnType("INTEGER"); + + b.Property<string>("SortBy") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property<int>("SortOrder") + .HasColumnType("INTEGER"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.Property<int>("ViewType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemDisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<int>("Kind") + .HasColumnType("INTEGER"); + + b.Property<Guid?>("Permission_Permissions_Guid") + .HasColumnType("TEXT"); + + b.Property<uint>("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property<Guid?>("UserId") + .HasColumnType("TEXT"); + + b.Property<bool>("Value") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<int>("Kind") + .HasColumnType("INTEGER"); + + b.Property<Guid?>("Preference_Preferences_Guid") + .HasColumnType("TEXT"); + + b.Property<uint>("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property<Guid?>("UserId") + .HasColumnType("TEXT"); + + b.Property<string>("Value") + .IsRequired() + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Preferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<string>("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property<DateTime>("DateCreated") + .HasColumnType("TEXT"); + + b.Property<DateTime>("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property<string>("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccessToken") + .IsUnique(); + + b.ToTable("ApiKeys"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<string>("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property<string>("AppName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property<string>("AppVersion") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<DateTime>("DateCreated") + .HasColumnType("TEXT"); + + b.Property<DateTime>("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property<DateTime>("DateModified") + .HasColumnType("TEXT"); + + b.Property<string>("DeviceId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property<string>("DeviceName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property<bool>("IsActive") + .HasColumnType("INTEGER"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("AccessToken", "DateLastActivity"); + + b.HasIndex("DeviceId", "DateLastActivity"); + + b.HasIndex("UserId", "DeviceId"); + + b.ToTable("Devices"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<string>("CustomName") + .HasColumnType("TEXT"); + + b.Property<string>("DeviceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.ToTable("DeviceOptions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b => + { + b.Property<Guid>("ItemId") + .HasColumnType("TEXT"); + + b.Property<int>("Width") + .HasColumnType("INTEGER"); + + b.Property<int>("Bandwidth") + .HasColumnType("INTEGER"); + + b.Property<int>("Height") + .HasColumnType("INTEGER"); + + b.Property<int>("Interval") + .HasColumnType("INTEGER"); + + b.Property<int>("ThumbnailCount") + .HasColumnType("INTEGER"); + + b.Property<int>("TileHeight") + .HasColumnType("INTEGER"); + + b.Property<int>("TileWidth") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "Width"); + + b.ToTable("TrickplayInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property<string>("AudioLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property<string>("AuthenticationProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property<bool>("DisplayCollectionsView") + .HasColumnType("INTEGER"); + + b.Property<bool>("DisplayMissingEpisodes") + .HasColumnType("INTEGER"); + + b.Property<bool>("EnableAutoLogin") + .HasColumnType("INTEGER"); + + b.Property<bool>("EnableLocalPassword") + .HasColumnType("INTEGER"); + + b.Property<bool>("EnableNextEpisodeAutoPlay") + .HasColumnType("INTEGER"); + + b.Property<bool>("EnableUserPreferenceAccess") + .HasColumnType("INTEGER"); + + b.Property<bool>("HidePlayedInLatest") + .HasColumnType("INTEGER"); + + b.Property<long>("InternalId") + .HasColumnType("INTEGER"); + + b.Property<int>("InvalidLoginAttemptCount") + .HasColumnType("INTEGER"); + + b.Property<DateTime?>("LastActivityDate") + .HasColumnType("TEXT"); + + b.Property<DateTime?>("LastLoginDate") + .HasColumnType("TEXT"); + + b.Property<int?>("LoginAttemptsBeforeLockout") + .HasColumnType("INTEGER"); + + b.Property<int>("MaxActiveSessions") + .HasColumnType("INTEGER"); + + b.Property<int?>("MaxParentalAgeRating") + .HasColumnType("INTEGER"); + + b.Property<bool>("MustUpdatePassword") + .HasColumnType("INTEGER"); + + b.Property<string>("Password") + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.Property<string>("PasswordResetProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property<bool>("PlayDefaultAudioTrack") + .HasColumnType("INTEGER"); + + b.Property<bool>("RememberAudioSelections") + .HasColumnType("INTEGER"); + + b.Property<bool>("RememberSubtitleSelections") + .HasColumnType("INTEGER"); + + b.Property<int?>("RemoteClientBitrateLimit") + .HasColumnType("INTEGER"); + + b.Property<uint>("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property<string>("SubtitleLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property<int>("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property<int>("SyncPlayAccess") + .HasColumnType("INTEGER"); + + b.Property<string>("Username") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("AccessSchedules") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("DisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null) + .WithMany("HomeSections") + .HasForeignKey("DisplayPreferencesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithOne("ProfileImage") + .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("ItemDisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Permissions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Preferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => + { + b.HasOne("Jellyfin.Data.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Navigation("HomeSections"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Navigation("AccessSchedules"); + + b.Navigation("DisplayPreferences"); + + b.Navigation("ItemDisplayPreferences"); + + b.Navigation("Permissions"); + + b.Navigation("Preferences"); + + b.Navigation("ProfileImage"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.cs b/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.cs new file mode 100644 index 0000000000..76b12de083 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.cs @@ -0,0 +1,40 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + /// <inheritdoc /> + public partial class AddTrickplayInfos : Migration + { + /// <inheritdoc /> + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "TrickplayInfos", + columns: table => new + { + ItemId = table.Column<Guid>(type: "TEXT", nullable: false), + Width = table.Column<int>(type: "INTEGER", nullable: false), + Height = table.Column<int>(type: "INTEGER", nullable: false), + TileWidth = table.Column<int>(type: "INTEGER", nullable: false), + TileHeight = table.Column<int>(type: "INTEGER", nullable: false), + ThumbnailCount = table.Column<int>(type: "INTEGER", nullable: false), + Interval = table.Column<int>(type: "INTEGER", nullable: false), + Bandwidth = table.Column<int>(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TrickplayInfos", x => new { x.ItemId, x.Width }); + }); + } + + /// <inheritdoc /> + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TrickplayInfos"); + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs index d23508096f..3c06e1cfc9 100644 --- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs +++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs @@ -1,4 +1,4 @@ -// <auto-generated /> +// <auto-generated /> using System; using Jellyfin.Server.Implementations; using Microsoft.EntityFrameworkCore; @@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "7.0.5"); + modelBuilder.HasAnnotation("ProductVersion", "7.0.7"); modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => { @@ -442,6 +442,37 @@ namespace Jellyfin.Server.Implementations.Migrations b.ToTable("DeviceOptions"); }); + modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b => + { + b.Property<Guid>("ItemId") + .HasColumnType("TEXT"); + + b.Property<int>("Width") + .HasColumnType("INTEGER"); + + b.Property<int>("Bandwidth") + .HasColumnType("INTEGER"); + + b.Property<int>("Height") + .HasColumnType("INTEGER"); + + b.Property<int>("Interval") + .HasColumnType("INTEGER"); + + b.Property<int>("ThumbnailCount") + .HasColumnType("INTEGER"); + + b.Property<int>("TileHeight") + .HasColumnType("INTEGER"); + + b.Property<int>("TileWidth") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "Width"); + + b.ToTable("TrickplayInfos"); + }); + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => { b.Property<Guid>("Id") diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs new file mode 100644 index 0000000000..dc1c17e5ef --- /dev/null +++ b/Jellyfin.Server.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs @@ -0,0 +1,18 @@ +using Jellyfin.Data.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Jellyfin.Server.Implementations.ModelConfiguration +{ + /// <summary> + /// FluentAPI configuration for the TrickplayInfo entity. + /// </summary> + public class TrickplayInfoConfiguration : IEntityTypeConfiguration<TrickplayInfo> + { + /// <inheritdoc/> + public void Configure(EntityTypeBuilder<TrickplayInfo> builder) + { + builder.HasKey(info => new { info.ItemId, info.Width }); + } + } +} diff --git a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs similarity index 68% rename from MediaBrowser.Providers/Trickplay/TrickplayManager.cs rename to Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs index 3ea7c00d07..34b27e21a8 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs +++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs @@ -6,19 +6,20 @@ using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Entities; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Trickplay; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace MediaBrowser.Providers.Trickplay; +namespace Jellyfin.Server.Implementations.Trickplay; /// <summary> /// ITrickplayManager implementation. @@ -26,13 +27,13 @@ namespace MediaBrowser.Providers.Trickplay; public class TrickplayManager : ITrickplayManager { private readonly ILogger<TrickplayManager> _logger; - private readonly IItemRepository _itemRepo; private readonly IMediaEncoder _mediaEncoder; private readonly IFileSystem _fileSystem; private readonly EncodingHelper _encodingHelper; private readonly ILibraryManager _libraryManager; private readonly IServerConfigurationManager _config; private readonly IImageEncoder _imageEncoder; + private readonly IDbContextFactory<JellyfinDbContext> _dbProvider; private static readonly SemaphoreSlim _resourcePool = new(1, 1); private static readonly string[] _trickplayImgExtensions = { ".jpg" }; @@ -41,31 +42,31 @@ public class TrickplayManager : ITrickplayManager /// Initializes a new instance of the <see cref="TrickplayManager"/> class. /// </summary> /// <param name="logger">The logger.</param> - /// <param name="itemRepo">The item repository.</param> /// <param name="mediaEncoder">The media encoder.</param> /// <param name="fileSystem">The file systen.</param> /// <param name="encodingHelper">The encoding helper.</param> /// <param name="libraryManager">The library manager.</param> /// <param name="config">The server configuration manager.</param> /// <param name="imageEncoder">The image encoder.</param> + /// <param name="dbProvider">The database provider.</param> public TrickplayManager( ILogger<TrickplayManager> logger, - IItemRepository itemRepo, IMediaEncoder mediaEncoder, IFileSystem fileSystem, EncodingHelper encodingHelper, ILibraryManager libraryManager, IServerConfigurationManager config, - IImageEncoder imageEncoder) + IImageEncoder imageEncoder, + IDbContextFactory<JellyfinDbContext> dbProvider) { _logger = logger; - _itemRepo = itemRepo; _mediaEncoder = mediaEncoder; _fileSystem = fileSystem; _encodingHelper = encodingHelper; _libraryManager = libraryManager; _config = config; _imageEncoder = imageEncoder; + _dbProvider = dbProvider; } /// <inheritdoc /> @@ -105,7 +106,7 @@ public class TrickplayManager : ITrickplayManager try { - if (!replace && Directory.Exists(outputDir) && GetTilesResolutions(video.Id).ContainsKey(width)) + if (!replace && Directory.Exists(outputDir) && (await GetTrickplayResolutions(video.Id).ConfigureAwait(false)).ContainsKey(width)) { _logger.LogDebug("Found existing trickplay files for {ItemId}. Exiting.", video.Id); return; @@ -152,14 +153,16 @@ public class TrickplayManager : ITrickplayManager // Create tiles var tilesTempDir = Path.Combine(imgTempDir, Guid.NewGuid().ToString("N")); - var tilesInfo = CreateTiles(images, width, options, tilesTempDir, outputDir); + var trickplayInfo = CreateTiles(images, width, options, tilesTempDir, outputDir); // Save tiles info try { - if (tilesInfo is not null) + if (trickplayInfo is not null) { - SaveTilesInfo(video.Id, tilesInfo); + trickplayInfo.ItemId = video.Id; + await SaveTrickplayInfo(trickplayInfo).ConfigureAwait(false); + _logger.LogInformation("Finished creation of trickplay files for {0}", mediaPath); } else @@ -191,7 +194,7 @@ public class TrickplayManager : ITrickplayManager } } - private TrickplayTilesInfo CreateTiles(List<string> images, int width, TrickplayOptions options, string workDir, string outputDir) + private TrickplayInfo CreateTiles(List<string> images, int width, TrickplayOptions options, string workDir, string outputDir) { if (images.Count == 0) { @@ -200,48 +203,48 @@ public class TrickplayManager : ITrickplayManager Directory.CreateDirectory(workDir); - var tilesInfo = new TrickplayTilesInfo + var trickplayInfo = new TrickplayInfo { Width = width, Interval = options.Interval, TileWidth = options.TileWidth, TileHeight = options.TileHeight, - TileCount = images.Count, + ThumbnailCount = images.Count, // Set during image generation Height = 0, Bandwidth = 0 }; /* - * Generate trickplay tile grids from sets of images + * Generate trickplay tiles from sets of thumbnails */ var imageOptions = new ImageCollageOptions { - Width = tilesInfo.TileWidth, - Height = tilesInfo.TileHeight + Width = trickplayInfo.TileWidth, + Height = trickplayInfo.TileHeight }; - var tilesPerGrid = tilesInfo.TileWidth * tilesInfo.TileHeight; - var requiredTileGrids = (int)Math.Ceiling((double)images.Count / tilesPerGrid); + var thumbnailsPerTile = trickplayInfo.TileWidth * trickplayInfo.TileHeight; + var requiredTiles = (int)Math.Ceiling((double)images.Count / thumbnailsPerTile); - for (int i = 0; i < requiredTileGrids; i++) + for (int i = 0; i < requiredTiles; i++) { // Set output/input paths - var tileGridPath = Path.Combine(workDir, $"{i}.jpg"); + var tilePath = Path.Combine(workDir, $"{i}.jpg"); - imageOptions.OutputPath = tileGridPath; - imageOptions.InputPaths = images.Skip(i * tilesPerGrid).Take(tilesPerGrid).ToList(); + imageOptions.OutputPath = tilePath; + imageOptions.InputPaths = images.Skip(i * thumbnailsPerTile).Take(thumbnailsPerTile).ToList(); // Generate image and use returned height for tiles info - var height = _imageEncoder.CreateTrickplayGrid(imageOptions, options.JpegQuality, tilesInfo.Width, tilesInfo.Height != 0 ? tilesInfo.Height : null); - if (tilesInfo.Height == 0) + var height = _imageEncoder.CreateTrickplayTile(imageOptions, options.JpegQuality, trickplayInfo.Width, trickplayInfo.Height != 0 ? trickplayInfo.Height : null); + if (trickplayInfo.Height == 0) { - tilesInfo.Height = height; + trickplayInfo.Height = height; } // Update bitrate - var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tileGridPath).Length * 8 / tilesInfo.TileWidth / tilesInfo.TileHeight / (tilesInfo.Interval / 1000)); - tilesInfo.Bandwidth = Math.Max(tilesInfo.Bandwidth, bitrate); + var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tilePath).Length * 8 / trickplayInfo.TileWidth / trickplayInfo.TileHeight / (trickplayInfo.Interval / 1000)); + trickplayInfo.Bandwidth = Math.Max(trickplayInfo.Bandwidth, bitrate); } /* @@ -249,7 +252,7 @@ public class TrickplayManager : ITrickplayManager */ Directory.CreateDirectory(Directory.GetParent(outputDir)!.FullName); - // Replace existing tile grids if they already exist + // Replace existing tiles if they already exist if (Directory.Exists(outputDir)) { Directory.Delete(outputDir, true); @@ -257,7 +260,7 @@ public class TrickplayManager : ITrickplayManager MoveDirectory(workDir, outputDir); - return tilesInfo; + return trickplayInfo; } private bool CanGenerateTrickplay(Video video, int interval) @@ -299,21 +302,62 @@ public class TrickplayManager : ITrickplayManager } /// <inheritdoc /> - public Dictionary<int, TrickplayTilesInfo> GetTilesResolutions(Guid itemId) + public async Task<Dictionary<int, TrickplayInfo>> GetTrickplayResolutions(Guid itemId) { - return _itemRepo.GetTilesResolutions(itemId); + var trickplayResolutions = new Dictionary<int, TrickplayInfo>(); + + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + var trickplayInfos = await dbContext.TrickplayInfos + .AsNoTracking() + .Where(i => i.ItemId.Equals(itemId)) + .ToListAsync() + .ConfigureAwait(false); + + foreach (var info in trickplayInfos) + { + trickplayResolutions[info.Width] = info; + } + } + + return trickplayResolutions; } /// <inheritdoc /> - public void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo) + public async Task SaveTrickplayInfo(TrickplayInfo info) { - _itemRepo.SaveTilesInfo(itemId, tilesInfo); + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + var oldInfo = await dbContext.TrickplayInfos.FindAsync(info.ItemId, info.Width).ConfigureAwait(false); + if (oldInfo is not null) + { + dbContext.TrickplayInfos.Remove(oldInfo); + } + + dbContext.Add(info); + + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } } /// <inheritdoc /> - public Dictionary<Guid, Dictionary<int, TrickplayTilesInfo>> GetTrickplayManifest(BaseItem item) + public async Task<Dictionary<Guid, Dictionary<int, TrickplayInfo>>> GetTrickplayManifest(BaseItem item) { - return _itemRepo.GetTrickplayManifest(item); + var trickplayManifest = new Dictionary<Guid, Dictionary<int, TrickplayInfo>>(); + foreach (var mediaSource in item.GetMediaSources(false)) + { + var mediaSourceId = Guid.Parse(mediaSource.Id); + var trickplayResolutions = await GetTrickplayResolutions(mediaSourceId).ConfigureAwait(false); + + if (trickplayResolutions.Count > 0) + { + trickplayManifest[mediaSourceId] = trickplayResolutions; + } + } + + return trickplayManifest; } /// <inheritdoc /> @@ -323,42 +367,41 @@ public class TrickplayManager : ITrickplayManager } /// <inheritdoc /> - public string? GetHlsPlaylist(Guid itemId, int width, string? apiKey) + public async Task<string?> GetHlsPlaylist(Guid itemId, int width, string? apiKey) { - var tilesResolutions = GetTilesResolutions(itemId); - if (tilesResolutions is not null && tilesResolutions.TryGetValue(width, out var tilesInfo)) + var trickplayResolutions = await GetTrickplayResolutions(itemId).ConfigureAwait(false); + if (trickplayResolutions is not null && trickplayResolutions.TryGetValue(width, out var trickplayInfo)) { var builder = new StringBuilder(128); - if (tilesInfo.TileCount > 0) + if (trickplayInfo.ThumbnailCount > 0) { const string urlFormat = "Trickplay/{0}/{1}.jpg?MediaSourceId={2}&api_key={3}"; const string decimalFormat = "{0:0.###}"; - var resolution = $"{tilesInfo.Width}x{tilesInfo.Height}"; - var layout = $"{tilesInfo.TileWidth}x{tilesInfo.TileHeight}"; - var tilesPerGrid = tilesInfo.TileWidth * tilesInfo.TileHeight; - var tileDuration = tilesInfo.Interval / 1000d; - var infDuration = tileDuration * tilesPerGrid; - var tileGridCount = (int)Math.Ceiling((decimal)tilesInfo.TileCount / tilesPerGrid); + var resolution = $"{trickplayInfo.Width}x{trickplayInfo.Height}"; + var layout = $"{trickplayInfo.TileWidth}x{trickplayInfo.TileHeight}"; + var thumbnailsPerTile = trickplayInfo.TileWidth * trickplayInfo.TileHeight; + var thumbnailDuration = trickplayInfo.Interval / 1000d; + var infDuration = thumbnailDuration * thumbnailsPerTile; + var tileCount = (int)Math.Ceiling((decimal)trickplayInfo.ThumbnailCount / thumbnailsPerTile); builder .AppendLine("#EXTM3U") .Append("#EXT-X-TARGETDURATION:") - .AppendLine(tileGridCount.ToString(CultureInfo.InvariantCulture)) + .AppendLine(tileCount.ToString(CultureInfo.InvariantCulture)) .AppendLine("#EXT-X-VERSION:7") .AppendLine("#EXT-X-MEDIA-SEQUENCE:1") .AppendLine("#EXT-X-PLAYLIST-TYPE:VOD") .AppendLine("#EXT-X-IMAGES-ONLY"); - for (int i = 0; i < tileGridCount; i++) + for (int i = 0; i < tileCount; i++) { - // All tile grids before the last one must contain full amount of tiles. - // The final grid will be 0 < count <= maxTiles - if (i == tileGridCount - 1) + // All tiles prior to the last must contain full amount of thumbnails (no black). + if (i == tileCount - 1) { - tilesPerGrid = tilesInfo.TileCount - (i * tilesPerGrid); - infDuration = tileDuration * tilesPerGrid; + thumbnailsPerTile = trickplayInfo.ThumbnailCount - (i * thumbnailsPerTile); + infDuration = thumbnailDuration * thumbnailsPerTile; } // EXTINF @@ -374,7 +417,7 @@ public class TrickplayManager : ITrickplayManager .Append(",LAYOUT=") .Append(layout) .Append(",DURATION=") - .AppendFormat(CultureInfo.InvariantCulture, decimalFormat, tileDuration) + .AppendFormat(CultureInfo.InvariantCulture, decimalFormat, thumbnailDuration) .AppendLine(); // URL diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index 939376dd8d..18d924aa8f 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -11,6 +11,7 @@ using Jellyfin.Server.Implementations.Activity; using Jellyfin.Server.Implementations.Devices; using Jellyfin.Server.Implementations.Events; using Jellyfin.Server.Implementations.Security; +using Jellyfin.Server.Implementations.Trickplay; using Jellyfin.Server.Implementations.Users; using MediaBrowser.Controller; using MediaBrowser.Controller.BaseItemManager; @@ -21,6 +22,7 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Lyrics; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Security; +using MediaBrowser.Controller.Trickplay; using MediaBrowser.Model.Activity; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -77,6 +79,7 @@ namespace Jellyfin.Server serviceCollection.AddSingleton<IUserManager, UserManager>(); serviceCollection.AddScoped<IDisplayPreferencesManager, DisplayPreferencesManager>(); serviceCollection.AddSingleton<IDeviceManager, DeviceManager>(); + serviceCollection.AddSingleton<ITrickplayManager, TrickplayManager>(); // TODO search the assemblies instead of adding them manually? serviceCollection.AddSingleton<IWebSocketListener, SessionWebSocketListener>(); diff --git a/MediaBrowser.Controller/Drawing/IImageEncoder.cs b/MediaBrowser.Controller/Drawing/IImageEncoder.cs index 42c680761d..c7bfbdb534 100644 --- a/MediaBrowser.Controller/Drawing/IImageEncoder.cs +++ b/MediaBrowser.Controller/Drawing/IImageEncoder.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Drawing; namespace MediaBrowser.Controller.Drawing @@ -84,13 +83,13 @@ namespace MediaBrowser.Controller.Drawing void CreateSplashscreen(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops); /// <summary> - /// Creates a new jpeg trickplay grid image. + /// Creates a new trickplay tile image. /// </summary> - /// <param name="options">The options to use when creating the image. Width and Height are a quantity of tiles in this case, not pixels.</param> + /// <param name="options">The options to use when creating the image. Width and Height are a quantity of thumbnails in this case, not pixels.</param> /// <param name="quality">The image encode quality.</param> - /// <param name="imgWidth">The width of a single trickplay image.</param> - /// <param name="imgHeight">Optional height of a single trickplay image, if it is known.</param> - /// <returns>Height of single decoded trickplay image.</returns> - int CreateTrickplayGrid(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight); + /// <param name="imgWidth">The width of a single trickplay thumbnail.</param> + /// <param name="imgHeight">Optional height of a single trickplay thumbnail, if it is known.</param> + /// <returns>Height of single decoded trickplay thumbnail.</returns> + int CreateTrickplayTile(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight); } } diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index 11eb4932c9..2c52b2b45e 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -61,27 +61,6 @@ namespace MediaBrowser.Controller.Persistence /// <param name="chapters">The list of chapters to save.</param> void SaveChapters(Guid id, IReadOnlyList<ChapterInfo> chapters); - /// <summary> - /// Get available trickplay resolutions and corresponding info. - /// </summary> - /// <param name="itemId">The item.</param> - /// <returns>Map of width resolutions to trickplay tiles info.</returns> - Dictionary<int, TrickplayTilesInfo> GetTilesResolutions(Guid itemId); - - /// <summary> - /// Saves trickplay tiles info. - /// </summary> - /// <param name="itemId">The item.</param> - /// <param name="tilesInfo">The trickplay tiles info.</param> - void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo); - - /// <summary> - /// Gets trickplay data for an item. - /// </summary> - /// <param name="item">The item.</param> - /// <returns>A map of media source id to a map of tile width to tile info.</returns> - Dictionary<Guid, Dictionary<int, TrickplayTilesInfo>> GetTrickplayManifest(BaseItem item); - /// <summary> /// Gets the media streams. /// </summary> diff --git a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs index 8d36fc3ff9..0a1e780b2e 100644 --- a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs +++ b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs @@ -2,8 +2,8 @@ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Entities; using MediaBrowser.Controller.Entities; -using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Trickplay; @@ -13,7 +13,7 @@ namespace MediaBrowser.Controller.Trickplay; public interface ITrickplayManager { /// <summary> - /// Generate or replace trickplay data. + /// Generates new trickplay images and metadata. /// </summary> /// <param name="video">The video.</param> /// <param name="replace">Whether or not existing data should be replaced.</param> @@ -26,28 +26,28 @@ public interface ITrickplayManager /// </summary> /// <param name="itemId">The item.</param> /// <returns>Map of width resolutions to trickplay tiles info.</returns> - Dictionary<int, TrickplayTilesInfo> GetTilesResolutions(Guid itemId); + Task<Dictionary<int, TrickplayInfo>> GetTrickplayResolutions(Guid itemId); /// <summary> - /// Saves trickplay tiles info. + /// Saves trickplay info. /// </summary> - /// <param name="itemId">The item.</param> - /// <param name="tilesInfo">The trickplay tiles info.</param> - void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo); + /// <param name="info">The trickplay info.</param> + /// <returns>Task.</returns> + Task SaveTrickplayInfo(TrickplayInfo info); /// <summary> - /// Gets the trickplay manifest. + /// Gets all trickplay infos for all media streams of an item. /// </summary> /// <param name="item">The item.</param> - /// <returns>A map of media source id to a map of tile width to tile info.</returns> - Dictionary<Guid, Dictionary<int, TrickplayTilesInfo>> GetTrickplayManifest(BaseItem item); + /// <returns>A map of media source id to a map of tile width to trickplay info.</returns> + Task<Dictionary<Guid, Dictionary<int, TrickplayInfo>>> GetTrickplayManifest(BaseItem item); /// <summary> - /// Gets the path to a trickplay tiles image. + /// Gets the path to a trickplay tile image. /// </summary> /// <param name="item">The item.</param> - /// <param name="width">The width of a single tile.</param> - /// <param name="index">The tile grid's index.</param> + /// <param name="width">The width of a single thumbnail.</param> + /// <param name="index">The tile's index.</param> /// <returns>The absolute path.</returns> string GetTrickplayTilePath(BaseItem item, int width, int index); @@ -55,8 +55,8 @@ public interface ITrickplayManager /// Gets the trickplay HLS playlist. /// </summary> /// <param name="itemId">The item.</param> - /// <param name="width">The width of a single tile.</param> + /// <param name="width">The width of a single thumbnail.</param> /// <param name="apiKey">Optional api key of the requesting user.</param> /// <returns>The text content of the .m3u8 playlist.</returns> - string? GetHlsPlaylist(Guid itemId, int width, string? apiKey); + Task<string?> GetHlsPlaylist(Guid itemId, int width, string? apiKey); } diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs index 3db9cb08b5..287966dd0e 100644 --- a/MediaBrowser.Model/Dto/BaseItemDto.cs +++ b/MediaBrowser.Model/Dto/BaseItemDto.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Entities; @@ -572,7 +573,7 @@ namespace MediaBrowser.Model.Dto /// Gets or sets the trickplay manifest. /// </summary> /// <value>The trickplay manifest.</value> - public Dictionary<string, Dictionary<int, TrickplayTilesInfo>> Trickplay { get; set; } + public Dictionary<string, Dictionary<int, TrickplayInfo>> Trickplay { get; set; } /// <summary> /// Gets or sets the type of the location. diff --git a/MediaBrowser.Model/Entities/TrickplayTilesInfo.cs b/MediaBrowser.Model/Entities/TrickplayTilesInfo.cs deleted file mode 100644 index 86d37787f3..0000000000 --- a/MediaBrowser.Model/Entities/TrickplayTilesInfo.cs +++ /dev/null @@ -1,49 +0,0 @@ -namespace MediaBrowser.Model.Entities; - -/// <summary> -/// Class TrickplayTilesInfo. -/// </summary> -public class TrickplayTilesInfo -{ - /// <summary> - /// Gets or sets width of an individual tile. - /// </summary> - /// <value>The width.</value> - public int Width { get; set; } - - /// <summary> - /// Gets or sets height of an individual tile. - /// </summary> - /// <value>The height.</value> - public int Height { get; set; } - - /// <summary> - /// Gets or sets amount of tiles per row. - /// </summary> - /// <value>The tile grid's width.</value> - public int TileWidth { get; set; } - - /// <summary> - /// Gets or sets amount of tiles per column. - /// </summary> - /// <value>The tile grid's height.</value> - public int TileHeight { get; set; } - - /// <summary> - /// Gets or sets total amount of non-black tiles. - /// </summary> - /// <value>The tile count.</value> - public int TileCount { get; set; } - - /// <summary> - /// Gets or sets interval in milliseconds between each trickplay tile. - /// </summary> - /// <value>The interval.</value> - public int Interval { get; set; } - - /// <summary> - /// Gets or sets peak bandwith usage in bits per second. - /// </summary> - /// <value>The bandwidth.</value> - public int Bandwidth { get; set; } -} diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 2facf0f370..b387c437b8 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -531,7 +531,7 @@ public class SkiaEncoder : IImageEncoder } /// <inheritdoc /> - public int CreateTrickplayGrid(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight) + public int CreateTrickplayTile(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight) { var paths = options.InputPaths; var tileWidth = options.Width; diff --git a/src/Jellyfin.Drawing/NullImageEncoder.cs b/src/Jellyfin.Drawing/NullImageEncoder.cs index 15345e1bc8..1495661c12 100644 --- a/src/Jellyfin.Drawing/NullImageEncoder.cs +++ b/src/Jellyfin.Drawing/NullImageEncoder.cs @@ -50,7 +50,7 @@ public class NullImageEncoder : IImageEncoder } /// <inheritdoc /> - public int CreateTrickplayGrid(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight) + public int CreateTrickplayTile(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight) { throw new NotImplementedException(); } From fc619337481e3bb0f686aee06d9c5630926baaef Mon Sep 17 00:00:00 2001 From: Nick <20588554+nicknsy@users.noreply.github.com> Date: Mon, 26 Jun 2023 22:04:39 -0700 Subject: [PATCH 405/858] Minor code changes --- Emby.Server.Implementations/Dto/DtoService.cs | 2 +- MediaBrowser.Model/Configuration/ServerConfiguration.cs | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 933b95df04..19ae396628 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -1064,7 +1064,7 @@ namespace Emby.Server.Implementations.Dto if (options.ContainsField(ItemFields.Trickplay)) { - var manifest = _trickplayManager.GetTrickplayManifest(item).ConfigureAwait(false).GetAwaiter().GetResult(); + var manifest = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult(); // To stay consistent with other fields, this must go from a Guid to a non-dashed string. // This does not seem to occur automatically to dictionaries like it does with other Guid fields. diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 097eff295a..18f107503e 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -265,6 +265,10 @@ namespace MediaBrowser.Model.Configuration /// <value>The limit for parallel image encoding.</value> public int ParallelImageEncodingLimit { get; set; } + /// <summary> + /// Gets or sets the trickplay options. + /// </summary> + /// <value>The trickplay options.</value> public TrickplayOptions TrickplayOptions { get; set; } = new TrickplayOptions(); } } From a8486a7b3b65331b71357a1ecf9228d129bebfcd Mon Sep 17 00:00:00 2001 From: Niels van Velzen <git@ndat.nl> Date: Tue, 27 Jun 2023 13:58:54 +0200 Subject: [PATCH 406/858] Add default value to OpenAPI specification for UserPolicy.EnableCollectionManagement This fixes a breaking API change during authentication --- MediaBrowser.Model/Users/UserPolicy.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs index 80f5e2c37e..8354c60efb 100644 --- a/MediaBrowser.Model/Users/UserPolicy.cs +++ b/MediaBrowser.Model/Users/UserPolicy.cs @@ -2,6 +2,7 @@ #pragma warning disable CS1591, CA1819 using System; +using System.ComponentModel; using System.Xml.Serialization; using Jellyfin.Data.Enums; using AccessSchedule = Jellyfin.Data.Entities.AccessSchedule; @@ -79,6 +80,7 @@ namespace MediaBrowser.Model.Users /// Gets or sets a value indicating whether this instance can manage collections. /// </summary> /// <value><c>true</c> if this instance is hidden; otherwise, <c>false</c>.</value> + [DefaultValue(false)] public bool EnableCollectionManagement { get; set; } /// <summary> From 215def56e892dbfe0f3233c7efc7fc801f723bb6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 Jun 2023 13:58:53 +0000 Subject: [PATCH 407/858] chore(deps): update dependency microsoft.net.test.sdk to v17.6.3 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index c335839a6c..a11d5910f9 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -45,7 +45,7 @@ <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" /> <PackageVersion Include="Microsoft.Extensions.Logging" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Options" Version="7.0.1" /> - <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.6.2" /> + <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.6.3" /> <PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" /> <PackageVersion Include="MimeTypes" Version="2.4.0" /> <PackageVersion Include="Mono.Nat" Version="3.0.4" /> From 0f60ec3013a4b1c776513fd92dc3829897b78db7 Mon Sep 17 00:00:00 2001 From: JPVenson <JPVenson@users.noreply.github.com> Date: Tue, 27 Jun 2023 16:05:07 +0200 Subject: [PATCH 408/858] Update Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs Co-authored-by: Bond-009 <bond.009@outlook.com> --- .../ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs index 989c37242e..f8b044d46f 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs @@ -88,7 +88,7 @@ public class CleanupCollectionPathsTask : IScheduledTask } } - if (itemsToRemove.Any()) + if (itemsToRemove.Count != 0) { _logger.LogDebug("Update Boxset {CollectionName}", collection.Name); collection.LinkedChildren = collection.LinkedChildren.Except(itemsToRemove).ToArray(); From cc82ca189fcbd068c34c0d1c38a67d2332a96d44 Mon Sep 17 00:00:00 2001 From: Cody Robibero <cody@robibe.ro> Date: Tue, 27 Jun 2023 21:19:15 -0600 Subject: [PATCH 409/858] suggestions from review --- .../ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs index f8b044d46f..f78fc6f970 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs @@ -74,11 +74,12 @@ public class CleanupCollectionPathsTask : IScheduledTask var collections = collectionsFolder.Children.OfType<BoxSet>().ToArray(); _logger.LogDebug("Found {CollectionLength} Boxsets", collections.Length); + var itemsToRemove = new List<LinkedChild>(); for (var index = 0; index < collections.Length; index++) { var collection = collections[index]; _logger.LogDebug("Check Boxset {CollectionName}", collection.Name); - var itemsToRemove = new List<LinkedChild>(); + foreach (var collectionLinkedChild in collection.LinkedChildren) { if (!File.Exists(collectionLinkedChild.Path)) @@ -102,6 +103,8 @@ public class CleanupCollectionPathsTask : IScheduledTask ForceSave = true }, RefreshPriority.High); + + itemsToRemove.Clear(); } progress.Report(100D / collections.Length * (index + 1)); From e935d787ef7ed2fc6719cd2060b339664bee7746 Mon Sep 17 00:00:00 2001 From: Craig Andrews <candrews@integralblue.com> Date: Tue, 27 Jun 2023 23:20:41 -0400 Subject: [PATCH 410/858] chore(ci): add labels to docker images (#9210) --- .ci/azure-pipelines-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml index 1618237f1a..c28b1bf7f0 100644 --- a/.ci/azure-pipelines-package.yml +++ b/.ci/azure-pipelines-package.yml @@ -47,7 +47,7 @@ jobs: displayName: Set release version (stable) condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') - - script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-server-$(BuildConfiguration) deployment' + - script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-server-$(BuildConfiguration) --label "org.opencontainers.image.url=$(Build.Repository.Uri)" --label "org.opencontainers.image.revision=$(Build.SourceVersion)" deployment' displayName: 'Build Dockerfile' - script: 'docker image ls -a && docker run -v $(pwd)/deployment/dist:/dist -v $(pwd):/jellyfin -e IS_UNSTABLE="yes" -e BUILD_ID=$(Build.BuildNumber) jellyfin-server-$(BuildConfiguration)' From 3d635269eb74ee814d13bbd0e3dc1f591fe5e6fb Mon Sep 17 00:00:00 2001 From: JPVenson <JPVenson@users.noreply.github.com> Date: Wed, 28 Jun 2023 06:32:31 +0300 Subject: [PATCH 411/858] Fixed RTL text not beeing rendered properly on Lib images (#9612) Co-authored-by: Cody Robibero <cody@robibe.ro> --- Directory.Packages.props | 2 ++ .../Jellyfin.Drawing.Skia.csproj | 2 ++ .../StripCollageBuilder.cs | 20 +++++++++++++++++-- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index a11d5910f9..c3532467af 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -67,6 +67,8 @@ <PackageVersion Include="SharpFuzz" Version="2.1.0" /> <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.3" /> <PackageVersion Include="SkiaSharp.Svg" Version="1.60.0" /> + <PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.3" /> + <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.8.2.3" /> <PackageVersion Include="SkiaSharp" Version="2.88.3" /> <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> <PackageVersion Include="SQLitePCL.pretty.netstandard" Version="3.1.0" /> diff --git a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj index 3b03332994..0346913226 100644 --- a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj +++ b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj @@ -21,6 +21,8 @@ <PackageReference Include="SkiaSharp" /> <PackageReference Include="SkiaSharp.NativeAssets.Linux" /> <PackageReference Include="SkiaSharp.Svg" /> + <PackageReference Include="SkiaSharp.HarfBuzz" /> + <PackageReference Include="HarfBuzzSharp.NativeAssets.Linux" /> </ItemGroup> <ItemGroup> diff --git a/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs index eee24c4236..a7a3338df4 100644 --- a/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs +++ b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs @@ -3,13 +3,14 @@ using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; using SkiaSharp; +using SkiaSharp.HarfBuzz; namespace Jellyfin.Drawing.Skia; /// <summary> /// Used to build collages of multiple images arranged in vertical strips. /// </summary> -public class StripCollageBuilder +public partial class StripCollageBuilder { private readonly SkiaEncoder _skiaEncoder; @@ -22,6 +23,9 @@ public class StripCollageBuilder _skiaEncoder = skiaEncoder; } + [GeneratedRegex(@"\p{IsArabic}|\p{IsArmenian}|\p{IsHebrew}|\p{IsSyriac}|\p{IsThaana}")] + private static partial Regex IsRtlTextRegex(); + /// <summary> /// Check which format an image has been encoded with using its filename extension. /// </summary> @@ -144,7 +148,19 @@ public class StripCollageBuilder textPaint.TextSize = 0.9f * width * textPaint.TextSize / textWidth; } - canvas.DrawText(libraryName, width / 2f, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), textPaint); + if (string.IsNullOrWhiteSpace(libraryName)) + { + return bitmap; + } + + if (IsRtlTextRegex().IsMatch(libraryName)) + { + canvas.DrawShapedText(libraryName, width / 2f, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), textPaint); + } + else + { + canvas.DrawText(libraryName, width / 2f, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), textPaint); + } return bitmap; } From f954dc5c969ef5654c31bec7a81b0b92b38637ae Mon Sep 17 00:00:00 2001 From: Bond-009 <bond.009@outlook.com> Date: Wed, 28 Jun 2023 05:36:39 +0200 Subject: [PATCH 412/858] Do HEAD request to get content type instead of checking for extension (#8823) --- .../LiveTv/TunerHosts/M3UTunerHost.cs | 21 ++++++--- .../LiveTv/TunerHosts/SharedHttpStream.cs | 47 +++++-------------- 2 files changed, 25 insertions(+), 43 deletions(-) diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index bcb42e1626..acf3964c8c 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -30,12 +30,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { public class M3UTunerHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost { - private static readonly string[] _disallowedSharedStreamExtensions = + private static readonly string[] _disallowedMimeTypes = { - ".mkv", - ".mp4", - ".m3u8", - ".mpd" + "video/x-matroska", + "video/mp4", + "application/vnd.apple.mpegurl", + "application/mpegurl", + "application/x-mpegurl", + "video/vnd.mpeg.dash.mpd" }; private readonly IHttpClientFactory _httpClientFactory; @@ -118,9 +120,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts if (mediaSource.Protocol == MediaProtocol.Http && !mediaSource.RequiresLooping) { - var extension = Path.GetExtension(mediaSource.Path) ?? string.Empty; + using var message = new HttpRequestMessage(HttpMethod.Head, mediaSource.Path); + using var response = await _httpClientFactory.CreateClient(NamedClient.Default) + .SendAsync(message, cancellationToken) + .ConfigureAwait(false); - if (!_disallowedSharedStreamExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) + response.EnsureSuccessStatusCode(); + + if (!_disallowedMimeTypes.Contains(response.Content.Headers.ContentType?.ToString(), StringComparison.OrdinalIgnoreCase)) { return new SharedHttpStream(mediaSource, tunerHost, streamId, FileSystem, _httpClientFactory, Logger, Config, _appHost, _streamHelper); } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs index e84e1e074c..51f46f4dac 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs @@ -38,7 +38,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts _httpClientFactory = httpClientFactory; _appHost = appHost; OriginalStreamId = originalStreamId; - EnableStreamSharing = true; } public override async Task Open(CancellationToken openCancellationToken) @@ -59,39 +58,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts .GetAsync(url, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None) .ConfigureAwait(false); - var contentType = response.Content.Headers.ContentType?.ToString() ?? string.Empty; - if (contentType.Contains("matroska", StringComparison.OrdinalIgnoreCase) - || contentType.Contains("mp4", StringComparison.OrdinalIgnoreCase) - || contentType.Contains("dash", StringComparison.OrdinalIgnoreCase) - || contentType.Contains("mpegURL", StringComparison.OrdinalIgnoreCase) - || contentType.Contains("text/", StringComparison.OrdinalIgnoreCase)) - { - // Close the stream without any sharing features - response.Dispose(); - return; - } - - SetTempFilePath("ts"); - var taskCompletionSource = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously); _ = StartStreaming(response, taskCompletionSource, LiveStreamCancellationTokenSource.Token); - // OpenedMediaSource.Protocol = MediaProtocol.File; - // OpenedMediaSource.Path = tempFile; - // OpenedMediaSource.ReadAtNativeFramerate = true; - MediaSource.Path = _appHost.GetApiUrlForLocalAccess() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts"; MediaSource.Protocol = MediaProtocol.Http; - // OpenedMediaSource.Path = TempFilePath; - // OpenedMediaSource.Protocol = MediaProtocol.File; - - // OpenedMediaSource.Path = _tempFilePath; - // OpenedMediaSource.Protocol = MediaProtocol.File; - // OpenedMediaSource.SupportsDirectPlay = false; - // OpenedMediaSource.SupportsDirectStream = true; - // OpenedMediaSource.SupportsTranscoding = true; var res = await taskCompletionSource.Task.ConfigureAwait(false); if (!res) { @@ -108,15 +81,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts try { Logger.LogInformation("Beginning {StreamType} stream to {FilePath}", GetType().Name, TempFilePath); - using var message = response; - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); - await StreamHelper.CopyToAsync( - stream, - fileStream, - IODefaults.CopyToBufferSize, - () => Resolve(openTaskCompletionSource), - cancellationToken).ConfigureAwait(false); + using (response) + { + await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); + await StreamHelper.CopyToAsync( + stream, + fileStream, + IODefaults.CopyToBufferSize, + () => Resolve(openTaskCompletionSource), + cancellationToken).ConfigureAwait(false); + } } catch (OperationCanceledException ex) { From 20a4509991e7ba81414312f8a860917fd29b6702 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Thu, 15 Jun 2023 13:28:01 +0200 Subject: [PATCH 413/858] Migrate VideoRange and VideoRangeType to Enum --- .../Controllers/DynamicHlsController.cs | 3 +- Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 13 ++- Jellyfin.Data/Enums/VideoRange.cs | 22 +++++ Jellyfin.Data/Enums/VideoRangeType.cs | 37 ++++++++ .../MediaEncoding/EncodingHelper.cs | 20 ++-- .../MediaEncoding/EncodingJobInfo.cs | 10 +- MediaBrowser.Model/Dlna/ConditionProcessor.cs | 93 ++++++++++++++++++- .../Dlna/ContentFeatureBuilder.cs | 3 +- MediaBrowser.Model/Dlna/DeviceProfile.cs | 3 +- MediaBrowser.Model/Dlna/StreamBuilder.cs | 9 +- MediaBrowser.Model/Dlna/StreamInfo.cs | 12 ++- MediaBrowser.Model/Entities/MediaStream.cs | 21 +++-- 12 files changed, 201 insertions(+), 45 deletions(-) create mode 100644 Jellyfin.Data/Enums/VideoRange.cs create mode 100644 Jellyfin.Data/Enums/VideoRangeType.cs diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 09cf7303e7..898c673ea5 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -12,6 +12,7 @@ using Jellyfin.Api.Attributes; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.PlaybackDtos; using Jellyfin.Api.Models.StreamingDtos; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using Jellyfin.MediaEncoding.Hls.Playlist; using MediaBrowser.Common.Configuration; @@ -1809,7 +1810,7 @@ public class DynamicHlsController : BaseJellyfinApiController || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)) { if (EncodingHelper.IsCopyCodec(codec) - && (string.Equals(state.VideoStream.VideoRangeType, "DOVI", StringComparison.OrdinalIgnoreCase) + && (state.VideoStream.VideoRangeType == VideoRangeType.DOVI || string.Equals(state.VideoStream.CodecTag, "dovi", StringComparison.OrdinalIgnoreCase) || string.Equals(state.VideoStream.CodecTag, "dvh1", StringComparison.OrdinalIgnoreCase) || string.Equals(state.VideoStream.CodecTag, "dvhe", StringComparison.OrdinalIgnoreCase))) diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index 4486954c62..ce73b57541 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Extensions; using Jellyfin.Api.Models.StreamingDtos; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; @@ -211,8 +212,7 @@ public class DynamicHlsHelper // Provide SDR HEVC entrance for backward compatibility. if (encodingOptions.AllowHevcEncoding && EncodingHelper.IsCopyCodec(state.OutputVideoCodec) - && !string.IsNullOrEmpty(state.VideoStream.VideoRange) - && string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase) + && state.VideoStream.VideoRange == VideoRange.HDR && string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)) { var requestedVideoProfiles = state.GetRequestedProfiles("hevc"); @@ -255,8 +255,7 @@ public class DynamicHlsHelper if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec) && state.VideoStream.Level.HasValue && state.VideoStream.Level > 150 - && !string.IsNullOrEmpty(state.VideoStream.VideoRange) - && string.Equals(state.VideoStream.VideoRange, "SDR", StringComparison.OrdinalIgnoreCase) + && state.VideoStream.VideoRange == VideoRange.SDR && string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)) { var playlistCodecsField = new StringBuilder(); @@ -340,17 +339,17 @@ public class DynamicHlsHelper /// <param name="state">StreamState of the current stream.</param> private void AppendPlaylistVideoRangeField(StringBuilder builder, StreamState state) { - if (state.VideoStream is not null && !string.IsNullOrEmpty(state.VideoStream.VideoRange)) + if (state.VideoStream is not null && state.VideoStream.VideoRange != VideoRange.Unknown) { var videoRange = state.VideoStream.VideoRange; if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)) { - if (string.Equals(videoRange, "SDR", StringComparison.OrdinalIgnoreCase)) + if (videoRange == VideoRange.SDR) { builder.Append(",VIDEO-RANGE=SDR"); } - if (string.Equals(videoRange, "HDR", StringComparison.OrdinalIgnoreCase)) + if (videoRange == VideoRange.HDR) { builder.Append(",VIDEO-RANGE=PQ"); } diff --git a/Jellyfin.Data/Enums/VideoRange.cs b/Jellyfin.Data/Enums/VideoRange.cs new file mode 100644 index 0000000000..5072e5ba3e --- /dev/null +++ b/Jellyfin.Data/Enums/VideoRange.cs @@ -0,0 +1,22 @@ +namespace Jellyfin.Data.Enums; + +/// <summary> +/// An enum representing video ranges. +/// </summary> +public enum VideoRange +{ + /// <summary> + /// Unknown video range. + /// </summary> + Unknown, + + /// <summary> + /// SDR video range. + /// </summary> + SDR, + + /// <summary> + /// HDR video range. + /// </summary> + HDR +} diff --git a/Jellyfin.Data/Enums/VideoRangeType.cs b/Jellyfin.Data/Enums/VideoRangeType.cs new file mode 100644 index 0000000000..7ac7bc20a3 --- /dev/null +++ b/Jellyfin.Data/Enums/VideoRangeType.cs @@ -0,0 +1,37 @@ +namespace Jellyfin.Data.Enums; + +/// <summary> +/// An enum representing types of video ranges. +/// </summary> +public enum VideoRangeType +{ + /// <summary> + /// Unknown video range type. + /// </summary> + Unknown, + + /// <summary> + /// SDR video range type (8bit). + /// </summary> + SDR, + + /// <summary> + /// HDR10 video range type (10bit). + /// </summary> + HDR10, + + /// <summary> + /// HLG video range type (10bit). + /// </summary> + HLG, + + /// <summary> + /// Dolby Vision video range type (12bit). + /// </summary> + DOVI, + + /// <summary> + /// HDR10+ video range type (10bit to 16bit). + /// </summary> + HDR10Plus +} diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 39d53768e9..027053b13c 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -209,8 +209,8 @@ namespace MediaBrowser.Controller.MediaEncoding } if (string.Equals(state.VideoStream.Codec, "hevc", StringComparison.OrdinalIgnoreCase) - && string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase) - && string.Equals(state.VideoStream.VideoRangeType, "DOVI", StringComparison.OrdinalIgnoreCase)) + && state.VideoStream.VideoRange == VideoRange.HDR + && state.VideoStream.VideoRangeType == VideoRangeType.DOVI) { // Only native SW decoder and HW accelerator can parse dovi rpu. var vidDecoder = GetHardwareVideoDecoder(state, options) ?? string.Empty; @@ -221,9 +221,9 @@ namespace MediaBrowser.Controller.MediaEncoding return isSwDecoder || isNvdecDecoder || isVaapiDecoder || isD3d11vaDecoder; } - return string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase) - && (string.Equals(state.VideoStream.VideoRangeType, "HDR10", StringComparison.OrdinalIgnoreCase) - || string.Equals(state.VideoStream.VideoRangeType, "HLG", StringComparison.OrdinalIgnoreCase)); + return state.VideoStream.VideoRange == VideoRange.HDR + && (state.VideoStream.VideoRangeType == VideoRangeType.HDR10 + || state.VideoStream.VideoRangeType == VideoRangeType.HLG); } private bool IsVulkanHwTonemapAvailable(EncodingJobInfo state, EncodingOptions options) @@ -235,7 +235,7 @@ namespace MediaBrowser.Controller.MediaEncoding // libplacebo has partial Dolby Vision to SDR tonemapping support. return options.EnableTonemapping - && string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase) + && state.VideoStream.VideoRange == VideoRange.HDR && GetVideoColorBitDepth(state) == 10; } @@ -250,8 +250,8 @@ namespace MediaBrowser.Controller.MediaEncoding // Native VPP tonemapping may come to QSV in the future. - return string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase) - && string.Equals(state.VideoStream.VideoRangeType, "HDR10", StringComparison.OrdinalIgnoreCase); + return state.VideoStream.VideoRange == VideoRange.HDR + && state.VideoStream.VideoRangeType == VideoRangeType.HDR10; } /// <summary> @@ -1945,12 +1945,12 @@ namespace MediaBrowser.Controller.MediaEncoding var requestedRangeTypes = state.GetRequestedRangeTypes(videoStream.Codec); if (requestedRangeTypes.Length > 0) { - if (string.IsNullOrEmpty(videoStream.VideoRangeType)) + if (videoStream.VideoRangeType == VideoRangeType.Unknown) { return false; } - if (!requestedRangeTypes.Contains(videoStream.VideoRangeType, StringComparison.OrdinalIgnoreCase)) + if (!requestedRangeTypes.Contains(videoStream.VideoRangeType.ToString(), StringComparison.OrdinalIgnoreCase)) { return false; } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs index a6b5416601..17813559a8 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; @@ -367,22 +368,21 @@ namespace MediaBrowser.Controller.MediaEncoding /// <summary> /// Gets the target video range type. /// </summary> - public string TargetVideoRangeType + public VideoRangeType TargetVideoRangeType { get { if (BaseRequest.Static || EncodingHelper.IsCopyCodec(OutputVideoCodec)) { - return VideoStream?.VideoRangeType; + return VideoStream?.VideoRangeType ?? VideoRangeType.Unknown; } - var requestedRangeType = GetRequestedRangeTypes(ActualOutputVideoCodec).FirstOrDefault(); - if (!string.IsNullOrEmpty(requestedRangeType)) + if (Enum.TryParse(GetRequestedRangeTypes(ActualOutputVideoCodec).FirstOrDefault() ?? "Unknown", true, out VideoRangeType requestedRangeType)) { return requestedRangeType; } - return null; + return VideoRangeType.Unknown; } } diff --git a/MediaBrowser.Model/Dlna/ConditionProcessor.cs b/MediaBrowser.Model/Dlna/ConditionProcessor.cs index f5e1a3c496..af0787990d 100644 --- a/MediaBrowser.Model/Dlna/ConditionProcessor.cs +++ b/MediaBrowser.Model/Dlna/ConditionProcessor.cs @@ -1,14 +1,38 @@ -#pragma warning disable CS1591 - using System; using System.Globalization; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Model.MediaInfo; namespace MediaBrowser.Model.Dlna { + /// <summary> + /// The condition processor. + /// </summary> public static class ConditionProcessor { + /// <summary> + /// Checks if a video condition is satisfied. + /// </summary> + /// <param name="condition">The <see cref="ProfileCondition"/>.</param> + /// <param name="width">The width.</param> + /// <param name="height">The height.</param> + /// <param name="videoBitDepth">The bit depth.</param> + /// <param name="videoBitrate">The bitrate.</param> + /// <param name="videoProfile">The video profile.</param> + /// <param name="videoRangeType">The <see cref="VideoRangeType"/>.</param> + /// <param name="videoLevel">The video level.</param> + /// <param name="videoFramerate">The framerate.</param> + /// <param name="packetLength">The packet length.</param> + /// <param name="timestamp">The <see cref="TransportStreamTimestamp"/>.</param> + /// <param name="isAnamorphic">A value indicating whether tthe video is anamorphic.</param> + /// <param name="isInterlaced">A value indicating whether tthe video is interlaced.</param> + /// <param name="refFrames">The reference frames.</param> + /// <param name="numVideoStreams">The number of video streams.</param> + /// <param name="numAudioStreams">The number of audio streams.</param> + /// <param name="videoCodecTag">The video codec tag.</param> + /// <param name="isAvc">A value indicating whether the video is AVC.</param> + /// <returns><b>True</b> if the condition is satisfied.</returns> public static bool IsVideoConditionSatisfied( ProfileCondition condition, int? width, @@ -16,7 +40,7 @@ namespace MediaBrowser.Model.Dlna int? videoBitDepth, int? videoBitrate, string? videoProfile, - string? videoRangeType, + VideoRangeType? videoRangeType, double? videoLevel, float? videoFramerate, int? packetLength, @@ -70,6 +94,13 @@ namespace MediaBrowser.Model.Dlna } } + /// <summary> + /// Checks if a image condition is satisfied. + /// </summary> + /// <param name="condition">The <see cref="ProfileCondition"/>.</param> + /// <param name="width">The width.</param> + /// <param name="height">The height.</param> + /// <returns><b>True</b> if the condition is satisfied.</returns> public static bool IsImageConditionSatisfied(ProfileCondition condition, int? width, int? height) { switch (condition.Property) @@ -83,6 +114,15 @@ namespace MediaBrowser.Model.Dlna } } + /// <summary> + /// Checks if an audio condition is satisfied. + /// </summary> + /// <param name="condition">The <see cref="ProfileCondition"/>.</param> + /// <param name="audioChannels">The channel count.</param> + /// <param name="audioBitrate">The bitrate.</param> + /// <param name="audioSampleRate">The sample rate.</param> + /// <param name="audioBitDepth">The bit depth.</param> + /// <returns><b>True</b> if the condition is satisfied.</returns> public static bool IsAudioConditionSatisfied(ProfileCondition condition, int? audioChannels, int? audioBitrate, int? audioSampleRate, int? audioBitDepth) { switch (condition.Property) @@ -100,6 +140,17 @@ namespace MediaBrowser.Model.Dlna } } + /// <summary> + /// Checks if an audio condition is satisfied for a video. + /// </summary> + /// <param name="condition">The <see cref="ProfileCondition"/>.</param> + /// <param name="audioChannels">The channel count.</param> + /// <param name="audioBitrate">The bitrate.</param> + /// <param name="audioSampleRate">The sample rate.</param> + /// <param name="audioBitDepth">The bit depth.</param> + /// <param name="audioProfile">The profile.</param> + /// <param name="isSecondaryTrack">A value indicating whether the audio is a secondary track.</param> + /// <returns><b>True</b> if the condition is satisfied.</returns> public static bool IsVideoAudioConditionSatisfied( ProfileCondition condition, int? audioChannels, @@ -281,5 +332,41 @@ namespace MediaBrowser.Model.Dlna throw new InvalidOperationException("Unexpected ProfileConditionType: " + condition.Condition); } } + + private static bool IsConditionSatisfied(ProfileCondition condition, VideoRangeType? currentValue) + { + if (!currentValue.HasValue || currentValue.Equals(VideoRangeType.Unknown)) + { + // If the value is unknown, it satisfies if not marked as required + return !condition.IsRequired; + } + + var conditionType = condition.Condition; + if (conditionType == ProfileConditionType.EqualsAny) + { + foreach (var singleConditionString in condition.Value.AsSpan().Split('|')) + { + if (Enum.TryParse(singleConditionString, true, out VideoRangeType conditionValue) + && conditionValue.Equals(currentValue)) + { + return true; + } + } + + return false; + } + + if (Enum.TryParse(condition.Value, true, out VideoRangeType expected)) + { + return conditionType switch + { + ProfileConditionType.Equals => currentValue.Value == expected, + ProfileConditionType.NotEquals => currentValue.Value != expected, + _ => throw new InvalidOperationException("Unexpected ProfileConditionType: " + condition.Condition) + }; + } + + return false; + } } } diff --git a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs index 1d5d0b1de3..f29022b54e 100644 --- a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs +++ b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using Jellyfin.Data.Enums; using MediaBrowser.Model.MediaInfo; namespace MediaBrowser.Model.Dlna @@ -128,7 +129,7 @@ namespace MediaBrowser.Model.Dlna bool isDirectStream, long? runtimeTicks, string videoProfile, - string videoRangeType, + VideoRangeType videoRangeType, double? videoLevel, float? videoFramerate, int? packetLength, diff --git a/MediaBrowser.Model/Dlna/DeviceProfile.cs b/MediaBrowser.Model/Dlna/DeviceProfile.cs index 79ae951708..b7c23669df 100644 --- a/MediaBrowser.Model/Dlna/DeviceProfile.cs +++ b/MediaBrowser.Model/Dlna/DeviceProfile.cs @@ -2,6 +2,7 @@ using System; using System.ComponentModel; using System.Xml.Serialization; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Model.MediaInfo; @@ -445,7 +446,7 @@ namespace MediaBrowser.Model.Dlna int? bitDepth, int? videoBitrate, string videoProfile, - string videoRangeType, + VideoRangeType videoRangeType, double? videoLevel, float? videoFramerate, int? packetLength, diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 0a955e917f..2dbd14da4d 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using Jellyfin.Data.Enums; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.MediaInfo; @@ -889,7 +890,7 @@ namespace MediaBrowser.Model.Dlna int? videoBitrate = videoStream?.BitRate; double? videoLevel = videoStream?.Level; string? videoProfile = videoStream?.Profile; - string? videoRangeType = videoStream?.VideoRangeType; + VideoRangeType? videoRangeType = videoStream?.VideoRangeType; float videoFramerate = videoStream is null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0; bool? isAnamorphic = videoStream?.IsAnamorphic; bool? isInterlaced = videoStream?.IsInterlaced; @@ -1144,7 +1145,7 @@ namespace MediaBrowser.Model.Dlna int? videoBitrate = videoStream?.BitRate; double? videoLevel = videoStream?.Level; string? videoProfile = videoStream?.Profile; - string? videoRangeType = videoStream?.VideoRangeType; + VideoRangeType? videoRangeType = videoStream?.VideoRangeType; float videoFramerate = videoStream is null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0; bool? isAnamorphic = videoStream?.IsAnamorphic; bool? isInterlaced = videoStream?.IsInterlaced; @@ -1932,6 +1933,10 @@ namespace MediaBrowser.Model.Dlna { item.SetOption(qualifier, "rangetype", string.Join(',', values)); } + else if (condition.Condition == ProfileConditionType.NotEquals) + { + item.SetOption(qualifier, "rangetype", string.Join(',', Enum.GetNames(typeof(VideoRangeType)).Except(values))); + } else if (condition.Condition == ProfileConditionType.EqualsAny) { var currentValue = item.GetOption(qualifier, "rangetype"); diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index a78a28e13a..00543616d1 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using Jellyfin.Data.Enums; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; @@ -281,23 +282,24 @@ namespace MediaBrowser.Model.Dlna /// <summary> /// Gets the target video range type that will be in the output stream. /// </summary> - public string TargetVideoRangeType + public VideoRangeType TargetVideoRangeType { get { if (IsDirectStream) { - return TargetVideoStream?.VideoRangeType; + return TargetVideoStream?.VideoRangeType ?? VideoRangeType.Unknown; } var targetVideoCodecs = TargetVideoCodec; var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0]; - if (!string.IsNullOrEmpty(videoCodec)) + if (!string.IsNullOrEmpty(videoCodec) + && Enum.TryParse(GetOption(videoCodec, "rangetype"), true, out VideoRangeType videoRangeType)) { - return GetOption(videoCodec, "rangetype"); + return videoRangeType; } - return TargetVideoStream?.VideoRangeType; + return TargetVideoStream?.VideoRangeType ?? VideoRangeType.Unknown; } } diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index 47341f4e17..34642b83aa 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Extensions; @@ -148,7 +149,7 @@ namespace MediaBrowser.Model.Entities /// Gets the video range. /// </summary> /// <value>The video range.</value> - public string VideoRange + public VideoRange VideoRange { get { @@ -162,7 +163,7 @@ namespace MediaBrowser.Model.Entities /// Gets the video range type. /// </summary> /// <value>The video range type.</value> - public string VideoRangeType + public VideoRangeType VideoRangeType { get { @@ -306,9 +307,9 @@ namespace MediaBrowser.Model.Entities attributes.Add(Codec.ToUpperInvariant()); } - if (!string.IsNullOrEmpty(VideoRange)) + if (VideoRange != VideoRange.Unknown) { - attributes.Add(VideoRange.ToUpperInvariant()); + attributes.Add(VideoRange.ToString()); } if (!string.IsNullOrEmpty(Title)) @@ -677,23 +678,23 @@ namespace MediaBrowser.Model.Entities return true; } - public (string VideoRange, string VideoRangeType) GetVideoColorRange() + public (VideoRange VideoRange, VideoRangeType VideoRangeType) GetVideoColorRange() { if (Type != MediaStreamType.Video) { - return (null, null); + return (VideoRange.Unknown, VideoRangeType.Unknown); } var colorTransfer = ColorTransfer; if (string.Equals(colorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase)) { - return ("HDR", "HDR10"); + return (VideoRange.HDR, VideoRangeType.HDR10); } if (string.Equals(colorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase)) { - return ("HDR", "HLG"); + return (VideoRange.HDR, VideoRangeType.HLG); } var codecTag = CodecTag; @@ -711,10 +712,10 @@ namespace MediaBrowser.Model.Entities || string.Equals(codecTag, "dvhe", StringComparison.OrdinalIgnoreCase) || string.Equals(codecTag, "dav1", StringComparison.OrdinalIgnoreCase)) { - return ("HDR", "DOVI"); + return (VideoRange.HDR, VideoRangeType.DOVI); } - return ("SDR", "SDR"); + return (VideoRange.SDR, VideoRangeType.SDR); } } } From b5f0760db8dba96e9edd67d4b9c914cf25c3d26a Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Mon, 22 May 2023 22:48:09 +0200 Subject: [PATCH 414/858] Use RegexGenerator where possible --- Emby.Dlna/PlayTo/DlnaHttpClient.cs | 10 ++---- Emby.Naming/Audio/AlbumParser.cs | 13 ++++---- Emby.Naming/TV/SeriesResolver.cs | 7 +++-- Emby.Naming/Video/VideoListResolver.cs | 12 ++++--- .../Library/Resolvers/Movies/MovieResolver.cs | 10 +++--- .../LegacyHdHomerunChannelCommands.cs | 7 +++-- .../LiveTv/TunerHosts/M3uParser.cs | 9 ++++-- .../Users/UserManager.cs | 20 ++++++------ .../Extensions/BaseExtensions.cs | 13 ++++---- .../MediaEncoding/EncodingHelper.cs | 7 +++-- .../Encoder/EncoderValidator.cs | 15 +++++---- .../Encoder/MediaEncoder.cs | 11 ++++--- .../Subtitles/AssWriter.cs | 7 +++-- .../Subtitles/SrtWriter.cs | 7 +++-- .../Subtitles/SsaWriter.cs | 7 +++-- .../Subtitles/TtmlWriter.cs | 7 +++-- .../Subtitles/VttWriter.cs | 7 +++-- MediaBrowser.Model/Dlna/SearchCriteria.cs | 31 +++++-------------- .../MediaInfo/AudioFileProber.cs | 7 +++-- .../Plugins/Tmdb/TmdbUtils.cs | 9 +++--- .../Savers/BaseNfoSaver.cs | 15 ++++----- jellyfin.ruleset | 2 ++ .../StripCollageBuilder.cs | 3 +- src/Jellyfin.Extensions/StringExtensions.cs | 10 +++--- .../Manager/ItemImageProviderTests.cs | 7 +++-- 25 files changed, 137 insertions(+), 116 deletions(-) diff --git a/Emby.Dlna/PlayTo/DlnaHttpClient.cs b/Emby.Dlna/PlayTo/DlnaHttpClient.cs index 8b983e9e3d..8454c1afd3 100644 --- a/Emby.Dlna/PlayTo/DlnaHttpClient.cs +++ b/Emby.Dlna/PlayTo/DlnaHttpClient.cs @@ -31,6 +31,9 @@ namespace Emby.Dlna.PlayTo _httpClientFactory = httpClientFactory; } + [GeneratedRegex("(&(?![a-z]*;))")] + private static partial Regex EscapeAmpersandRegex(); + private static string NormalizeServiceUrl(string baseUrl, string serviceUrl) { // If it's already a complete url, don't stick anything onto the front of it @@ -128,12 +131,5 @@ namespace Emby.Dlna.PlayTo // Have to await here instead of returning the Task directly, otherwise request would be disposed too soon return await SendRequestAsync(request, cancellationToken).ConfigureAwait(false); } - - /// <summary> - /// Compile-time generated regular expression for escaping ampersands. - /// </summary> - /// <returns>Compiled regular expression.</returns> - [GeneratedRegex("(&(?![a-z]*;))")] - private static partial Regex EscapeAmpersandRegex(); } } diff --git a/Emby.Naming/Audio/AlbumParser.cs b/Emby.Naming/Audio/AlbumParser.cs index 86a5641531..73424a1345 100644 --- a/Emby.Naming/Audio/AlbumParser.cs +++ b/Emby.Naming/Audio/AlbumParser.cs @@ -10,7 +10,7 @@ namespace Emby.Naming.Audio /// <summary> /// Helper class to determine if Album is multipart. /// </summary> - public class AlbumParser + public partial class AlbumParser { private readonly NamingOptions _options; @@ -23,6 +23,9 @@ namespace Emby.Naming.Audio _options = options; } + [GeneratedRegex(@"([-\.\(\)]|\s+)")] + private static partial Regex CleanRegex(); + /// <summary> /// Function that determines if album is multipart. /// </summary> @@ -42,13 +45,9 @@ namespace Emby.Naming.Audio // Normalize // Remove whitespace - filename = filename.Replace('-', ' '); - filename = filename.Replace('.', ' '); - filename = filename.Replace('(', ' '); - filename = filename.Replace(')', ' '); - filename = Regex.Replace(filename, @"\s+", " "); + filename = CleanRegex().Replace(filename, " "); - ReadOnlySpan<char> trimmedFilename = filename.TrimStart(); + ReadOnlySpan<char> trimmedFilename = filename.AsSpan().TrimStart(); foreach (var prefix in _options.AlbumStackingPrefixes) { diff --git a/Emby.Naming/TV/SeriesResolver.cs b/Emby.Naming/TV/SeriesResolver.cs index 307a840964..d8fa417436 100644 --- a/Emby.Naming/TV/SeriesResolver.cs +++ b/Emby.Naming/TV/SeriesResolver.cs @@ -7,14 +7,15 @@ namespace Emby.Naming.TV /// <summary> /// Used to resolve information about series from path. /// </summary> - public static class SeriesResolver + public static partial class SeriesResolver { /// <summary> /// Regex that matches strings of at least 2 characters separated by a dot or underscore. /// Used for removing separators between words, i.e turns "The_show" into "The show" while /// preserving namings like "S.H.O.W". /// </summary> - private static readonly Regex _seriesNameRegex = new Regex(@"((?<a>[^\._]{2,})[\._]*)|([\._](?<b>[^\._]{2,}))", RegexOptions.Compiled); + [GeneratedRegex(@"((?<a>[^\._]{2,})[\._]*)|([\._](?<b>[^\._]{2,}))")] + private static partial Regex SeriesNameRegex(); /// <summary> /// Resolve information about series from path. @@ -37,7 +38,7 @@ namespace Emby.Naming.TV if (!string.IsNullOrEmpty(seriesName)) { - seriesName = _seriesNameRegex.Replace(seriesName, "${a} ${b}").Trim(); + seriesName = SeriesNameRegex().Replace(seriesName, "${a} ${b}").Trim(); } return new SeriesInfo(path) diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs index 6209cd46f4..51f29cf088 100644 --- a/Emby.Naming/Video/VideoListResolver.cs +++ b/Emby.Naming/Video/VideoListResolver.cs @@ -12,9 +12,13 @@ namespace Emby.Naming.Video /// <summary> /// Resolves alternative versions and extras from list of video files. /// </summary> - public static class VideoListResolver + public static partial class VideoListResolver { - private static readonly Regex _resolutionRegex = new Regex("[0-9]{2}[0-9]+[ip]", RegexOptions.IgnoreCase | RegexOptions.Compiled); + [GeneratedRegex("[0-9]{2}[0-9]+[ip]", RegexOptions.IgnoreCase)] + private static partial Regex ResolutionRegex(); + + [GeneratedRegex(@"^\[([^]]*)\]")] + private static partial Regex CheckMultiVersionRegex(); /// <summary> /// Resolves alternative versions and extras from list of video files. @@ -131,7 +135,7 @@ namespace Emby.Naming.Video if (videos.Count > 1) { - var groups = videos.GroupBy(x => _resolutionRegex.IsMatch(x.Files[0].FileNameWithoutExtension)).ToList(); + var groups = videos.GroupBy(x => ResolutionRegex().IsMatch(x.Files[0].FileNameWithoutExtension)).ToList(); videos.Clear(); foreach (var group in groups) { @@ -201,7 +205,7 @@ namespace Emby.Naming.Video // The CleanStringParser should have removed common keywords etc. return testFilename.IsEmpty || testFilename[0] == '-' - || Regex.IsMatch(testFilename, @"^\[([^]]*)\]", RegexOptions.Compiled); + || CheckMultiVersionRegex().IsMatch(testFilename); } } } diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index ea980b9929..0b65bf921e 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -24,7 +24,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies /// <summary> /// Class MovieResolver. /// </summary> - public class MovieResolver : BaseVideoResolver<Video>, IMultiItemResolver + public partial class MovieResolver : BaseVideoResolver<Video>, IMultiItemResolver { private readonly IImageProcessor _imageProcessor; @@ -56,6 +56,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies /// <value>The priority.</value> public override ResolverPriority Priority => ResolverPriority.Fourth; + [GeneratedRegex(@"\bsample\b", RegexOptions.IgnoreCase)] + private static partial Regex IsIgnoredRegex(); + /// <inheritdoc /> public MultiItemResolverResult ResolveMultiple( Folder parent, @@ -261,7 +264,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies { leftOver.Add(child); } - else if (!IsIgnored(child.Name)) + else if (!IsIgnoredRegex().IsMatch(child.Name)) { files.Add(child); } @@ -314,9 +317,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies return result; } - private static bool IsIgnored(ReadOnlySpan<char> filename) - => Regex.IsMatch(filename, @"\bsample\b", RegexOptions.IgnoreCase | RegexOptions.Compiled); - private static bool ContainsFile(IReadOnlyList<VideoInfo> result, FileSystemMetadata file) { for (var i = 0; i < result.Count; i++) diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs index 3450f971fc..654474e971 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs @@ -5,7 +5,7 @@ using System.Text.RegularExpressions; namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { - public class LegacyHdHomerunChannelCommands : IHdHomerunChannelCommands + public partial class LegacyHdHomerunChannelCommands : IHdHomerunChannelCommands { private string? _channel; private string? _program; @@ -13,7 +13,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun public LegacyHdHomerunChannelCommands(string url) { // parse url for channel and program - var match = Regex.Match(url, @"\/ch([0-9]+)-?([0-9]*)"); + var match = ChannelAndProgramRegex().Match(url); if (match.Success) { _channel = match.Groups[1].Value; @@ -21,6 +21,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun } } + [GeneratedRegex(@"\/ch([0-9]+)-?([0-9]*)")] + private static partial Regex ChannelAndProgramRegex(); + public IEnumerable<(string CommandName, string CommandValue)> GetCommands() { if (!string.IsNullOrEmpty(_channel)) diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs index b418162304..df9101f48c 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs @@ -20,7 +20,7 @@ using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.LiveTv.TunerHosts { - public class M3uParser + public partial class M3uParser { private const string ExtInfPrefix = "#EXTINF:"; @@ -33,6 +33,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts _httpClientFactory = httpClientFactory; } + [GeneratedRegex(@"([a-z0-9\-_]+)=\""([^""]+)\""", RegexOptions.IgnoreCase, "en-US")] + private static partial Regex KeyValueRegex(); + public async Task<List<ChannelInfo>> Parse(TunerHostInfo info, string channelIdPrefix, CancellationToken cancellationToken) { // Read the file and display it line by line. @@ -311,7 +314,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - var matches = Regex.Matches(line, @"([a-z0-9\-_]+)=\""([^""]+)\""", RegexOptions.IgnoreCase); + var matches = KeyValueRegex().Matches(line); remaining = line; @@ -320,7 +323,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts var key = match.Groups[1].Value; var value = match.Groups[2].Value; - dict[match.Groups[1].Value] = match.Groups[2].Value; + dict[key] = value; remaining = remaining.Replace(key + "=\"" + value + "\"", string.Empty, StringComparison.OrdinalIgnoreCase); } diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 1d03baa4c6..ec0c64cd72 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -31,7 +31,7 @@ namespace Jellyfin.Server.Implementations.Users /// <summary> /// Manages the creation and retrieval of <see cref="User"/> instances. /// </summary> - public class UserManager : IUserManager + public partial class UserManager : IUserManager { private readonly IDbContextFactory<JellyfinDbContext> _dbProvider; private readonly IEventManager _eventManager; @@ -105,6 +105,12 @@ namespace Jellyfin.Server.Implementations.Users /// <inheritdoc/> public IEnumerable<Guid> UsersIds => _users.Keys; + // This is some regex that matches only on unicode "word" characters, as well as -, _ and @ + // In theory this will cut out most if not all 'control' characters which should help minimize any weirdness + // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), periods (.) and spaces ( ) + [GeneratedRegex("^[\\w\\ \\-'._@]+$")] + private static partial Regex ValidUsernameRegex(); + /// <inheritdoc/> public User? GetUserById(Guid id) { @@ -527,7 +533,7 @@ namespace Jellyfin.Server.Implementations.Users } var defaultName = Environment.UserName; - if (string.IsNullOrWhiteSpace(defaultName) || !IsValidUsername(defaultName)) + if (string.IsNullOrWhiteSpace(defaultName) || !ValidUsernameRegex().IsMatch(defaultName)) { defaultName = "MyJellyfinUser"; } @@ -710,7 +716,7 @@ namespace Jellyfin.Server.Implementations.Users internal static void ThrowIfInvalidUsername(string name) { - if (!string.IsNullOrWhiteSpace(name) && IsValidUsername(name)) + if (!string.IsNullOrWhiteSpace(name) && ValidUsernameRegex().IsMatch(name)) { return; } @@ -718,14 +724,6 @@ namespace Jellyfin.Server.Implementations.Users throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)", nameof(name)); } - private static bool IsValidUsername(ReadOnlySpan<char> name) - { - // This is some regex that matches only on unicode "word" characters, as well as -, _ and @ - // In theory this will cut out most if not all 'control' characters which should help minimize any weirdness - // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), periods (.) and spaces ( ) - return Regex.IsMatch(name, @"^[\w\ \-'._@]+$"); - } - private IAuthenticationProvider GetAuthenticationProvider(User user) { return GetAuthenticationProviders(user)[0]; diff --git a/MediaBrowser.Common/Extensions/BaseExtensions.cs b/MediaBrowser.Common/Extensions/BaseExtensions.cs index e3775021e1..3615b662b8 100644 --- a/MediaBrowser.Common/Extensions/BaseExtensions.cs +++ b/MediaBrowser.Common/Extensions/BaseExtensions.cs @@ -8,20 +8,19 @@ namespace MediaBrowser.Common.Extensions /// <summary> /// Class BaseExtensions. /// </summary> - public static class BaseExtensions + public static partial class BaseExtensions { + // http://stackoverflow.com/questions/1349023/how-can-i-strip-html-from-text-in-net + [GeneratedRegex(@"<(.|\n)*?>")] + private static partial Regex StripHtmlRegex(); + /// <summary> /// Strips the HTML. /// </summary> /// <param name="htmlString">The HTML string.</param> /// <returns><see cref="string" />.</returns> public static string StripHtml(this string htmlString) - { - // http://stackoverflow.com/questions/1349023/how-can-i-strip-html-from-text-in-net - const string Pattern = @"<(.|\n)*?>"; - - return Regex.Replace(htmlString, Pattern, string.Empty).Trim(); - } + => StripHtmlRegex().Replace(htmlString, string.Empty).Trim(); /// <summary> /// Gets the Md5. diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index b155d674de..89cf55b789 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -23,7 +23,7 @@ using Microsoft.Extensions.Configuration; namespace MediaBrowser.Controller.MediaEncoding { - public class EncodingHelper + public partial class EncodingHelper { private const string QsvAlias = "qs"; private const string VaapiAlias = "va"; @@ -112,6 +112,9 @@ namespace MediaBrowser.Controller.MediaEncoding _config = config; } + [GeneratedRegex(@"\s+")] + private static partial Regex WhiteSpaceRegex(); + public string GetH264Encoder(EncodingJobInfo state, EncodingOptions encodingOptions) => GetH264OrH265Encoder("libx264", "h264", state, encodingOptions); @@ -1733,7 +1736,7 @@ namespace MediaBrowser.Controller.MediaEncoding } var profile = state.GetRequestedProfiles(targetVideoCodec).FirstOrDefault() ?? string.Empty; - profile = Regex.Replace(profile, @"\s+", string.Empty); + profile = WhiteSpaceRegex().Replace(profile, string.Empty); // We only transcode to HEVC 8-bit for now, force Main Profile. if (profile.Contains("main10", StringComparison.OrdinalIgnoreCase) diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index d3843796f7..0e493afb47 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -10,7 +10,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.MediaEncoding.Encoder { - public class EncoderValidator + public partial class EncoderValidator { private static readonly string[] _requiredDecoders = new[] { @@ -160,6 +160,12 @@ namespace MediaBrowser.MediaEncoding.Encoder public static Version? MaxVersion { get; } = null; + [GeneratedRegex(@"^ffmpeg version n?((?:[0-9]+\.?)+)")] + private static partial Regex FfmpegVersionRegex(); + + [GeneratedRegex(@"((?<name>lib\w+)\s+(?<major>[0-9]+)\.\s*(?<minor>[0-9]+))", RegexOptions.Multiline)] + private static partial Regex LibraryRegex(); + public bool ValidateVersion() { string output; @@ -278,7 +284,7 @@ namespace MediaBrowser.MediaEncoding.Encoder internal Version? GetFFmpegVersionInternal(string output) { // For pre-built binaries the FFmpeg version should be mentioned at the very start of the output - var match = Regex.Match(output, @"^ffmpeg version n?((?:[0-9]+\.?)+)"); + var match = FfmpegVersionRegex().Match(output); if (match.Success) { @@ -326,10 +332,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { var map = new Dictionary<string, Version>(); - foreach (Match match in Regex.Matches( - output, - @"((?<name>lib\w+)\s+(?<major>[0-9]+)\.\s*(?<minor>[0-9]+))", - RegexOptions.Multiline)) + foreach (Match match in LibraryRegex().Matches(output)) { var version = new Version( int.Parse(match.Groups["major"].ValueSpan, CultureInfo.InvariantCulture), diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 4e63d205c9..0885fbe920 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -36,7 +36,7 @@ namespace MediaBrowser.MediaEncoding.Encoder /// <summary> /// Class MediaEncoder. /// </summary> - public class MediaEncoder : IMediaEncoder, IDisposable + public partial class MediaEncoder : IMediaEncoder, IDisposable { /// <summary> /// The default SDR image extraction timeout in milliseconds. @@ -142,6 +142,9 @@ namespace MediaBrowser.MediaEncoding.Encoder /// <inheritdoc /> public bool IsVaapiDeviceSupportVulkanFmtModifier => _isVaapiDeviceSupportVulkanFmtModifier; + [GeneratedRegex(@"[^\/\\]+?(\.[^\/\\\n.]+)?$")] + private static partial Regex FfprobePathRegex(); + /// <summary> /// Run at startup or if the user removes a Custom path from transcode page. /// Sets global variables FFmpegPath. @@ -176,7 +179,7 @@ namespace MediaBrowser.MediaEncoding.Encoder if (_ffmpegPath is not null) { // Determine a probe path from the mpeg path - _ffprobePath = Regex.Replace(_ffmpegPath, @"[^\/\\]+?(\.[^\/\\\n.]+)?$", @"ffprobe$1"); + _ffprobePath = FfprobePathRegex().Replace(_ffmpegPath, @"ffprobe$1"); // Interrogate to understand what coders are supported var validator = new EncoderValidator(_logger, _ffmpegPath); @@ -416,8 +419,8 @@ namespace MediaBrowser.MediaEncoding.Encoder public Task<MediaInfo> GetMediaInfo(MediaInfoRequest request, CancellationToken cancellationToken) { var extractChapters = request.MediaType == DlnaProfileType.Video && request.ExtractChapters; - string analyzeDuration = string.Empty; - string ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty; + var analyzeDuration = string.Empty; + var ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty; if (request.MediaSource.AnalyzeDurationMs > 0) { diff --git a/MediaBrowser.MediaEncoding/Subtitles/AssWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/AssWriter.cs index 0d1cf6e258..7d7b80e99d 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/AssWriter.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/AssWriter.cs @@ -11,8 +11,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles /// <summary> /// ASS subtitle writer. /// </summary> - public class AssWriter : ISubtitleWriter + public partial class AssWriter : ISubtitleWriter { + [GeneratedRegex(@"\n", RegexOptions.IgnoreCase)] + private static partial Regex NewLineRegex(); + /// <inheritdoc /> public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken) { @@ -40,7 +43,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles var trackEvent = trackEvents[i]; var startTime = TimeSpan.FromTicks(trackEvent.StartPositionTicks).ToString(timeFormat, CultureInfo.InvariantCulture); var endTime = TimeSpan.FromTicks(trackEvent.EndPositionTicks).ToString(timeFormat, CultureInfo.InvariantCulture); - var text = Regex.Replace(trackEvent.Text, @"\n", "\\n", RegexOptions.IgnoreCase); + var text = NewLineRegex().Replace(trackEvent.Text, "\\n"); writer.WriteLine( "Dialogue: 0,{0},{1},Default,{2}", diff --git a/MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs index 143c010b71..86f77aa067 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs @@ -11,8 +11,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles /// <summary> /// SRT subtitle writer. /// </summary> - public class SrtWriter : ISubtitleWriter + public partial class SrtWriter : ISubtitleWriter { + [GeneratedRegex(@"\\n", RegexOptions.IgnoreCase)] + private static partial Regex NewLineEscapedRegex(); + /// <inheritdoc /> public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken) { @@ -35,7 +38,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles var text = trackEvent.Text; // TODO: Not sure how to handle these - text = Regex.Replace(text, @"\\n", " ", RegexOptions.IgnoreCase); + text = NewLineEscapedRegex().Replace(text, " "); writer.WriteLine(text); writer.WriteLine(); diff --git a/MediaBrowser.MediaEncoding/Subtitles/SsaWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/SsaWriter.cs index 6761cd3099..b5fd1ed935 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SsaWriter.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SsaWriter.cs @@ -11,8 +11,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles /// <summary> /// SSA subtitle writer. /// </summary> - public class SsaWriter : ISubtitleWriter + public partial class SsaWriter : ISubtitleWriter { + [GeneratedRegex(@"\n", RegexOptions.IgnoreCase)] + private static partial Regex NewLineRegex(); + /// <inheritdoc /> public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken) { @@ -40,7 +43,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles var trackEvent = trackEvents[i]; var startTime = TimeSpan.FromTicks(trackEvent.StartPositionTicks).ToString(timeFormat, CultureInfo.InvariantCulture); var endTime = TimeSpan.FromTicks(trackEvent.EndPositionTicks).ToString(timeFormat, CultureInfo.InvariantCulture); - var text = Regex.Replace(trackEvent.Text, @"\n", "\\n", RegexOptions.IgnoreCase); + var text = NewLineRegex().Replace(trackEvent.Text, "\\n"); writer.WriteLine( "Dialogue: 0,{0},{1},Default,{2}", diff --git a/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs index e5c785bc57..ea45f2070a 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs @@ -9,8 +9,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles /// <summary> /// TTML subtitle writer. /// </summary> - public class TtmlWriter : ISubtitleWriter + public partial class TtmlWriter : ISubtitleWriter { + [GeneratedRegex(@"\\n", RegexOptions.IgnoreCase)] + private static partial Regex NewLineEscapeRegex(); + /// <inheritdoc /> public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken) { @@ -38,7 +41,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles { var text = trackEvent.Text; - text = Regex.Replace(text, @"\\n", "<br/>", RegexOptions.IgnoreCase); + text = NewLineEscapeRegex().Replace(text, "<br/>"); writer.WriteLine( "<p begin=\"{0}\" dur=\"{1}\">{2}</p>", diff --git a/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs index 38ef57dee3..3e0f47b5ae 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs @@ -10,8 +10,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles /// <summary> /// Subtitle writer for the WebVTT format. /// </summary> - public class VttWriter : ISubtitleWriter + public partial class VttWriter : ISubtitleWriter { + [GeneratedRegex(@"\\n", RegexOptions.IgnoreCase)] + private static partial Regex NewlineEscapeRegex(); + /// <inheritdoc /> public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken) { @@ -39,7 +42,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles var text = trackEvent.Text; // TODO: Not sure how to handle these - text = Regex.Replace(text, @"\\n", " ", RegexOptions.IgnoreCase); + text = NewlineEscapeRegex().Replace(text, " "); writer.WriteLine(text); writer.WriteLine(); diff --git a/MediaBrowser.Model/Dlna/SearchCriteria.cs b/MediaBrowser.Model/Dlna/SearchCriteria.cs index 77d6a55eaf..6f4a692c8c 100644 --- a/MediaBrowser.Model/Dlna/SearchCriteria.cs +++ b/MediaBrowser.Model/Dlna/SearchCriteria.cs @@ -5,7 +5,7 @@ using System.Text.RegularExpressions; namespace MediaBrowser.Model.Dlna { - public class SearchCriteria + public partial class SearchCriteria { public SearchCriteria(string search) { @@ -13,10 +13,10 @@ namespace MediaBrowser.Model.Dlna SearchType = SearchType.Unknown; - string[] factors = RegexSplit(search, "(and|or)"); + string[] factors = AndOrRegex().Split(search); foreach (string factor in factors) { - string[] subFactors = RegexSplit(factor.Trim().Trim('(').Trim(')').Trim(), "\\s", 3); + string[] subFactors = WhiteSpaceRegex().Split(factor.Trim().Trim('(').Trim(')').Trim(), 3); if (subFactors.Length == 3) { @@ -46,27 +46,10 @@ namespace MediaBrowser.Model.Dlna public SearchType SearchType { get; set; } - /// <summary> - /// Splits the specified string. - /// </summary> - /// <param name="str">The string.</param> - /// <param name="term">The term.</param> - /// <param name="limit">The limit.</param> - /// <returns>System.String[].</returns> - private static string[] RegexSplit(string str, string term, int limit) - { - return new Regex(term).Split(str, limit); - } + [GeneratedRegex("\\s")] + private static partial Regex WhiteSpaceRegex(); - /// <summary> - /// Splits the specified string. - /// </summary> - /// <param name="str">The string.</param> - /// <param name="term">The term.</param> - /// <returns>System.String[].</returns> - private static string[] RegexSplit(string str, string term) - { - return Regex.Split(str, term, RegexOptions.IgnoreCase); - } + [GeneratedRegex("(and|or)", RegexOptions.IgnoreCase)] + private static partial Regex AndOrRegex(); } } diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index e1dcbc9939..a7e8f774c6 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -25,7 +25,7 @@ namespace MediaBrowser.Providers.MediaInfo /// <summary> /// Probes audio files for metadata. /// </summary> - public class AudioFileProber + public partial class AudioFileProber { // Default LUFS value for use with the web interface, at -18db gain will be 1(no db gain). private const float DefaultLUFSValue = -18; @@ -58,6 +58,9 @@ namespace MediaBrowser.Providers.MediaInfo _mediaSourceManager = mediaSourceManager; } + [GeneratedRegex("I:\\s+(.*?)\\s+LUFS")] + private static partial Regex LUFSRegex(); + /// <summary> /// Probes the specified item for metadata. /// </summary> @@ -129,7 +132,7 @@ namespace MediaBrowser.Providers.MediaInfo output = await process.StandardError.ReadToEndAsync(cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); - MatchCollection split = Regex.Matches(output, @"I:\s+(.*?)\s+LUFS"); + MatchCollection split = LUFSRegex().Matches(output); if (split.Count != 0) { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs index 516eee758c..a7c93ac4c8 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs @@ -11,10 +11,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// <summary> /// Utilities for the TMDb provider. /// </summary> - public static class TmdbUtils + public static partial class TmdbUtils { - private static readonly Regex _nonWords = new(@"[\W_]+", RegexOptions.Compiled); - /// <summary> /// URL of the TMDb instance to use. /// </summary> @@ -50,6 +48,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb PersonKind.Producer }; + [GeneratedRegex(@"[\W_]+")] + private static partial Regex NonWordRegex(); + /// <summary> /// Cleans the name according to TMDb requirements. /// </summary> @@ -58,7 +59,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb public static string CleanName(string name) { // TMDb expects a space separated list of words make sure that is the case - return _nonWords.Replace(name, " "); + return NonWordRegex().Replace(name, " "); } /// <summary> diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs index 4f8f869ace..bf66a31458 100644 --- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs @@ -27,15 +27,12 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.XbmcMetadata.Savers { - public abstract class BaseNfoSaver : IMetadataFileSaver + public abstract partial class BaseNfoSaver : IMetadataFileSaver { public const string DateAddedFormat = "yyyy-MM-dd HH:mm:ss"; public const string YouTubeWatchUrl = "https://www.youtube.com/watch?v="; - // filters control characters but allows only properly-formed surrogate sequences - private const string _invalidXMLCharsRegex = @"(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F\uFEFF\uFFFE\uFFFF]"; - private static readonly HashSet<string> _commonTags = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "plot", @@ -148,6 +145,12 @@ namespace MediaBrowser.XbmcMetadata.Savers public static string SaverName => "Nfo"; + // filters control characters but allows only properly-formed surrogate sequences + // http://web.archive.org/web/20181230211547/https://emby.media/community/index.php?/topic/49071-nfo-not-generated-on-actualize-or-rescan-or-identify + // Web Archive version of link since it's not really explained in the thread. + [GeneratedRegex(@"(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F\uFEFF\uFFFE\uFFFF]")] + private static partial Regex InvalidXMLCharsRegexRegex(); + /// <inheritdoc /> public string GetSavePath(BaseItem item) => GetLocalSavePath(item); @@ -354,9 +357,7 @@ namespace MediaBrowser.XbmcMetadata.Savers if (!string.IsNullOrEmpty(stream.Language)) { - // http://web.archive.org/web/20181230211547/https://emby.media/community/index.php?/topic/49071-nfo-not-generated-on-actualize-or-rescan-or-identify - // Web Archive version of link since it's not really explained in the thread. - writer.WriteElementString("language", Regex.Replace(stream.Language, _invalidXMLCharsRegex, string.Empty)); + writer.WriteElementString("language", InvalidXMLCharsRegexRegex().Replace(stream.Language, string.Empty)); } var scanType = stream.IsInterlaced ? "interlaced" : "progressive"; diff --git a/jellyfin.ruleset b/jellyfin.ruleset index b611caa11a..c846e2cd41 100644 --- a/jellyfin.ruleset +++ b/jellyfin.ruleset @@ -60,6 +60,8 @@ <Rule Id="SA1515" Action="None" /> <!-- disable warning SA1600: Elements should be documented --> <Rule Id="SA1600" Action="None" /> + <!-- disable warning SA1601: Partial elements should be documented --> + <Rule Id="SA1601" Action="None" /> <!-- disable warning SA1602: Enumeration items should be documented --> <Rule Id="SA1602" Action="None" /> <!-- disable warning SA1633: The file header is missing or not located at the top of the file --> diff --git a/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs index a7a3338df4..537fbf2366 100644 --- a/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs +++ b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs @@ -123,8 +123,7 @@ public partial class StripCollageBuilder var typeFace = SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright); // use the system fallback to find a typeface for the given CJK character - var nonCjkPattern = @"[^\p{IsCJKUnifiedIdeographs}\p{IsCJKUnifiedIdeographsExtensionA}\p{IsKatakana}\p{IsHiragana}\p{IsHangulSyllables}\p{IsHangulJamo}]"; - var filteredName = Regex.Replace(libraryName ?? string.Empty, nonCjkPattern, string.Empty); + var filteredName = NonCjkPatternRegex().Replace(libraryName ?? string.Empty, string.Empty); if (!string.IsNullOrEmpty(filteredName)) { typeFace = SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, filteredName[0]); diff --git a/src/Jellyfin.Extensions/StringExtensions.cs b/src/Jellyfin.Extensions/StringExtensions.cs index b22eb7c4ea..fd8f7e59a4 100644 --- a/src/Jellyfin.Extensions/StringExtensions.cs +++ b/src/Jellyfin.Extensions/StringExtensions.cs @@ -6,11 +6,13 @@ namespace Jellyfin.Extensions /// <summary> /// Provides extensions methods for <see cref="string" />. /// </summary> - public static class StringExtensions + public static partial class StringExtensions { // Matches non-conforming unicode chars // https://mnaoumov.wordpress.com/2014/06/14/stripping-invalid-characters-from-utf-16-strings/ - private static readonly Regex _nonConformingUnicode = new Regex("([\ud800-\udbff](?![\udc00-\udfff]))|((?<![\ud800-\udbff])[\udc00-\udfff])|(\ufffd)", RegexOptions.Compiled); + + [GeneratedRegex("([\ud800-\udbff](?![\udc00-\udfff]))|((?<![\ud800-\udbff])[\udc00-\udfff])|(�)")] + private static partial Regex NonConformingUnicodeRegex(); /// <summary> /// Removes the diacritics character from the strings. @@ -19,7 +21,7 @@ namespace Jellyfin.Extensions /// <returns>The string without diacritics character.</returns> public static string RemoveDiacritics(this string text) => Diacritics.Extensions.StringExtensions.RemoveDiacritics( - _nonConformingUnicode.Replace(text, string.Empty)); + NonConformingUnicodeRegex().Replace(text, string.Empty)); /// <summary> /// Checks whether or not the specified string has diacritics in it. @@ -28,7 +30,7 @@ namespace Jellyfin.Extensions /// <returns>True if the string has diacritics, false otherwise.</returns> public static bool HasDiacritics(this string text) => Diacritics.Extensions.StringExtensions.HasDiacritics(text) - || _nonConformingUnicode.IsMatch(text); + || NonConformingUnicodeRegex().IsMatch(text); /// <summary> /// Counts the number of occurrences of [needle] in the string. diff --git a/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs b/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs index 925e8fa199..f157f01e5a 100644 --- a/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs +++ b/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs @@ -25,10 +25,13 @@ using Xunit; namespace Jellyfin.Providers.Tests.Manager { - public class ItemImageProviderTests + public partial class ItemImageProviderTests { private const string TestDataImagePath = "Test Data/Images/blank{0}.jpg"; + [GeneratedRegex("[0-9]+")] + private static partial Regex NumbersRegex(); + [Fact] public void ValidateImages_PhotoEmptyProviders_NoChange() { @@ -463,7 +466,7 @@ namespace Jellyfin.Providers.Tests.Manager // images from the provider manager are sorted by preference (earlier images are higher priority) so we can verify that low url numbers are chosen foreach (var image in actualImages) { - var index = int.Parse(Regex.Match(image.Path, @"[0-9]+").Value, NumberStyles.Integer, CultureInfo.InvariantCulture); + var index = int.Parse(NumbersRegex().Match(image.Path).ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture); Assert.True(index < imageCount); } } From 83d6f21fd021c66ab3b1f6501ecdfd016fee540f Mon Sep 17 00:00:00 2001 From: Bond-009 <bond.009@outlook.com> Date: Tue, 23 May 2023 15:44:47 +0200 Subject: [PATCH 415/858] Fix clean regex --- Emby.Naming/Audio/AlbumParser.cs | 2 +- src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Emby.Naming/Audio/AlbumParser.cs b/Emby.Naming/Audio/AlbumParser.cs index 73424a1345..97961778f3 100644 --- a/Emby.Naming/Audio/AlbumParser.cs +++ b/Emby.Naming/Audio/AlbumParser.cs @@ -23,7 +23,7 @@ namespace Emby.Naming.Audio _options = options; } - [GeneratedRegex(@"([-\.\(\)]|\s+)")] + [GeneratedRegex(@"[-\.\(\)\s]+")] private static partial Regex CleanRegex(); /// <summary> diff --git a/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs index 537fbf2366..a8f80f7e21 100644 --- a/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs +++ b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs @@ -23,6 +23,9 @@ public partial class StripCollageBuilder _skiaEncoder = skiaEncoder; } + [GeneratedRegex(@"[^\p{IsCJKUnifiedIdeographs}\p{IsCJKUnifiedIdeographsExtensionA}\p{IsKatakana}\p{IsHiragana}\p{IsHangulSyllables}\p{IsHangulJamo}]")] + private static partial Regex NonCjkPatternRegex(); + [GeneratedRegex(@"\p{IsArabic}|\p{IsArmenian}|\p{IsHebrew}|\p{IsSyriac}|\p{IsThaana}")] private static partial Regex IsRtlTextRegex(); From b5bbb98175e0542d43c01f80c15e8dce04e58b53 Mon Sep 17 00:00:00 2001 From: Cody Robibero <cody@robibe.ro> Date: Thu, 29 Jun 2023 05:44:36 -0600 Subject: [PATCH 416/858] Fix Websocket OpenApi (#9935) * Further split inbound and outbound messages * Fix datatype for inbound start messages * fixes from review --- .../HttpServer/WebSocketConnection.cs | 16 +++++++-- .../Session/WebSocketController.cs | 4 +-- .../Net/BasePeriodicWebSocketListener.cs | 4 +-- .../Net/IWebSocketConnection.cs | 1 - .../Net/WebSocketMessage.cs | 6 ---- .../Net/WebSocketMessageInfo.cs | 2 -- .../Inbound/ActivityLogEntryStartMessage.cs | 10 +++--- .../Inbound/ActivityLogEntryStopMessage.cs | 13 +------- .../Inbound/InboundKeepAliveMessage.cs | 14 ++++++++ .../Inbound/ScheduledTasksInfoStartMessage.cs | 9 +++-- .../Inbound/ScheduledTasksInfoStopMessage.cs | 13 +------- .../Inbound/SessionsStartMessage.cs | 8 ++--- .../Inbound/SessionsStopMessage.cs | 12 +------ .../InboundWebSocketMessage.cs | 7 ++-- .../InboundWebSocketMessageOfT.cs | 26 +++++++++++++++ .../Outbound/ActivityLogEntryMessage.cs | 2 +- .../Outbound/ForceKeepAliveMessage.cs | 2 +- .../Outbound/GeneralCommandMessage.cs | 2 +- .../Outbound/LibraryChangedMessage.cs | 2 +- .../Outbound/OutboundKeepAliveMessage.cs | 14 ++++++++ .../WebSocketMessages/Outbound/PlayMessage.cs | 2 +- .../Outbound/PlaystateMessage.cs | 2 +- .../PluginInstallationCancelledMessage.cs | 2 +- .../PluginInstallationCompletedMessage.cs | 2 +- .../PluginInstallationFailedMessage.cs | 2 +- .../Outbound/PluginInstallingMessage.cs | 2 +- .../Outbound/PluginUninstalledMessage.cs | 2 +- .../Outbound/RefreshProgressMessage.cs | 2 +- .../Outbound/RestartRequiredMessage.cs | 2 +- .../Outbound/ScheduledTaskEndedMessage.cs | 2 +- .../Outbound/ScheduledTasksInfoMessage.cs | 2 +- .../Outbound/SeriesTimerCancelledMessage.cs | 2 +- .../Outbound/SeriesTimerCreatedMessage.cs | 2 +- .../Outbound/ServerRestartingMessage.cs | 2 +- .../Outbound/ServerShuttingDownMessage.cs | 2 +- .../Outbound/SessionsMessage.cs | 5 +-- .../Outbound/SyncPlayCommandMessage.cs | 2 +- .../SyncPlayGroupUpdateCommandMessage.cs | 2 +- ...layGroupUpdateCommandOfGroupInfoMessage.cs | 2 +- ...pUpdateCommandOfGroupStateUpdateMessage.cs | 2 +- ...upUpdateCommandOfPlayQueueUpdateMessage.cs | 2 +- ...ncPlayGroupUpdateCommandOfStringMessage.cs | 2 +- .../Outbound/TimerCancelledMessage.cs | 2 +- .../Outbound/TimerCreatedMessage.cs | 2 +- .../Outbound/UserDataChangedMessage.cs | 2 +- .../Outbound/UserDeletedMessage.cs | 2 +- .../Outbound/UserUpdatedMessage.cs | 2 +- .../OutboundWebSocketMessage.cs | 11 +++++-- .../OutboundWebSocketMessageOfT.cs | 33 +++++++++++++++++++ .../Shared/KeepAliveMessage.cs | 23 ------------- 50 files changed, 165 insertions(+), 126 deletions(-) create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Inbound/InboundKeepAliveMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/InboundWebSocketMessageOfT.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Outbound/OutboundKeepAliveMessage.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/OutboundWebSocketMessageOfT.cs delete mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Shared/KeepAliveMessage.cs diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs index b1a99853ad..af79c18c4e 100644 --- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs +++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs @@ -9,7 +9,7 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Extensions.Json; using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Net; +using MediaBrowser.Controller.Net.WebSocketMessages; using MediaBrowser.Model.Session; using Microsoft.Extensions.Logging; @@ -85,6 +85,18 @@ namespace Emby.Server.Implementations.HttpServer /// <value>The state.</value> public WebSocketState State => _socket.State; + /// <summary> + /// Sends a message asynchronously. + /// </summary> + /// <param name="message">The message.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + public Task SendAsync(WebSocketMessage message, CancellationToken cancellationToken) + { + var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions); + return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken); + } + /// <summary> /// Sends a message asynchronously. /// </summary> @@ -224,7 +236,7 @@ namespace Emby.Server.Implementations.HttpServer { LastKeepAliveDate = DateTime.UtcNow; return SendAsync( - new WebSocketMessage<string> + new OutboundWebSocketMessage { MessageId = Guid.NewGuid(), MessageType = SessionMessageType.KeepAlive diff --git a/Emby.Server.Implementations/Session/WebSocketController.cs b/Emby.Server.Implementations/Session/WebSocketController.cs index cdc736950e..cf8e0fb006 100644 --- a/Emby.Server.Implementations/Session/WebSocketController.cs +++ b/Emby.Server.Implementations/Session/WebSocketController.cs @@ -7,8 +7,8 @@ using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Net.WebSocketMessages; using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Net; using MediaBrowser.Model.Session; using Microsoft.Extensions.Logging; @@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.Session } return socket.SendAsync( - new WebSocketMessage<T> + new OutboundWebSocketMessage<T> { Data = data, MessageType = name, diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs index 0524999c79..a07d9b3eb4 100644 --- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs +++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs @@ -9,7 +9,7 @@ using System.Linq; using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Model.Net; +using MediaBrowser.Controller.Net.WebSocketMessages; using MediaBrowser.Model.Session; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -169,7 +169,7 @@ namespace MediaBrowser.Controller.Net if (data is not null) { await connection.SendAsync( - new WebSocketMessage<TReturnDataType> + new OutboundWebSocketMessage<TReturnDataType> { MessageId = Guid.NewGuid(), MessageType = Type, diff --git a/MediaBrowser.Controller/Net/IWebSocketConnection.cs b/MediaBrowser.Controller/Net/IWebSocketConnection.cs index 4f2492b891..04b333230d 100644 --- a/MediaBrowser.Controller/Net/IWebSocketConnection.cs +++ b/MediaBrowser.Controller/Net/IWebSocketConnection.cs @@ -5,7 +5,6 @@ using System.Net; using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Model.Net; namespace MediaBrowser.Controller.Net { diff --git a/MediaBrowser.Controller/Net/WebSocketMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessage.cs index c02bcd70b6..92183e7929 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessage.cs @@ -1,4 +1,3 @@ -using System; using System.Text.Json.Serialization; using MediaBrowser.Model.Session; @@ -15,11 +14,6 @@ public abstract class WebSocketMessage /// </summary> public virtual SessionMessageType MessageType { get; set; } - /// <summary> - /// Gets or sets the message id. - /// </summary> - public Guid MessageId { get; set; } - /// <summary> /// Gets or sets the server id. /// </summary> diff --git a/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs b/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs index 6f7ebf1565..2d986b7b34 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs @@ -1,7 +1,5 @@ #nullable disable -using MediaBrowser.Model.Net; - namespace MediaBrowser.Controller.Net { /// <summary> diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ActivityLogEntryStartMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ActivityLogEntryStartMessage.cs index b9f71b9225..b3a60199a9 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ActivityLogEntryStartMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ActivityLogEntryStartMessage.cs @@ -1,20 +1,20 @@ -using System.Collections.Generic; using System.ComponentModel; -using MediaBrowser.Model.Activity; using MediaBrowser.Model.Session; namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound; /// <summary> /// Activity log entry start message. +/// Data is the timing data encoded as "$initialDelay,$interval" in ms. /// </summary> -public class ActivityLogEntryStartMessage : WebSocketMessage<IReadOnlyCollection<ActivityLogEntry>>, IInboundWebSocketMessage +public class ActivityLogEntryStartMessage : InboundWebSocketMessage<string> { /// <summary> /// Initializes a new instance of the <see cref="ActivityLogEntryStartMessage"/> class. + /// Data is the timing data encoded as "$initialDelay,$interval" in ms. /// </summary> - /// <param name="data">Collection of activity log entries.</param> - public ActivityLogEntryStartMessage(IReadOnlyCollection<ActivityLogEntry> data) + /// <param name="data">The timing data encoded as "$initialDelay,$interval".</param> + public ActivityLogEntryStartMessage(string data) : base(data) { } diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ActivityLogEntryStopMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ActivityLogEntryStopMessage.cs index eac129b20a..6f65cb2c77 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ActivityLogEntryStopMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ActivityLogEntryStopMessage.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; using System.ComponentModel; -using MediaBrowser.Model.Activity; using MediaBrowser.Model.Session; namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound; @@ -8,17 +6,8 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound; /// <summary> /// Activity log entry stop message. /// </summary> -public class ActivityLogEntryStopMessage : WebSocketMessage<IReadOnlyCollection<ActivityLogEntry>>, IInboundWebSocketMessage +public class ActivityLogEntryStopMessage : InboundWebSocketMessage { - /// <summary> - /// Initializes a new instance of the <see cref="ActivityLogEntryStopMessage"/> class. - /// </summary> - /// <param name="data">Collection of activity log entries.</param> - public ActivityLogEntryStopMessage(IReadOnlyCollection<ActivityLogEntry> data) - : base(data) - { - } - /// <inheritdoc /> [DefaultValue(SessionMessageType.ActivityLogEntryStop)] public override SessionMessageType MessageType => SessionMessageType.ActivityLogEntryStop; diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/InboundKeepAliveMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/InboundKeepAliveMessage.cs new file mode 100644 index 0000000000..fec7cb4e41 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/InboundKeepAliveMessage.cs @@ -0,0 +1,14 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound; + +/// <summary> +/// Keep alive websocket messages. +/// </summary> +public class InboundKeepAliveMessage : InboundWebSocketMessage +{ + /// <inheritdoc /> + [DefaultValue(SessionMessageType.KeepAlive)] + public override SessionMessageType MessageType => SessionMessageType.KeepAlive; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ScheduledTasksInfoStartMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ScheduledTasksInfoStartMessage.cs index dd2a7145e3..bf98470bf2 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ScheduledTasksInfoStartMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ScheduledTasksInfoStartMessage.cs @@ -1,20 +1,19 @@ -using System.Collections.Generic; using System.ComponentModel; using MediaBrowser.Model.Session; -using MediaBrowser.Model.Tasks; namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound; /// <summary> /// Scheduled tasks info start message. +/// Data is the timing data encoded as "$initialDelay,$interval" in ms. /// </summary> -public class ScheduledTasksInfoStartMessage : WebSocketMessage<IReadOnlyCollection<TaskInfo>>, IInboundWebSocketMessage +public class ScheduledTasksInfoStartMessage : InboundWebSocketMessage<string> { /// <summary> /// Initializes a new instance of the <see cref="ScheduledTasksInfoStartMessage"/> class. /// </summary> - /// <param name="data">Collection of task info.</param> - public ScheduledTasksInfoStartMessage(IReadOnlyCollection<TaskInfo> data) + /// <param name="data">The timing data encoded as $initialDelay,$interval.</param> + public ScheduledTasksInfoStartMessage(string data) : base(data) { } diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ScheduledTasksInfoStopMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ScheduledTasksInfoStopMessage.cs index 84e1f01667..f36739c70a 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ScheduledTasksInfoStopMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ScheduledTasksInfoStopMessage.cs @@ -1,24 +1,13 @@ -using System.Collections.Generic; using System.ComponentModel; using MediaBrowser.Model.Session; -using MediaBrowser.Model.Tasks; namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound; /// <summary> /// Scheduled tasks info stop message. /// </summary> -public class ScheduledTasksInfoStopMessage : WebSocketMessage<IReadOnlyCollection<TaskInfo>>, IInboundWebSocketMessage +public class ScheduledTasksInfoStopMessage : InboundWebSocketMessage { - /// <summary> - /// Initializes a new instance of the <see cref="ScheduledTasksInfoStopMessage"/> class. - /// </summary> - /// <param name="data">Collection of task info.</param> - public ScheduledTasksInfoStopMessage(IReadOnlyCollection<TaskInfo> data) - : base(data) - { - } - /// <inheritdoc /> [DefaultValue(SessionMessageType.ScheduledTasksInfoStop)] public override SessionMessageType MessageType => SessionMessageType.ScheduledTasksInfoStop; diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/SessionsStartMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/SessionsStartMessage.cs index e35a5dc3ad..a40a0c79ee 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/SessionsStartMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/SessionsStartMessage.cs @@ -1,19 +1,19 @@ using System.ComponentModel; -using MediaBrowser.Controller.Session; using MediaBrowser.Model.Session; namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound; /// <summary> /// Sessions start message. +/// Data is the timing data encoded as "$initialDelay,$interval" in ms. /// </summary> -public class SessionsStartMessage : WebSocketMessage<SessionInfo>, IInboundWebSocketMessage +public class SessionsStartMessage : InboundWebSocketMessage<string> { /// <summary> /// Initializes a new instance of the <see cref="SessionsStartMessage"/> class. /// </summary> - /// <param name="data">Session info.</param> - public SessionsStartMessage(SessionInfo data) + /// <param name="data">The timing data encoded as $initialDelay,$interval.</param> + public SessionsStartMessage(string data) : base(data) { } diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/SessionsStopMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/SessionsStopMessage.cs index 7e3582d640..288d111c5c 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/SessionsStopMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/SessionsStopMessage.cs @@ -1,5 +1,4 @@ using System.ComponentModel; -using MediaBrowser.Controller.Session; using MediaBrowser.Model.Session; namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound; @@ -7,17 +6,8 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound; /// <summary> /// Sessions stop message. /// </summary> -public class SessionsStopMessage : WebSocketMessage<SessionInfo>, IInboundWebSocketMessage +public class SessionsStopMessage : InboundWebSocketMessage { - /// <summary> - /// Initializes a new instance of the <see cref="SessionsStopMessage"/> class. - /// </summary> - /// <param name="data">Session info.</param> - public SessionsStopMessage(SessionInfo data) - : base(data) - { - } - /// <inheritdoc /> [DefaultValue(SessionMessageType.SessionsStop)] public override SessionMessageType MessageType => SessionMessageType.SessionsStop; diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/InboundWebSocketMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/InboundWebSocketMessage.cs index 20ca888e11..8d6e821df8 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/InboundWebSocketMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/InboundWebSocketMessage.cs @@ -1,9 +1,8 @@ -namespace MediaBrowser.Controller.Net.WebSocketMessages; +namespace MediaBrowser.Controller.Net.WebSocketMessages; /// <summary> -/// Class representing the list of outbound websocket message types. -/// Only used in openapi generation. +/// Inbound websocket message. /// </summary> -public class InboundWebSocketMessage : WebSocketMessage +public class InboundWebSocketMessage : WebSocketMessage, IInboundWebSocketMessage { } diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/InboundWebSocketMessageOfT.cs b/MediaBrowser.Controller/Net/WebSocketMessages/InboundWebSocketMessageOfT.cs new file mode 100644 index 0000000000..4da5e7d31f --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/InboundWebSocketMessageOfT.cs @@ -0,0 +1,26 @@ +#pragma warning disable SA1649 // File name must equal class name. + +namespace MediaBrowser.Controller.Net.WebSocketMessages; + +/// <summary> +/// Inbound websocket message with data. +/// </summary> +/// <typeparam name="T">The data type.</typeparam> +public class InboundWebSocketMessage<T> : WebSocketMessage<T>, IInboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="InboundWebSocketMessage{T}"/> class. + /// </summary> + public InboundWebSocketMessage() + { + } + + /// <summary> + /// Initializes a new instance of the <see cref="InboundWebSocketMessage{T}"/> class. + /// </summary> + /// <param name="data">The data to send.</param> + protected InboundWebSocketMessage(T data) + { + Data = data; + } +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ActivityLogEntryMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ActivityLogEntryMessage.cs index 5650ee4bbe..2a098615d5 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ActivityLogEntryMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ActivityLogEntryMessage.cs @@ -8,7 +8,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Activity log created message. /// </summary> -public class ActivityLogEntryMessage : WebSocketMessage<IReadOnlyList<ActivityLogEntry>>, IOutboundWebSocketMessage +public class ActivityLogEntryMessage : OutboundWebSocketMessage<IReadOnlyList<ActivityLogEntry>> { /// <summary> /// Initializes a new instance of the <see cref="ActivityLogEntryMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ForceKeepAliveMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ForceKeepAliveMessage.cs index 94ade5e817..ca55340a05 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ForceKeepAliveMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ForceKeepAliveMessage.cs @@ -6,7 +6,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Force keep alive websocket messages. /// </summary> -public class ForceKeepAliveMessage : WebSocketMessage<int>, IOutboundWebSocketMessage +public class ForceKeepAliveMessage : OutboundWebSocketMessage<int> { /// <summary> /// Initializes a new instance of the <see cref="ForceKeepAliveMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/GeneralCommandMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/GeneralCommandMessage.cs index 6c71e73f9d..5fbbb06242 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/GeneralCommandMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/GeneralCommandMessage.cs @@ -6,7 +6,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// General command websocket message. /// </summary> -public class GeneralCommandMessage : WebSocketMessage<GeneralCommand>, IOutboundWebSocketMessage +public class GeneralCommandMessage : OutboundWebSocketMessage<GeneralCommand> { /// <summary> /// Initializes a new instance of the <see cref="GeneralCommandMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/LibraryChangedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/LibraryChangedMessage.cs index 6432ae8efb..47417c4059 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/LibraryChangedMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/LibraryChangedMessage.cs @@ -7,7 +7,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Library changed message. /// </summary> -public class LibraryChangedMessage : WebSocketMessage<LibraryUpdateInfo>, IOutboundWebSocketMessage +public class LibraryChangedMessage : OutboundWebSocketMessage<LibraryUpdateInfo> { /// <summary> /// Initializes a new instance of the <see cref="LibraryChangedMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/OutboundKeepAliveMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/OutboundKeepAliveMessage.cs new file mode 100644 index 0000000000..d907dcff95 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/OutboundKeepAliveMessage.cs @@ -0,0 +1,14 @@ +using System.ComponentModel; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; + +/// <summary> +/// Keep alive websocket messages. +/// </summary> +public class OutboundKeepAliveMessage : OutboundWebSocketMessage +{ + /// <inheritdoc /> + [DefaultValue(SessionMessageType.KeepAlive)] + public override SessionMessageType MessageType => SessionMessageType.KeepAlive; +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PlayMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PlayMessage.cs index 7f943bda10..86ee2ff900 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PlayMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PlayMessage.cs @@ -6,7 +6,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Play command websocket message. /// </summary> -public class PlayMessage : WebSocketMessage<PlayRequest>, IOutboundWebSocketMessage +public class PlayMessage : OutboundWebSocketMessage<PlayRequest> { /// <summary> /// Initializes a new instance of the <see cref="PlayMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PlaystateMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PlaystateMessage.cs index 804ccb37d6..cd6d28cb30 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PlaystateMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PlaystateMessage.cs @@ -6,7 +6,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Playstate message. /// </summary> -public class PlaystateMessage : WebSocketMessage<PlaystateRequest>, IOutboundWebSocketMessage +public class PlaystateMessage : OutboundWebSocketMessage<PlaystateRequest> { /// <summary> /// Initializes a new instance of the <see cref="PlaystateMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationCancelledMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationCancelledMessage.cs index 3d7dc5c937..17fd259384 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationCancelledMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationCancelledMessage.cs @@ -7,7 +7,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Plugin installation cancelled message. /// </summary> -public class PluginInstallationCancelledMessage : WebSocketMessage<InstallationInfo>, IOutboundWebSocketMessage +public class PluginInstallationCancelledMessage : OutboundWebSocketMessage<InstallationInfo> { /// <summary> /// Initializes a new instance of the <see cref="PluginInstallationCancelledMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationCompletedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationCompletedMessage.cs index 81268007fd..3e60198bac 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationCompletedMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationCompletedMessage.cs @@ -7,7 +7,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Plugin installation completed message. /// </summary> -public class PluginInstallationCompletedMessage : WebSocketMessage<InstallationInfo>, IOutboundWebSocketMessage +public class PluginInstallationCompletedMessage : OutboundWebSocketMessage<InstallationInfo> { /// <summary> /// Initializes a new instance of the <see cref="PluginInstallationCompletedMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationFailedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationFailedMessage.cs index 9177f12938..40032f16e4 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationFailedMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationFailedMessage.cs @@ -7,7 +7,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Plugin installation failed message. /// </summary> -public class PluginInstallationFailedMessage : WebSocketMessage<InstallationInfo>, IOutboundWebSocketMessage +public class PluginInstallationFailedMessage : OutboundWebSocketMessage<InstallationInfo> { /// <summary> /// Initializes a new instance of the <see cref="PluginInstallationFailedMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallingMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallingMessage.cs index e371440a0f..28861896f7 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallingMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallingMessage.cs @@ -7,7 +7,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Package installing message. /// </summary> -public class PluginInstallingMessage : WebSocketMessage<InstallationInfo>, IOutboundWebSocketMessage +public class PluginInstallingMessage : OutboundWebSocketMessage<InstallationInfo> { /// <summary> /// Initializes a new instance of the <see cref="PluginInstallingMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginUninstalledMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginUninstalledMessage.cs index b2994fc956..ca49591194 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginUninstalledMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginUninstalledMessage.cs @@ -7,7 +7,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Plugin uninstalled message. /// </summary> -public class PluginUninstalledMessage : WebSocketMessage<PluginInfo>, IOutboundWebSocketMessage +public class PluginUninstalledMessage : OutboundWebSocketMessage<PluginInfo> { /// <summary> /// Initializes a new instance of the <see cref="PluginUninstalledMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/RefreshProgressMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/RefreshProgressMessage.cs index 42dbc30295..41b3cd46ab 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/RefreshProgressMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/RefreshProgressMessage.cs @@ -7,7 +7,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Refresh progress message. /// </summary> -public class RefreshProgressMessage : WebSocketMessage<Dictionary<string, string>>, IOutboundWebSocketMessage +public class RefreshProgressMessage : OutboundWebSocketMessage<Dictionary<string, string>> { /// <summary> /// Initializes a new instance of the <see cref="RefreshProgressMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/RestartRequiredMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/RestartRequiredMessage.cs index 3f3d9e4c84..a89f19b617 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/RestartRequiredMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/RestartRequiredMessage.cs @@ -6,7 +6,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Restart required. /// </summary> -public class RestartRequiredMessage : WebSocketMessage, IOutboundWebSocketMessage +public class RestartRequiredMessage : OutboundWebSocketMessage { /// <inheritdoc /> [DefaultValue(SessionMessageType.RestartRequired)] diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ScheduledTaskEndedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ScheduledTaskEndedMessage.cs index d69662b004..afa36fb722 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ScheduledTaskEndedMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ScheduledTaskEndedMessage.cs @@ -7,7 +7,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Scheduled task ended message. /// </summary> -public class ScheduledTaskEndedMessage : WebSocketMessage<TaskResult>, IOutboundWebSocketMessage +public class ScheduledTaskEndedMessage : OutboundWebSocketMessage<TaskResult> { /// <summary> /// Initializes a new instance of the <see cref="ScheduledTaskEndedMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ScheduledTasksInfoMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ScheduledTasksInfoMessage.cs index 41a05b0de2..c7360779f9 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ScheduledTasksInfoMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ScheduledTasksInfoMessage.cs @@ -8,7 +8,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Scheduled tasks info message. /// </summary> -public class ScheduledTasksInfoMessage : WebSocketMessage<IReadOnlyList<TaskInfo>>, IOutboundWebSocketMessage +public class ScheduledTasksInfoMessage : OutboundWebSocketMessage<IReadOnlyList<TaskInfo>> { /// <summary> /// Initializes a new instance of the <see cref="ScheduledTasksInfoMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SeriesTimerCancelledMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SeriesTimerCancelledMessage.cs index d4950b8b67..f832c8935e 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SeriesTimerCancelledMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SeriesTimerCancelledMessage.cs @@ -7,7 +7,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Series timer cancelled message. /// </summary> -public class SeriesTimerCancelledMessage : WebSocketMessage<TimerEventInfo>, IOutboundWebSocketMessage +public class SeriesTimerCancelledMessage : OutboundWebSocketMessage<TimerEventInfo> { /// <summary> /// Initializes a new instance of the <see cref="SeriesTimerCancelledMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SeriesTimerCreatedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SeriesTimerCreatedMessage.cs index 091c10be6d..450b4c7994 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SeriesTimerCreatedMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SeriesTimerCreatedMessage.cs @@ -7,7 +7,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Series timer created message. /// </summary> -public class SeriesTimerCreatedMessage : WebSocketMessage<TimerEventInfo>, IOutboundWebSocketMessage +public class SeriesTimerCreatedMessage : OutboundWebSocketMessage<TimerEventInfo> { /// <summary> /// Initializes a new instance of the <see cref="SeriesTimerCreatedMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ServerRestartingMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ServerRestartingMessage.cs index a465d8b008..8f09c802fe 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ServerRestartingMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ServerRestartingMessage.cs @@ -6,7 +6,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Server restarting down message. /// </summary> -public class ServerRestartingMessage : WebSocketMessage, IOutboundWebSocketMessage +public class ServerRestartingMessage : OutboundWebSocketMessage { /// <inheritdoc /> [DefaultValue(SessionMessageType.ServerRestarting)] diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ServerShuttingDownMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ServerShuttingDownMessage.cs index 0b998a5239..485e71b6e3 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ServerShuttingDownMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ServerShuttingDownMessage.cs @@ -6,7 +6,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Server shutting down message. /// </summary> -public class ServerShuttingDownMessage : WebSocketMessage, IOutboundWebSocketMessage +public class ServerShuttingDownMessage : OutboundWebSocketMessage { /// <inheritdoc /> [DefaultValue(SessionMessageType.ServerShuttingDown)] diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs index 4c91e0bca2..3504831b87 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.ComponentModel; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Session; @@ -7,13 +8,13 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Sessions message. /// </summary> -public class SessionsMessage : WebSocketMessage<SessionInfo>, IOutboundWebSocketMessage +public class SessionsMessage : OutboundWebSocketMessage<IReadOnlyList<SessionInfo>> { /// <summary> /// Initializes a new instance of the <see cref="SessionsMessage"/> class. /// </summary> /// <param name="data">Session info.</param> - public SessionsMessage(SessionInfo data) + public SessionsMessage(IReadOnlyList<SessionInfo> data) : base(data) { } diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayCommandMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayCommandMessage.cs index 17a0fc66e5..d0624ec016 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayCommandMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayCommandMessage.cs @@ -7,7 +7,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Sync play command. /// </summary> -public class SyncPlayCommandMessage : WebSocketMessage<SendCommand>, IOutboundWebSocketMessage +public class SyncPlayCommandMessage : OutboundWebSocketMessage<SendCommand> { /// <summary> /// Initializes a new instance of the <see cref="SyncPlayCommandMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandMessage.cs index d145d0e01a..6a501aa7ea 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandMessage.cs @@ -7,7 +7,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Untyped sync play command. /// </summary> -public class SyncPlayGroupUpdateCommandMessage : WebSocketMessage<GroupUpdate>, IOutboundWebSocketMessage +public class SyncPlayGroupUpdateCommandMessage : OutboundWebSocketMessage<GroupUpdate> { /// <summary> /// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupInfoMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupInfoMessage.cs index 668392c66b..47f706e2a4 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupInfoMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupInfoMessage.cs @@ -8,7 +8,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// Sync play group update command with group info. /// GroupUpdateTypes: GroupJoined. /// </summary> -public class SyncPlayGroupUpdateCommandOfGroupInfoMessage : WebSocketMessage<GroupUpdate<GroupInfoDto>>, IOutboundWebSocketMessage +public class SyncPlayGroupUpdateCommandOfGroupInfoMessage : OutboundWebSocketMessage<GroupUpdate<GroupInfoDto>> { /// <summary> /// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandOfGroupInfoMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage.cs index ec8c3344f4..11ddb1e250 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage.cs @@ -8,7 +8,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// Sync play group update command with group state update. /// GroupUpdateTypes: StateUpdate. /// </summary> -public class SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage : WebSocketMessage<GroupUpdate<GroupStateUpdate>>, IOutboundWebSocketMessage +public class SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage : OutboundWebSocketMessage<GroupUpdate<GroupStateUpdate>> { /// <summary> /// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage.cs index 465363f143..7e73399b1b 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage.cs @@ -8,7 +8,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// Sync play group update command with play queue update. /// GroupUpdateTypes: PlayQueue. /// </summary> -public class SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage : WebSocketMessage<GroupUpdate<PlayQueueUpdate>>, IOutboundWebSocketMessage +public class SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage : OutboundWebSocketMessage<GroupUpdate<PlayQueueUpdate>> { /// <summary> /// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfStringMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfStringMessage.cs index b87e9bf715..5b5ccd3eda 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfStringMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfStringMessage.cs @@ -8,7 +8,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// Sync play group update command with string. /// GroupUpdateTypes: GroupDoesNotExist (error), LibraryAccessDenied (error), NotInGroup (error), GroupLeft (groupId), UserJoined (username), UserLeft (username). /// </summary> -public class SyncPlayGroupUpdateCommandOfStringMessage : WebSocketMessage<GroupUpdate<string>>, IOutboundWebSocketMessage +public class SyncPlayGroupUpdateCommandOfStringMessage : OutboundWebSocketMessage<GroupUpdate<string>> { /// <summary> /// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandOfStringMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/TimerCancelledMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/TimerCancelledMessage.cs index 0e70549ef8..f44fd126b6 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/TimerCancelledMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/TimerCancelledMessage.cs @@ -7,7 +7,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Timer cancelled message. /// </summary> -public class TimerCancelledMessage : WebSocketMessage<TimerEventInfo>, IOutboundWebSocketMessage +public class TimerCancelledMessage : OutboundWebSocketMessage<TimerEventInfo> { /// <summary> /// Initializes a new instance of the <see cref="TimerCancelledMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/TimerCreatedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/TimerCreatedMessage.cs index 295b3081ce..8c1e102eb2 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/TimerCreatedMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/TimerCreatedMessage.cs @@ -7,7 +7,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Timer created message. /// </summary> -public class TimerCreatedMessage : WebSocketMessage<TimerEventInfo>, IOutboundWebSocketMessage +public class TimerCreatedMessage : OutboundWebSocketMessage<TimerEventInfo> { /// <summary> /// Initializes a new instance of the <see cref="TimerCreatedMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserDataChangedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserDataChangedMessage.cs index b60769540d..6a053643d8 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserDataChangedMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserDataChangedMessage.cs @@ -6,7 +6,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// User data changed message. /// </summary> -public class UserDataChangedMessage : WebSocketMessage<UserDataChangeInfo>, IOutboundWebSocketMessage +public class UserDataChangedMessage : OutboundWebSocketMessage<UserDataChangeInfo> { /// <summary> /// Initializes a new instance of the <see cref="UserDataChangedMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserDeletedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserDeletedMessage.cs index 6d527be7f2..add3f77717 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserDeletedMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserDeletedMessage.cs @@ -7,7 +7,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// User deleted message. /// </summary> -public class UserDeletedMessage : WebSocketMessage<Guid>, IOutboundWebSocketMessage +public class UserDeletedMessage : OutboundWebSocketMessage<Guid> { /// <summary> /// Initializes a new instance of the <see cref="UserDeletedMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserUpdatedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserUpdatedMessage.cs index 99e9a1f911..9a72deae1f 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserUpdatedMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserUpdatedMessage.cs @@ -7,7 +7,7 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// User updated message. /// </summary> -public class UserUpdatedMessage : WebSocketMessage<UserDto>, IOutboundWebSocketMessage +public class UserUpdatedMessage : OutboundWebSocketMessage<UserDto> { /// <summary> /// Initializes a new instance of the <see cref="UserUpdatedMessage"/> class. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/OutboundWebSocketMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/OutboundWebSocketMessage.cs index dba3c8392b..ad97796e70 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/OutboundWebSocketMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/OutboundWebSocketMessage.cs @@ -1,9 +1,14 @@ +using System; + namespace MediaBrowser.Controller.Net.WebSocketMessages; /// <summary> -/// Class representing the list of outbound websocket message types. -/// Only used in openapi generation. +/// Outbound websocket message. /// </summary> -public class OutboundWebSocketMessage : WebSocketMessage +public class OutboundWebSocketMessage : WebSocketMessage, IOutboundWebSocketMessage { + /// <summary> + /// Gets or sets the message id. + /// </summary> + public Guid MessageId { get; set; } } diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/OutboundWebSocketMessageOfT.cs b/MediaBrowser.Controller/Net/WebSocketMessages/OutboundWebSocketMessageOfT.cs new file mode 100644 index 0000000000..f09f294b41 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessages/OutboundWebSocketMessageOfT.cs @@ -0,0 +1,33 @@ +#pragma warning disable SA1649 // File name must equal class name. + +using System; + +namespace MediaBrowser.Controller.Net.WebSocketMessages; + +/// <summary> +/// Outbound websocket message with data. +/// </summary> +/// <typeparam name="T">The data type.</typeparam> +public class OutboundWebSocketMessage<T> : WebSocketMessage<T>, IOutboundWebSocketMessage +{ + /// <summary> + /// Initializes a new instance of the <see cref="OutboundWebSocketMessage{T}"/> class. + /// </summary> + public OutboundWebSocketMessage() + { + } + + /// <summary> + /// Initializes a new instance of the <see cref="OutboundWebSocketMessage{T}"/> class. + /// </summary> + /// <param name="data">The data to send.</param> + protected OutboundWebSocketMessage(T data) + { + Data = data; + } + + /// <summary> + /// Gets or sets the message id. + /// </summary> + public Guid MessageId { get; set; } +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Shared/KeepAliveMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Shared/KeepAliveMessage.cs deleted file mode 100644 index 7f636212ca..0000000000 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Shared/KeepAliveMessage.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.ComponentModel; -using MediaBrowser.Model.Session; - -namespace MediaBrowser.Controller.Net.WebSocketMessages.Shared; - -/// <summary> -/// Keep alive websocket messages. -/// </summary> -public class KeepAliveMessage : WebSocketMessage<int>, IInboundWebSocketMessage, IOutboundWebSocketMessage -{ - /// <summary> - /// Initializes a new instance of the <see cref="KeepAliveMessage"/> class. - /// </summary> - /// <param name="data">The seconds to keep alive for.</param> - public KeepAliveMessage(int data) - : base(data) - { - } - - /// <inheritdoc /> - [DefaultValue(SessionMessageType.KeepAlive)] - public override SessionMessageType MessageType => SessionMessageType.KeepAlive; -} From 46763b7661948b463303fd5b12831a146e987886 Mon Sep 17 00:00:00 2001 From: Ronan Charles-Lorel <ronan.charleslorel@gmail.com> Date: Thu, 29 Jun 2023 15:21:39 +0200 Subject: [PATCH 417/858] Remove call to Path.GetInvalidFileNameChars Superseded by a static char list to avoid platform-dependent issues --- Emby.Server.Implementations/IO/ManagedFileSystem.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 60ab668cde..e9e75166bc 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -275,9 +275,14 @@ namespace Emby.Server.Implementations.IO /// <exception cref="ArgumentNullException">The filename is null.</exception> public string GetValidFilename(string filename) { - // necessary because (as per the doc) GetInvalidFileNameChars is not exhaustive and may not return all invalid chars, which creates issues - char[] genericInvalidChars = { ':' }; - var invalid = Path.GetInvalidFileNameChars().Concat(genericInvalidChars).ToArray(); + // using a character list instead of GetInvalidFileNameChars, as it is not exhaustive and may not return all invalid chars + char[] invalid = { + '\"', '<', '>', '|', '\0', + (char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10, + (char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20, + (char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30, + (char)31, ':', '*', '?', '\\', '/' + }; var first = filename.IndexOfAny(invalid); if (first == -1) { From 07c142d5bd8d1dbea61cb16d5089f04f00b2ca3a Mon Sep 17 00:00:00 2001 From: Ronan Charles-Lorel <ronan.charleslorel@gmail.com> Date: Thu, 29 Jun 2023 16:04:45 +0200 Subject: [PATCH 418/858] Moving invalid chars list at class level with a better name --- .../IO/ManagedFileSystem.cs | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index e9e75166bc..193185dfea 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -20,7 +20,13 @@ namespace Emby.Server.Implementations.IO private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>(); private readonly string _tempPath; private static readonly bool _isEnvironmentCaseInsensitive = OperatingSystem.IsWindows(); - + private static readonly char[] _invalidPathCharacters = { + '\"', '<', '>', '|', '\0', + (char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10, + (char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20, + (char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30, + (char)31, ':', '*', '?', '\\', '/' + }; /// <summary> /// Initializes a new instance of the <see cref="ManagedFileSystem"/> class. /// </summary> @@ -275,15 +281,7 @@ namespace Emby.Server.Implementations.IO /// <exception cref="ArgumentNullException">The filename is null.</exception> public string GetValidFilename(string filename) { - // using a character list instead of GetInvalidFileNameChars, as it is not exhaustive and may not return all invalid chars - char[] invalid = { - '\"', '<', '>', '|', '\0', - (char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10, - (char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20, - (char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30, - (char)31, ':', '*', '?', '\\', '/' - }; - var first = filename.IndexOfAny(invalid); + var first = filename.IndexOfAny(_invalidPathCharacters); if (first == -1) { // Fast path for clean strings @@ -292,7 +290,7 @@ namespace Emby.Server.Implementations.IO return string.Create( filename.Length, - (filename, invalid, first), + (filename, _invalidPathCharacters, first), (chars, state) => { state.filename.AsSpan().CopyTo(chars); @@ -300,7 +298,7 @@ namespace Emby.Server.Implementations.IO chars[state.first++] = ' '; var len = chars.Length; - foreach (var c in state.invalid) + foreach (var c in state._invalidPathCharacters) { for (int i = state.first; i < len; i++) { From 6be45f73bc60d7b4646d7967d29634eea0b7e422 Mon Sep 17 00:00:00 2001 From: Niels van Velzen <git@ndat.nl> Date: Thu, 29 Jun 2023 21:16:29 +0200 Subject: [PATCH 419/858] Simplify file extension checks in lyrics parsers and provider --- MediaBrowser.Providers/Lyric/DefaultLyricProvider.cs | 4 ++-- MediaBrowser.Providers/Lyric/LrcLyricParser.cs | 4 ++-- MediaBrowser.Providers/Lyric/TxtLyricParser.cs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/MediaBrowser.Providers/Lyric/DefaultLyricProvider.cs b/MediaBrowser.Providers/Lyric/DefaultLyricProvider.cs index f828ec26b9..ae6350dd7d 100644 --- a/MediaBrowser.Providers/Lyric/DefaultLyricProvider.cs +++ b/MediaBrowser.Providers/Lyric/DefaultLyricProvider.cs @@ -10,7 +10,7 @@ namespace MediaBrowser.Providers.Lyric; /// <inheritdoc /> public class DefaultLyricProvider : ILyricProvider { - private static readonly string[] _lyricExtensions = { "lrc", "elrc", "txt", "elrc" }; + private static readonly string[] _lyricExtensions = { ".lrc", ".elrc", ".txt" }; /// <inheritdoc /> public string Name => "DefaultLyricProvider"; @@ -55,7 +55,7 @@ public class DefaultLyricProvider : ILyricProvider foreach (var lyricFilePath in Directory.GetFiles(itemDirectoryPath, $"{Path.GetFileNameWithoutExtension(item.Path)}.*")) { - if (_lyricExtensions.Contains(Path.GetExtension(lyricFilePath.AsSpan())[1..], StringComparison.OrdinalIgnoreCase)) + if (_lyricExtensions.Contains(Path.GetExtension(lyricFilePath.AsSpan()), StringComparison.OrdinalIgnoreCase)) { return lyricFilePath; } diff --git a/MediaBrowser.Providers/Lyric/LrcLyricParser.cs b/MediaBrowser.Providers/Lyric/LrcLyricParser.cs index 01a0dddf1f..7f1ecd7435 100644 --- a/MediaBrowser.Providers/Lyric/LrcLyricParser.cs +++ b/MediaBrowser.Providers/Lyric/LrcLyricParser.cs @@ -18,7 +18,7 @@ public class LrcLyricParser : ILyricParser { private readonly LyricParser _lrcLyricParser; - private static readonly string[] _supportedMediaTypes = { "lrc", "elrc" }; + private static readonly string[] _supportedMediaTypes = { ".lrc", ".elrc" }; private static readonly string[] _acceptedTimeFormats = { "HH:mm:ss", "H:mm:ss", "mm:ss", "m:ss" }; /// <summary> @@ -41,7 +41,7 @@ public class LrcLyricParser : ILyricParser /// <inheritdoc /> public LyricResponse? ParseLyrics(LyricFile lyrics) { - if (!_supportedMediaTypes.Contains(Path.GetExtension(lyrics.Name.AsSpan())[1..], StringComparison.OrdinalIgnoreCase)) + if (!_supportedMediaTypes.Contains(Path.GetExtension(lyrics.Name.AsSpan()), StringComparison.OrdinalIgnoreCase)) { return null; } diff --git a/MediaBrowser.Providers/Lyric/TxtLyricParser.cs b/MediaBrowser.Providers/Lyric/TxtLyricParser.cs index 7e029ee42a..ed4f2cb7a6 100644 --- a/MediaBrowser.Providers/Lyric/TxtLyricParser.cs +++ b/MediaBrowser.Providers/Lyric/TxtLyricParser.cs @@ -11,7 +11,7 @@ namespace MediaBrowser.Providers.Lyric; /// </summary> public class TxtLyricParser : ILyricParser { - private static readonly string[] _supportedMediaTypes = { "lrc", "elrc", "txt" }; + private static readonly string[] _supportedMediaTypes = { ".lrc", ".elrc", ".txt" }; private static readonly string[] _lineBreakCharacters = { "\r\n", "\r", "\n" }; /// <inheritdoc /> @@ -26,7 +26,7 @@ public class TxtLyricParser : ILyricParser /// <inheritdoc /> public LyricResponse? ParseLyrics(LyricFile lyrics) { - if (!_supportedMediaTypes.Contains(Path.GetExtension(lyrics.Name.AsSpan())[1..], StringComparison.OrdinalIgnoreCase)) + if (!_supportedMediaTypes.Contains(Path.GetExtension(lyrics.Name.AsSpan()), StringComparison.OrdinalIgnoreCase)) { return null; } From c21140eeb54fe628de957d07a06bd800ad518f42 Mon Sep 17 00:00:00 2001 From: Ronan Charles-Lorel <ronan.charleslorel@gmail.com> Date: Sat, 1 Jul 2023 03:24:19 +0200 Subject: [PATCH 420/858] Formatting Fixes debug build? Co-authored-by: Bond-009 <bond.009@outlook.com> --- .../IO/ManagedFileSystem.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 193185dfea..d27024f8ae 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -20,13 +20,15 @@ namespace Emby.Server.Implementations.IO private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>(); private readonly string _tempPath; private static readonly bool _isEnvironmentCaseInsensitive = OperatingSystem.IsWindows(); - private static readonly char[] _invalidPathCharacters = { - '\"', '<', '>', '|', '\0', - (char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10, - (char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20, - (char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30, - (char)31, ':', '*', '?', '\\', '/' - }; + private static readonly char[] _invalidPathCharacters = + { + '\"', '<', '>', '|', '\0', + (char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10, + (char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20, + (char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30, + (char)31, ':', '*', '?', '\\', '/' + }; + /// <summary> /// Initializes a new instance of the <see cref="ManagedFileSystem"/> class. /// </summary> From 4dc87a6f93249b15d6660781c0026b77f1066c37 Mon Sep 17 00:00:00 2001 From: Ronan Charles-Lorel <ronan.charleslorel@gmail.com> Date: Sat, 1 Jul 2023 03:37:18 +0200 Subject: [PATCH 421/858] Align indentation on bottom brace of new list Should stop error SA1137 in debug build --- Emby.Server.Implementations/IO/ManagedFileSystem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index d27024f8ae..0ba4a488b0 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -27,7 +27,7 @@ namespace Emby.Server.Implementations.IO (char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20, (char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30, (char)31, ':', '*', '?', '\\', '/' - }; + }; /// <summary> /// Initializes a new instance of the <see cref="ManagedFileSystem"/> class. From 0ae4d175a184b86ec0267b1bdb89c8c59d40c325 Mon Sep 17 00:00:00 2001 From: Niels van Velzen <git@ndat.nl> Date: Sat, 1 Jul 2023 11:16:21 +0200 Subject: [PATCH 422/858] Check for empty string in DefaultLyricProvider --- MediaBrowser.Controller/Lyrics/LyricFile.cs | 2 +- MediaBrowser.Providers/Lyric/DefaultLyricProvider.cs | 5 ++++- MediaBrowser.Providers/Lyric/TxtLyricParser.cs | 6 ------ 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/MediaBrowser.Controller/Lyrics/LyricFile.cs b/MediaBrowser.Controller/Lyrics/LyricFile.cs index 21096797ac..ede89403c6 100644 --- a/MediaBrowser.Controller/Lyrics/LyricFile.cs +++ b/MediaBrowser.Controller/Lyrics/LyricFile.cs @@ -9,7 +9,7 @@ public class LyricFile /// Initializes a new instance of the <see cref="LyricFile"/> class. /// </summary> /// <param name="name">The name.</param> - /// <param name="content">The content.</param> + /// <param name="content">The content, must not be empty.</param> public LyricFile(string name, string content) { Name = name; diff --git a/MediaBrowser.Providers/Lyric/DefaultLyricProvider.cs b/MediaBrowser.Providers/Lyric/DefaultLyricProvider.cs index ae6350dd7d..a26f2babbf 100644 --- a/MediaBrowser.Providers/Lyric/DefaultLyricProvider.cs +++ b/MediaBrowser.Providers/Lyric/DefaultLyricProvider.cs @@ -32,7 +32,10 @@ public class DefaultLyricProvider : ILyricProvider if (path is not null) { var content = await File.ReadAllTextAsync(path).ConfigureAwait(false); - return new LyricFile(path, content); + if (content.Length != 0) + { + return new LyricFile(path, content); + } } return null; diff --git a/MediaBrowser.Providers/Lyric/TxtLyricParser.cs b/MediaBrowser.Providers/Lyric/TxtLyricParser.cs index ed4f2cb7a6..706f13dbcd 100644 --- a/MediaBrowser.Providers/Lyric/TxtLyricParser.cs +++ b/MediaBrowser.Providers/Lyric/TxtLyricParser.cs @@ -32,12 +32,6 @@ public class TxtLyricParser : ILyricParser } string[] lyricTextLines = lyrics.Content.Split(_lineBreakCharacters, StringSplitOptions.None); - - if (lyricTextLines.Length == 0) - { - return null; - } - LyricLine[] lyricList = new LyricLine[lyricTextLines.Length]; for (int lyricLineIndex = 0; lyricLineIndex < lyricTextLines.Length; lyricLineIndex++) From 0af5373f6d9050e9bb5a7cb4ed19742a8893d074 Mon Sep 17 00:00:00 2001 From: Niels van Velzen <git@ndat.nl> Date: Sat, 1 Jul 2023 14:07:59 +0200 Subject: [PATCH 423/858] Use string.IsNullOrEmpty --- MediaBrowser.Providers/Lyric/DefaultLyricProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Providers/Lyric/DefaultLyricProvider.cs b/MediaBrowser.Providers/Lyric/DefaultLyricProvider.cs index a26f2babbf..ab09f278aa 100644 --- a/MediaBrowser.Providers/Lyric/DefaultLyricProvider.cs +++ b/MediaBrowser.Providers/Lyric/DefaultLyricProvider.cs @@ -32,7 +32,7 @@ public class DefaultLyricProvider : ILyricProvider if (path is not null) { var content = await File.ReadAllTextAsync(path).ConfigureAwait(false); - if (content.Length != 0) + if (!string.IsNullOrEmpty(content)) { return new LyricFile(path, content); } From 0e1ae2def24dd623d7edb765f4f9b92567d862e3 Mon Sep 17 00:00:00 2001 From: Nick <20588554+nicknsy@users.noreply.github.com> Date: Sat, 1 Jul 2023 16:16:41 -0700 Subject: [PATCH 424/858] Add CreateTiles to ITrickplayManager --- .../Trickplay/TrickplayManager.cs | 14 ++++++++++---- .../Trickplay/ITrickplayManager.cs | 14 ++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs index 34b27e21a8..e3bfdd50d6 100644 --- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs +++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs @@ -7,6 +7,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; +using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; @@ -34,6 +35,7 @@ public class TrickplayManager : ITrickplayManager private readonly IServerConfigurationManager _config; private readonly IImageEncoder _imageEncoder; private readonly IDbContextFactory<JellyfinDbContext> _dbProvider; + private readonly IApplicationPaths _appPaths; private static readonly SemaphoreSlim _resourcePool = new(1, 1); private static readonly string[] _trickplayImgExtensions = { ".jpg" }; @@ -49,6 +51,7 @@ public class TrickplayManager : ITrickplayManager /// <param name="config">The server configuration manager.</param> /// <param name="imageEncoder">The image encoder.</param> /// <param name="dbProvider">The database provider.</param> + /// <param name="appPaths">The application paths.</param> public TrickplayManager( ILogger<TrickplayManager> logger, IMediaEncoder mediaEncoder, @@ -57,7 +60,8 @@ public class TrickplayManager : ITrickplayManager ILibraryManager libraryManager, IServerConfigurationManager config, IImageEncoder imageEncoder, - IDbContextFactory<JellyfinDbContext> dbProvider) + IDbContextFactory<JellyfinDbContext> dbProvider, + IApplicationPaths appPaths) { _logger = logger; _mediaEncoder = mediaEncoder; @@ -67,6 +71,7 @@ public class TrickplayManager : ITrickplayManager _config = config; _imageEncoder = imageEncoder; _dbProvider = dbProvider; + _appPaths = appPaths; } /// <inheritdoc /> @@ -152,8 +157,7 @@ public class TrickplayManager : ITrickplayManager .ToList(); // Create tiles - var tilesTempDir = Path.Combine(imgTempDir, Guid.NewGuid().ToString("N")); - var trickplayInfo = CreateTiles(images, width, options, tilesTempDir, outputDir); + var trickplayInfo = CreateTiles(images, width, options, outputDir); // Save tiles info try @@ -194,13 +198,15 @@ public class TrickplayManager : ITrickplayManager } } - private TrickplayInfo CreateTiles(List<string> images, int width, TrickplayOptions options, string workDir, string outputDir) + /// <inheritdoc /> + public TrickplayInfo CreateTiles(List<string> images, int width, TrickplayOptions options, string outputDir) { if (images.Count == 0) { throw new ArgumentException("Can't create trickplay from 0 images."); } + var workDir = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid().ToString("N")); Directory.CreateDirectory(workDir); var trickplayInfo = new TrickplayInfo diff --git a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs index 0a1e780b2e..7bea54fbab 100644 --- a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs +++ b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Configuration; namespace MediaBrowser.Controller.Trickplay; @@ -21,6 +22,19 @@ public interface ITrickplayManager /// <returns>Task.</returns> Task RefreshTrickplayDataAsync(Video video, bool replace, CancellationToken cancellationToken); + /// <summary> + /// Creates trickplay tiles out of individual thumbnails. + /// </summary> + /// <param name="images">Ordered file paths of the thumbnails to be used.</param> + /// <param name="width">The width of a single thumbnail.</param> + /// <param name="options">The trickplay options.</param> + /// <param name="outputDir">The output directory.</param> + /// <returns>The associated trickplay information.</returns> + /// <remarks> + /// The output directory will be DELETED and replaced if it already exists. + /// </remarks> + TrickplayInfo CreateTiles(List<string> images, int width, TrickplayOptions options, string outputDir); + /// <summary> /// Get available trickplay resolutions and corresponding info. /// </summary> From 76538aacb90c4a6a2d38550215949222024ebe99 Mon Sep 17 00:00:00 2001 From: nyanmisaka <nst799610810@gmail.com> Date: Sun, 2 Jul 2023 01:41:17 -0400 Subject: [PATCH 425/858] Backport pull request #9928 from jellyfin/release-10.8.z Disable global_header on AMD VA-API encoder Original-merge: a732a28229564a6da7db18bab07bcee75d6f2648 Merged-by: Bond-009 <bond.009@outlook.com> Backported-by: Joshua M. Boniface <joshua@boniface.me> --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index c817cdfd95..e18c1733e1 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -1454,6 +1454,13 @@ namespace MediaBrowser.Controller.MediaEncoding args += keyFrameArg + gopArg; } + // global_header produced by AMD VA-API encoder causes non-playable fMP4 on iOS + if (codec.Contains("vaapi", StringComparison.OrdinalIgnoreCase) + && _mediaEncoder.IsVaapiDeviceAmd) + { + args += " -flags:v -global_header"; + } + return args; } From 9b0e44019a350ee89befcc92edcb95ae20787cf9 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Sun, 2 Jul 2023 12:34:50 +0200 Subject: [PATCH 426/858] Apply review suggestions --- Jellyfin.Networking/Manager/NetworkManager.cs | 7 ++-- MediaBrowser.Common/Net/INetworkManager.cs | 3 +- MediaBrowser.Common/Net/NetworkExtensions.cs | 35 ++++++++++--------- .../NetworkParseTests.cs | 11 +++--- 4 files changed, 30 insertions(+), 26 deletions(-) diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 1b1e91e9fe..7d21c26cd0 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Net; @@ -380,7 +381,7 @@ namespace Jellyfin.Networking.Manager // Remove all interfaces matching any virtual machine interface prefix if (config.IgnoreVirtualInterfaces) { - // Remove potentially exisiting * and split config string into prefixes + // Remove potentially existing * and split config string into prefixes var virtualInterfacePrefixes = config.VirtualInterfaceNames .Select(i => i.Replace("*", string.Empty, StringComparison.OrdinalIgnoreCase)); @@ -587,7 +588,7 @@ namespace Jellyfin.Networking.Manager } /// <inheritdoc/> - public bool TryParseInterface(string intf, out IReadOnlyList<IPData> result) + public bool TryParseInterface(string intf, [NotNullWhen(true)] out IReadOnlyList<IPData>? result) { var resultList = new List<IPData>(); if (string.IsNullOrEmpty(intf) || _interfaces is null) @@ -660,7 +661,7 @@ namespace Jellyfin.Networking.Manager /// <inheritdoc/> public IReadOnlyList<IPData> GetLoopbacks() { - if (!(IsIPv4Enabled && IsIPv4Enabled)) + if (!IsIPv4Enabled && !IsIPv6Enabled) { return Array.Empty<IPData>(); } diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs index a92b751f2a..c51090e385 100644 --- a/MediaBrowser.Common/Net/INetworkManager.cs +++ b/MediaBrowser.Common/Net/INetworkManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.NetworkInformation; using MediaBrowser.Model.Net; @@ -120,7 +121,7 @@ namespace MediaBrowser.Common.Net /// <param name="intf">Interface name.</param> /// <param name="result">Resulting object's IP addresses, if successful.</param> /// <returns>Success of the operation.</returns> - bool TryParseInterface(string intf, out IReadOnlyList<IPData> result); + bool TryParseInterface(string intf, [NotNullWhen(true)] out IReadOnlyList<IPData>? result); /// <summary> /// Returns all internal (LAN) bind interface addresses. diff --git a/MediaBrowser.Common/Net/NetworkExtensions.cs b/MediaBrowser.Common/Net/NetworkExtensions.cs index 8a14bf48db..ae85583a56 100644 --- a/MediaBrowser.Common/Net/NetworkExtensions.cs +++ b/MediaBrowser.Common/Net/NetworkExtensions.cs @@ -166,28 +166,30 @@ namespace MediaBrowser.Common.Net /// <param name="result">Collection of <see cref="IPNetwork"/>.</param> /// <param name="negated">Boolean signaling if negated or not negated values should be parsed.</param> /// <returns><c>True</c> if parsing was successful.</returns> - public static bool TryParseToSubnets(string[] values, out List<IPNetwork> result, bool negated = false) + public static bool TryParseToSubnets(string[] values, [NotNullWhen(true)] out IReadOnlyList<IPNetwork>? result, bool negated = false) { - result = new List<IPNetwork>(); - if (values is null || values.Length == 0) { + result = null; return false; } + var tmpResult = new List<IPNetwork>(); for (int a = 0; a < values.Length; a++) { if (TryParseToSubnet(values[a], out var innerResult, negated)) { - result.Add(innerResult); + tmpResult.Add(innerResult); } } - if (result.Count > 0) + if (tmpResult.Count > 0) { + result = tmpResult; return true; } + result = null; return false; } @@ -199,9 +201,8 @@ namespace MediaBrowser.Common.Net /// <param name="result">An <see cref="IPNetwork"/>.</param> /// <param name="negated">Boolean signaling if negated or not negated values should be parsed.</param> /// <returns><c>True</c> if parsing was successful.</returns> - public static bool TryParseToSubnet(ReadOnlySpan<char> value, out IPNetwork result, bool negated = false) + public static bool TryParseToSubnet(ReadOnlySpan<char> value, [NotNullWhen(true)] out IPNetwork? result, bool negated = false) { - result = new IPNetwork(IPAddress.None, 32); var splitString = value.Trim().Split('/'); if (splitString.MoveNext()) { @@ -224,25 +225,28 @@ namespace MediaBrowser.Common.Net if (int.TryParse(subnetBlock, out var netmask)) { result = new IPNetwork(address, netmask); + return true; } else if (IPAddress.TryParse(subnetBlock, out var netmaskAddress)) { result = new IPNetwork(address, NetworkExtensions.MaskToCidr(netmaskAddress)); + return true; } } else if (address.AddressFamily == AddressFamily.InterNetwork) { result = new IPNetwork(address, 32); + return true; } else if (address.AddressFamily == AddressFamily.InterNetworkV6) { result = new IPNetwork(address, 128); + return true; } - - return true; } } + result = null; return false; } @@ -254,11 +258,11 @@ namespace MediaBrowser.Common.Net /// <param name="isIPv4Enabled"><c>true</c> if IPv4 is enabled.</param> /// <param name="isIPv6Enabled"><c>true</c> if IPv6 is enabled.</param> /// <returns><c>true</c> if the parsing is successful, <c>false</c> if not.</returns> - public static bool TryParseHost(ReadOnlySpan<char> host, [NotNullWhen(true)] out IPAddress[] addresses, bool isIPv4Enabled = true, bool isIPv6Enabled = false) + public static bool TryParseHost(ReadOnlySpan<char> host, [NotNullWhen(true)] out IPAddress[]? addresses, bool isIPv4Enabled = true, bool isIPv6Enabled = false) { if (host.IsEmpty) { - addresses = Array.Empty<IPAddress>(); + addresses = null; return false; } @@ -301,9 +305,7 @@ namespace MediaBrowser.Common.Net } // Is an IP4 or IP4:port - host = hosts[0].Split('/')[0]; - - if (IPAddress.TryParse(host, out var address)) + if (IPAddress.TryParse(hosts[0].AsSpan().LeftPart('/'), out var address)) { if (((address.AddressFamily == AddressFamily.InterNetwork) && (!isIPv4Enabled && isIPv6Enabled)) || ((address.AddressFamily == AddressFamily.InterNetworkV6) && (isIPv4Enabled && !isIPv6Enabled))) @@ -318,10 +320,9 @@ namespace MediaBrowser.Common.Net return true; } } - else if (hosts.Count <= 9) // 8 octets + port + else if (hosts.Count > 0 && hosts.Count <= 9) // 8 octets + port { - var splitSpan = host.Split('/'); - if (splitSpan.MoveNext() && IPAddress.TryParse(splitSpan.Current, out var address)) + if (IPAddress.TryParse(host.LeftPart('/'), out var address)) { addresses = new[] { address }; return true; diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs index 8b7df0470d..77f18c5445 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs @@ -203,12 +203,13 @@ namespace Jellyfin.Networking.Tests using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); NetworkManager.MockNetworkSettings = string.Empty; - _ = nm.TryParseInterface(result, out IReadOnlyList<IPData>? resultObj); + // Check to see if DNS resolution is working. If not, skip test. + if (!NetworkExtensions.TryParseHost(source, out var host)) + { + return; + } - // Check to see if dns resolution is working. If not, skip test. - _ = NetworkExtensions.TryParseHost(source, out var host); - - if (resultObj is not null && host.Length > 0) + if (nm.TryParseInterface(result, out var resultObj)) { result = resultObj.First().Address.ToString(); var intf = nm.GetBindAddress(source, out _); From 649d884d2099cb2624005d9a7cd6e79c1f582cd3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 2 Jul 2023 17:17:48 +0000 Subject: [PATCH 427/858] chore(deps): update dependency fscheck.xunit to v2.16.6 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index c3532467af..910f4163c7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -18,7 +18,7 @@ <PackageVersion Include="DiscUtils.Udf" Version="0.16.13" /> <PackageVersion Include="DotNet.Glob" Version="3.1.3" /> <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.9.2" /> - <PackageVersion Include="FsCheck.Xunit" Version="2.16.5" /> + <PackageVersion Include="FsCheck.Xunit" Version="2.16.6" /> <PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" /> <PackageVersion Include="libse" Version="3.6.13" /> <PackageVersion Include="LrcParser" Version="2023.524.0" /> From 29308bbba79ae30603d3cb0955084160cc6a8335 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 2 Jul 2023 17:17:57 +0000 Subject: [PATCH 428/858] chore(deps): update dependency sharpfuzz to v2.1.1 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index c3532467af..17d480dfcf 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -64,7 +64,7 @@ <PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" /> <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.0.1" /> <PackageVersion Include="SerilogAnalyzer" Version="0.15.0" /> - <PackageVersion Include="SharpFuzz" Version="2.1.0" /> + <PackageVersion Include="SharpFuzz" Version="2.1.1" /> <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.3" /> <PackageVersion Include="SkiaSharp.Svg" Version="1.60.0" /> <PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.3" /> From 52252fcd554a7ac1105374ca6d1b440787820f0a Mon Sep 17 00:00:00 2001 From: Cody Robibero <cody@robibe.ro> Date: Sun, 2 Jul 2023 16:14:44 -0600 Subject: [PATCH 429/858] Fix sending websocket messages (#9948) --- .../HttpServer/WebSocketConnection.cs | 32 ++++++------------- .../Session/SessionWebSocketListener.cs | 9 ++---- .../Net/BasePeriodicWebSocketListener.cs | 1 - .../Net/IWebSocketConnection.cs | 12 ++++++- .../Net/WebSocketMessageInfo.cs | 4 ++- .../Net/WebSocketMessageOfT.cs | 5 ++- .../OutboundWebSocketMessage.cs | 2 +- .../OutboundWebSocketMessageOfT.cs | 2 +- 8 files changed, 29 insertions(+), 38 deletions(-) diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs index af79c18c4e..fd7653a32d 100644 --- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs +++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using Jellyfin.Extensions.Json; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net.WebSocketMessages; +using MediaBrowser.Controller.Net.WebSocketMessages.Outbound; using MediaBrowser.Model.Session; using Microsoft.Extensions.Logging; @@ -85,26 +86,15 @@ namespace Emby.Server.Implementations.HttpServer /// <value>The state.</value> public WebSocketState State => _socket.State; - /// <summary> - /// Sends a message asynchronously. - /// </summary> - /// <param name="message">The message.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - public Task SendAsync(WebSocketMessage message, CancellationToken cancellationToken) + /// <inheritdoc /> + public Task SendAsync(OutboundWebSocketMessage message, CancellationToken cancellationToken) { var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions); return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken); } - /// <summary> - /// Sends a message asynchronously. - /// </summary> - /// <typeparam name="T">The type of the message.</typeparam> - /// <param name="message">The message.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - public Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken) + /// <inheritdoc /> + public Task SendAsync<T>(OutboundWebSocketMessage<T> message, CancellationToken cancellationToken) { var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions); return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken); @@ -183,7 +173,7 @@ namespace Emby.Server.Implementations.HttpServer return; } - WebSocketMessage<object>? stub; + InboundWebSocketMessage<object>? stub; long bytesConsumed; try { @@ -224,10 +214,10 @@ namespace Emby.Server.Implementations.HttpServer } } - internal WebSocketMessage<object>? DeserializeWebSocketMessage(ReadOnlySequence<byte> bytes, out long bytesConsumed) + internal InboundWebSocketMessage<object>? DeserializeWebSocketMessage(ReadOnlySequence<byte> bytes, out long bytesConsumed) { var jsonReader = new Utf8JsonReader(bytes); - var ret = JsonSerializer.Deserialize<WebSocketMessage<object>>(ref jsonReader, _jsonOptions); + var ret = JsonSerializer.Deserialize<InboundWebSocketMessage<object>>(ref jsonReader, _jsonOptions); bytesConsumed = jsonReader.BytesConsumed; return ret; } @@ -236,11 +226,7 @@ namespace Emby.Server.Implementations.HttpServer { LastKeepAliveDate = DateTime.UtcNow; return SendAsync( - new OutboundWebSocketMessage - { - MessageId = Guid.NewGuid(), - MessageType = SessionMessageType.KeepAlive - }, + new OutboundKeepAliveMessage(), CancellationToken.None); } diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs index 4e427b1a4b..b3c93a904a 100644 --- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs +++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs @@ -6,9 +6,8 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Extensions; using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Net.WebSocketMessages.Outbound; using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Net; -using MediaBrowser.Model.Session; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -308,11 +307,7 @@ namespace Emby.Server.Implementations.Session private Task SendForceKeepAlive(IWebSocketConnection webSocket) { return webSocket.SendAsync( - new WebSocketMessage<int> - { - MessageType = SessionMessageType.ForceKeepAlive, - Data = WebSocketLostTimeout - }, + new ForceKeepAliveMessage(WebSocketLostTimeout), CancellationToken.None); } diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs index a07d9b3eb4..8f38d4976b 100644 --- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs +++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs @@ -171,7 +171,6 @@ namespace MediaBrowser.Controller.Net await connection.SendAsync( new OutboundWebSocketMessage<TReturnDataType> { - MessageId = Guid.NewGuid(), MessageType = Type, Data = data }, diff --git a/MediaBrowser.Controller/Net/IWebSocketConnection.cs b/MediaBrowser.Controller/Net/IWebSocketConnection.cs index 04b333230d..79f0846b4a 100644 --- a/MediaBrowser.Controller/Net/IWebSocketConnection.cs +++ b/MediaBrowser.Controller/Net/IWebSocketConnection.cs @@ -5,6 +5,7 @@ using System.Net; using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Controller.Net.WebSocketMessages; namespace MediaBrowser.Controller.Net { @@ -45,6 +46,15 @@ namespace MediaBrowser.Controller.Net /// <value>The remote end point.</value> IPAddress? RemoteEndPoint { get; } + /// <summary> + /// Sends a message asynchronously. + /// </summary> + /// <param name="message">The message.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + /// <exception cref="ArgumentNullException">The message is null.</exception> + Task SendAsync(OutboundWebSocketMessage message, CancellationToken cancellationToken); + /// <summary> /// Sends a message asynchronously. /// </summary> @@ -53,7 +63,7 @@ namespace MediaBrowser.Controller.Net /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> /// <exception cref="ArgumentNullException">The message is null.</exception> - Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken); + Task SendAsync<T>(OutboundWebSocketMessage<T> message, CancellationToken cancellationToken); Task ProcessAsync(CancellationToken cancellationToken = default); } diff --git a/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs b/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs index 2d986b7b34..f7a9ccc44d 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs @@ -1,11 +1,13 @@ #nullable disable +using MediaBrowser.Controller.Net.WebSocketMessages; + namespace MediaBrowser.Controller.Net { /// <summary> /// Class WebSocketMessageInfo. /// </summary> - public class WebSocketMessageInfo : WebSocketMessage<string> + public class WebSocketMessageInfo : InboundWebSocketMessage<string> { /// <summary> /// Gets or sets the connection. diff --git a/MediaBrowser.Controller/Net/WebSocketMessageOfT.cs b/MediaBrowser.Controller/Net/WebSocketMessageOfT.cs index 7c35c8010d..11e5a6bb29 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessageOfT.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessageOfT.cs @@ -6,13 +6,12 @@ namespace MediaBrowser.Controller.Net; /// Class WebSocketMessage. /// </summary> /// <typeparam name="T">The type of the data.</typeparam> -// TODO make this abstract, remove empty ctor. -public class WebSocketMessage<T> : WebSocketMessage +public abstract class WebSocketMessage<T> : WebSocketMessage { /// <summary> /// Initializes a new instance of the <see cref="WebSocketMessage{T}"/> class. /// </summary> - public WebSocketMessage() + protected WebSocketMessage() { } diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/OutboundWebSocketMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/OutboundWebSocketMessage.cs index ad97796e70..1782458516 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/OutboundWebSocketMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/OutboundWebSocketMessage.cs @@ -10,5 +10,5 @@ public class OutboundWebSocketMessage : WebSocketMessage, IOutboundWebSocketMess /// <summary> /// Gets or sets the message id. /// </summary> - public Guid MessageId { get; set; } + public Guid MessageId { get; set; } = Guid.NewGuid(); } diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/OutboundWebSocketMessageOfT.cs b/MediaBrowser.Controller/Net/WebSocketMessages/OutboundWebSocketMessageOfT.cs index f09f294b41..cce331805c 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/OutboundWebSocketMessageOfT.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/OutboundWebSocketMessageOfT.cs @@ -29,5 +29,5 @@ public class OutboundWebSocketMessage<T> : WebSocketMessage<T>, IOutboundWebSock /// <summary> /// Gets or sets the message id. /// </summary> - public Guid MessageId { get; set; } + public Guid MessageId { get; set; } = Guid.NewGuid(); } From 3f658515206a7ccea468de3fe9916e171de89b0d Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Mon, 3 Jul 2023 10:03:39 +0200 Subject: [PATCH 430/858] Apply review suggestions --- Jellyfin.Networking/Manager/NetworkManager.cs | 149 +++++++----------- MediaBrowser.Common/Net/NetworkExtensions.cs | 14 +- 2 files changed, 61 insertions(+), 102 deletions(-) diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 7d21c26cd0..c80038e7d0 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -590,35 +590,22 @@ namespace Jellyfin.Networking.Manager /// <inheritdoc/> public bool TryParseInterface(string intf, [NotNullWhen(true)] out IReadOnlyList<IPData>? result) { - var resultList = new List<IPData>(); - if (string.IsNullOrEmpty(intf) || _interfaces is null) + if (string.IsNullOrEmpty(intf) + || _interfaces is null + || _interfaces.Count == 0) { - result = resultList.AsReadOnly(); + result = null; return false; } // Match all interfaces starting with names starting with token - var matchedInterfaces = _interfaces.Where(s => s.Name.Equals(intf, StringComparison.OrdinalIgnoreCase)).OrderBy(x => x.Index).ToList(); - if (matchedInterfaces.Count > 0) - { - _logger.LogInformation("Interface {Token} used in settings. Using its interface addresses.", intf); - - // Use interface IP instead of name - foreach (var iface in matchedInterfaces) - { - if ((IsIPv4Enabled && iface.Address.AddressFamily == AddressFamily.InterNetwork) - || (IsIPv6Enabled && iface.Address.AddressFamily == AddressFamily.InterNetworkV6)) - { - resultList.Add(iface); - } - } - - result = resultList.AsReadOnly(); - return true; - } - - result = resultList.AsReadOnly(); - return false; + result = _interfaces + .Where(i => i.Name.Equals(intf, StringComparison.OrdinalIgnoreCase) + && ((IsIPv4Enabled && i.Address.AddressFamily == AddressFamily.InterNetwork) + || (IsIPv6Enabled && i.Address.AddressFamily == AddressFamily.InterNetworkV6))) + .OrderBy(x => x.Index) + .ToArray(); + return result.Count > 0; } /// <inheritdoc/> @@ -693,11 +680,7 @@ namespace Jellyfin.Networking.Manager if (individualInterfaces) { - foreach (var iface in _interfaces) - { - result.Add(iface); - } - + result.AddRange(_interfaces); return result; } @@ -792,37 +775,37 @@ namespace Jellyfin.Networking.Manager .ThenBy(x => x.Index) .ToList(); - if (availableInterfaces.Count > 0) + if (availableInterfaces.Count == 0) { - // If no source address is given, use the preferred (first) interface - if (source is null) - { - result = NetworkExtensions.FormatIPString(availableInterfaces.First().Address); - _logger.LogDebug("{Source}: Using first internal interface as bind address: {Result}", source, result); - return result; - } - - // Does the request originate in one of the interface subnets? - // (For systems with multiple internal network cards, and multiple subnets) - foreach (var intf in availableInterfaces) - { - if (intf.Subnet.Contains(source)) - { - result = NetworkExtensions.FormatIPString(intf.Address); - _logger.LogDebug("{Source}: Found interface with matching subnet, using it as bind address: {Result}", source, result); - return result; - } - } - - // Fallback to first available interface - result = NetworkExtensions.FormatIPString(availableInterfaces[0].Address); - _logger.LogDebug("{Source}: No matching interfaces found, using preferred interface as bind address: {Result}", source, result); + // There isn't any others, so we'll use the loopback. + result = IsIPv4Enabled && !IsIPv6Enabled ? "127.0.0.1" : "::1"; + _logger.LogWarning("{Source}: Only loopback {Result} returned, using that as bind address.", source, result); return result; } - // There isn't any others, so we'll use the loopback. - result = IsIPv4Enabled && !IsIPv6Enabled ? "127.0.0.1" : "::1"; - _logger.LogWarning("{Source}: Only loopback {Result} returned, using that as bind address.", source, result); + // If no source address is given, use the preferred (first) interface + if (source is null) + { + result = NetworkExtensions.FormatIPString(availableInterfaces.First().Address); + _logger.LogDebug("{Source}: Using first internal interface as bind address: {Result}", source, result); + return result; + } + + // Does the request originate in one of the interface subnets? + // (For systems with multiple internal network cards, and multiple subnets) + foreach (var intf in availableInterfaces) + { + if (intf.Subnet.Contains(source)) + { + result = NetworkExtensions.FormatIPString(intf.Address); + _logger.LogDebug("{Source}: Found interface with matching subnet, using it as bind address: {Result}", source, result); + return result; + } + } + + // Fallback to first available interface + result = NetworkExtensions.FormatIPString(availableInterfaces[0].Address); + _logger.LogDebug("{Source}: No matching interfaces found, using preferred interface as bind address: {Result}", source, result); return result; } @@ -845,18 +828,13 @@ namespace Jellyfin.Networking.Manager if (NetworkExtensions.TryParseHost(address, out var addresses, IsIPv4Enabled, IsIPv6Enabled)) { - bool match = false; foreach (var ept in addresses) { - match = match || IPAddress.IsLoopback(ept) || (_lanSubnets.Any(x => x.Contains(ept)) && !_excludedSubnets.Any(x => x.Contains(ept))); - - if (match) + if (IPAddress.IsLoopback(ept) || (_lanSubnets.Any(x => x.Contains(ept)) && !_excludedSubnets.Any(x => x.Contains(ept)))) { - break; + return true; } } - - return match; } return false; @@ -881,33 +859,23 @@ namespace Jellyfin.Networking.Manager private bool CheckIfLanAndNotExcluded(IPAddress address) { - bool match = false; foreach (var lanSubnet in _lanSubnets) { - match = lanSubnet.Contains(address); - - if (match) + if (lanSubnet.Contains(address)) { - break; + foreach (var excludedSubnet in _excludedSubnets) + { + if (excludedSubnet.Contains(address)) + { + return false; + } + } + + return true; } } - if (!match) - { - return match; - } - - foreach (var excludedSubnet in _excludedSubnets) - { - match = match && !excludedSubnet.Contains(address); - - if (!match) - { - break; - } - } - - return match; + return false; } /// <summary> @@ -1017,14 +985,11 @@ namespace Jellyfin.Networking.Manager .OrderByDescending(x => x.Subnet.Contains(source)) .ThenBy(x => x.Index) .Select(x => x.Address) - .FirstOrDefault(); + .First(); - if (bindAddress is not null) - { - result = NetworkExtensions.FormatIPString(bindAddress); - _logger.LogDebug("{Source}: External request received, matching external bind address found: {Result}", source, result); - return true; - } + result = NetworkExtensions.FormatIPString(bindAddress); + _logger.LogDebug("{Source}: External request received, matching external bind address found: {Result}", source, result); + return true; } _logger.LogWarning("{Source}: External request received, no matching external bind address found, trying internal addresses.", source); @@ -1073,7 +1038,7 @@ namespace Jellyfin.Networking.Manager // (For systems with multiple network cards and/or multiple subnets) foreach (var intf in extResult) { - if (!IsInLocalNetwork(intf.Address) && intf.Subnet.Contains(source)) + if (intf.Subnet.Contains(source)) { result = NetworkExtensions.FormatIPString(intf.Address); _logger.LogDebug("{Source}: Found external interface with matching subnet, using it as bind address: {Result}", source, result); diff --git a/MediaBrowser.Common/Net/NetworkExtensions.cs b/MediaBrowser.Common/Net/NetworkExtensions.cs index ae85583a56..47475b3da9 100644 --- a/MediaBrowser.Common/Net/NetworkExtensions.cs +++ b/MediaBrowser.Common/Net/NetworkExtensions.cs @@ -183,14 +183,8 @@ namespace MediaBrowser.Common.Net } } - if (tmpResult.Count > 0) - { - result = tmpResult; - return true; - } - - result = null; - return false; + result = tmpResult; + return tmpResult.Count > 0; } /// <summary> @@ -307,8 +301,8 @@ namespace MediaBrowser.Common.Net // Is an IP4 or IP4:port if (IPAddress.TryParse(hosts[0].AsSpan().LeftPart('/'), out var address)) { - if (((address.AddressFamily == AddressFamily.InterNetwork) && (!isIPv4Enabled && isIPv6Enabled)) || - ((address.AddressFamily == AddressFamily.InterNetworkV6) && (isIPv4Enabled && !isIPv6Enabled))) + if (((address.AddressFamily == AddressFamily.InterNetwork) && (!isIPv4Enabled && isIPv6Enabled)) + || ((address.AddressFamily == AddressFamily.InterNetworkV6) && (isIPv4Enabled && !isIPv6Enabled))) { addresses = Array.Empty<IPAddress>(); return false; From c9dd58928b81f1bbcb59131c6e8ffbe5255b10ec Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Jul 2023 11:20:32 +0000 Subject: [PATCH 431/858] chore(deps): update github/codeql-action action to v2.20.2 --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index f83b38949c..eea2381891 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@f6e388ebf0efc915c6c5b165b019ee61a6746a38 # v2.20.1 + uses: github/codeql-action/init@004c5de30b6423267685b897a3d595e944f7fed5 # v2.20.2 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@f6e388ebf0efc915c6c5b165b019ee61a6746a38 # v2.20.1 + uses: github/codeql-action/autobuild@004c5de30b6423267685b897a3d595e944f7fed5 # v2.20.2 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@f6e388ebf0efc915c6c5b165b019ee61a6746a38 # v2.20.1 + uses: github/codeql-action/analyze@004c5de30b6423267685b897a3d595e944f7fed5 # v2.20.2 From 32ac3b580c547f8b1923e242d0efd4816470fb45 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Mon, 3 Jul 2023 14:03:33 +0200 Subject: [PATCH 432/858] Rename additional values in NetworkConfiguration and add migration for all changed values --- .../ApplicationHost.cs | 8 +- .../EntryPoints/ExternalPortForwarding.cs | 8 +- .../Configuration/NetworkConfiguration.cs | 42 ++-- Jellyfin.Server/Migrations/MigrationRunner.cs | 3 +- .../MigrateNetworkConfiguration.cs | 195 ++++++++++++++++++ 5 files changed, 226 insertions(+), 30 deletions(-) create mode 100644 Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 31f98a20ca..8dc54ecfb2 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -475,8 +475,8 @@ namespace Emby.Server.Implementations } var networkConfiguration = ConfigurationManager.GetNetworkConfiguration(); - HttpPort = networkConfiguration.HttpServerPortNumber; - HttpsPort = networkConfiguration.HttpsPortNumber; + HttpPort = networkConfiguration.ServerPortNumberHttp; + HttpsPort = networkConfiguration.ServerPortNumberHttps; // Safeguard against invalid configuration if (HttpPort == HttpsPort) @@ -785,8 +785,8 @@ namespace Emby.Server.Implementations if (HttpPort != 0 && HttpsPort != 0) { // Need to restart if ports have changed - if (networkConfiguration.HttpServerPortNumber != HttpPort - || networkConfiguration.HttpsPortNumber != HttpsPort) + if (networkConfiguration.ServerPortNumberHttp != HttpPort + || networkConfiguration.ServerPortNumberHttps != HttpsPort) { if (ConfigurationManager.Configuration.IsPortAuthorized) { diff --git a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs index 06e57ad127..6e23c5f46d 100644 --- a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs +++ b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs @@ -57,8 +57,8 @@ namespace Emby.Server.Implementations.EntryPoints return new StringBuilder(32) .Append(config.EnableUPnP).Append(Separator) - .Append(config.PublicPort).Append(Separator) - .Append(config.PublicHttpsPort).Append(Separator) + .Append(config.PublicPortHttp).Append(Separator) + .Append(config.PublicPortHttps).Append(Separator) .Append(_appHost.HttpPort).Append(Separator) .Append(_appHost.HttpsPort).Append(Separator) .Append(_appHost.ListenWithHttps).Append(Separator) @@ -146,11 +146,11 @@ namespace Emby.Server.Implementations.EntryPoints private IEnumerable<Task> CreatePortMaps(INatDevice device) { var config = _config.GetNetworkConfiguration(); - yield return CreatePortMap(device, _appHost.HttpPort, config.PublicPort); + yield return CreatePortMap(device, _appHost.HttpPort, config.PublicPortHttp); if (_appHost.ListenWithHttps) { - yield return CreatePortMap(device, _appHost.HttpsPort, config.PublicHttpsPort); + yield return CreatePortMap(device, _appHost.HttpsPort, config.PublicPortHttps); } } diff --git a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs index f31d2bce2d..90c7718ce2 100644 --- a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs +++ b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs @@ -10,12 +10,12 @@ namespace Jellyfin.Networking.Configuration public class NetworkConfiguration { /// <summary> - /// The default value for <see cref="HttpServerPortNumber"/>. + /// The default value for <see cref="ServerPortNumberHttp"/>. /// </summary> public const int DefaultHttpPort = 8096; /// <summary> - /// The default value for <see cref="PublicHttpsPort"/> and <see cref="HttpsPortNumber"/>. + /// The default value for <see cref="PublicPortHttps"/> and <see cref="ServerPortNumberHttps"/>. /// </summary> public const int DefaultHttpsPort = 8920; @@ -78,29 +78,29 @@ namespace Jellyfin.Networking.Configuration /// </summary> public string CertificatePassword { get; set; } = string.Empty; - /// <summary> - /// Gets or sets the HTTPS server port number. - /// </summary> - /// <value>The HTTPS server port number.</value> - public int HttpsPortNumber { get; set; } = DefaultHttpsPort; - - /// <summary> - /// Gets or sets the public HTTPS port. - /// </summary> - /// <value>The public HTTPS port.</value> - public int PublicHttpsPort { get; set; } = DefaultHttpsPort; - /// <summary> /// Gets or sets the HTTP server port number. /// </summary> /// <value>The HTTP server port number.</value> - public int HttpServerPortNumber { get; set; } = DefaultHttpPort; + public int ServerPortNumberHttp { get; set; } = DefaultHttpPort; + + /// <summary> + /// Gets or sets the HTTPS server port number. + /// </summary> + /// <value>The HTTPS server port number.</value> + public int ServerPortNumberHttps { get; set; } = DefaultHttpsPort; /// <summary> /// Gets or sets the public mapped port. /// </summary> /// <value>The public mapped port.</value> - public int PublicPort { get; set; } = DefaultHttpPort; + public int PublicPortHttp { get; set; } = DefaultHttpPort; + + /// <summary> + /// Gets or sets the public HTTPS port. + /// </summary> + /// <value>The public HTTPS port.</value> + public int PublicPortHttps { get; set; } = DefaultHttpsPort; /// <summary> /// Gets or sets a value indicating whether Autodiscovery is enabled. @@ -113,17 +113,17 @@ namespace Jellyfin.Networking.Configuration public bool EnableUPnP { get; set; } /// <summary> - /// Gets or sets a value indicating whether IPv6 is enabled or not. + /// Gets or sets a value indicating whether IPv6 is enabled. /// </summary> public bool EnableIPv4 { get; set; } = true; /// <summary> - /// Gets or sets a value indicating whether IPv6 is enabled or not. + /// Gets or sets a value indicating whether IPv6 is enabled. /// </summary> public bool EnableIPv6 { get; set; } /// <summary> - /// Gets or sets a value indicating whether access outside of the LAN is permitted. + /// Gets or sets a value indicating whether access from outside of the LAN is permitted. /// </summary> public bool EnableRemoteAccess { get; set; } = true; @@ -138,12 +138,12 @@ namespace Jellyfin.Networking.Configuration public string[] LocalNetworkAddresses { get; set; } = Array.Empty<string>(); /// <summary> - /// Gets or sets the known proxies. If the proxy is a network, it's added to the KnownNetworks. + /// Gets or sets the known proxies. /// </summary> public string[] KnownProxies { get; set; } = Array.Empty<string>(); /// <summary> - /// Gets or sets a value indicating whether address names that match <see cref="VirtualInterfaceNames"/> should be Ignore for the purposes of binding. + /// Gets or sets a value indicating whether address names that match <see cref="VirtualInterfaceNames"/> should be ignored for the purposes of binding. /// </summary> public bool IgnoreVirtualInterfaces { get; set; } = true; diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs index abfdcd77d5..33c02f41c6 100644 --- a/Jellyfin.Server/Migrations/MigrationRunner.cs +++ b/Jellyfin.Server/Migrations/MigrationRunner.cs @@ -22,7 +22,8 @@ namespace Jellyfin.Server.Migrations private static readonly Type[] _preStartupMigrationTypes = { typeof(PreStartupRoutines.CreateNetworkConfiguration), - typeof(PreStartupRoutines.MigrateMusicBrainzTimeout) + typeof(PreStartupRoutines.MigrateMusicBrainzTimeout), + typeof(PreStartupRoutines.MigrateNetworkConfiguration) }; /// <summary> diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs new file mode 100644 index 0000000000..afcf5436c2 --- /dev/null +++ b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs @@ -0,0 +1,195 @@ +using System; +using System.IO; +using System.Xml; +using System.Xml.Serialization; +using Emby.Server.Implementations; +using Jellyfin.Networking.Configuration; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Migrations.PreStartupRoutines; + +/// <inheritdoc /> +public class MigrateNetworkConfiguration : IMigrationRoutine +{ + private readonly ServerApplicationPaths _applicationPaths; + private readonly ILogger<MigrateNetworkConfiguration> _logger; + + /// <summary> + /// Initializes a new instance of the <see cref="MigrateNetworkConfiguration"/> class. + /// </summary> + /// <param name="applicationPaths">An instance of <see cref="ServerApplicationPaths"/>.</param> + /// <param name="loggerFactory">An instance of the <see cref="ILoggerFactory"/> interface.</param> + public MigrateNetworkConfiguration(ServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory) + { + _applicationPaths = applicationPaths; + _logger = loggerFactory.CreateLogger<MigrateNetworkConfiguration>(); + } + + /// <inheritdoc /> + public Guid Id => Guid.Parse("4FB5C950-1991-11EE-9B4B-0800200C9A66"); + + /// <inheritdoc /> + public string Name => nameof(MigrateNetworkConfiguration); + + /// <inheritdoc /> + public bool PerformOnNewInstall => false; + + /// <inheritdoc /> + public void Perform() + { + string path = Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "network.xml"); + var oldNetworkConfigSerializer = new XmlSerializer(typeof(OldNetworkConfiguration), new XmlRootAttribute("NetworkConfiguration")); + using var xmlReader = XmlReader.Create(path); + var oldNetworkConfiguration = (OldNetworkConfiguration?)oldNetworkConfigSerializer.Deserialize(xmlReader); + + if (oldNetworkConfiguration is not null) + { + // Migrate network config values to new config schema + var networkConfiguration = new NetworkConfiguration(); + networkConfiguration.AutoDiscovery = oldNetworkConfiguration.AutoDiscovery; + networkConfiguration.BaseUrl = oldNetworkConfiguration.BaseUrl; + networkConfiguration.CertificatePassword = oldNetworkConfiguration.CertificatePassword; + networkConfiguration.CertificatePath = oldNetworkConfiguration.CertificatePath; + networkConfiguration.EnableHttps = oldNetworkConfiguration.EnableHttps; + networkConfiguration.EnableIPv4 = oldNetworkConfiguration.EnableIPV4; + networkConfiguration.EnableIPv6 = oldNetworkConfiguration.EnableIPV6; + networkConfiguration.EnablePublishedServerUriByRequest = oldNetworkConfiguration.EnablePublishedServerUriByRequest; + networkConfiguration.EnableRemoteAccess = oldNetworkConfiguration.EnableRemoteAccess; + networkConfiguration.EnableUPnP = oldNetworkConfiguration.EnableUPnP; + networkConfiguration.IgnoreVirtualInterfaces = oldNetworkConfiguration.IgnoreVirtualInterfaces; + networkConfiguration.IsRemoteIPFilterBlacklist = oldNetworkConfiguration.IsRemoteIPFilterBlacklist; + networkConfiguration.KnownProxies = oldNetworkConfiguration.KnownProxies; + networkConfiguration.LocalNetworkAddresses = oldNetworkConfiguration.LocalNetworkAddresses; + networkConfiguration.LocalNetworkSubnets = oldNetworkConfiguration.LocalNetworkSubnets; + networkConfiguration.PublicPortHttp = oldNetworkConfiguration.PublicPort; + networkConfiguration.PublicPortHttps = oldNetworkConfiguration.PublicHttpsPort; + networkConfiguration.PublishedServerUriBySubnet = oldNetworkConfiguration.PublishedServerUriBySubnet; + networkConfiguration.RemoteIPFilter = oldNetworkConfiguration.RemoteIPFilter; + networkConfiguration.RequireHttps = oldNetworkConfiguration.RequireHttps; + networkConfiguration.ServerPortNumberHttp = oldNetworkConfiguration.HttpServerPortNumber; + networkConfiguration.ServerPortNumberHttps = oldNetworkConfiguration.HttpsPortNumber; + + // Migrate old virtual interface name schema + var oldVirtualInterfaceNames = oldNetworkConfiguration.VirtualInterfaceNames; + if (oldVirtualInterfaceNames.Equals("vEthernet*", StringComparison.OrdinalIgnoreCase)) + { + networkConfiguration.VirtualInterfaceNames = new string[] { "veth" }; + } + else + { + networkConfiguration.VirtualInterfaceNames = oldVirtualInterfaceNames.Replace("*", string.Empty, StringComparison.OrdinalIgnoreCase).Split(','); + } + + var networkConfigSerializer = new XmlSerializer(typeof(NetworkConfiguration)); + var xmlWriterSettings = new XmlWriterSettings { Indent = true }; + using var xmlWriter = XmlWriter.Create(path, xmlWriterSettings); + networkConfigSerializer.Serialize(xmlWriter, networkConfiguration); + } + } + +#pragma warning disable + public sealed class OldNetworkConfiguration + { + public const int DefaultHttpPort = 8096; + + public const int DefaultHttpsPort = 8920; + + private string _baseUrl = string.Empty; + + public bool RequireHttps { get; set; } + + public string CertificatePath { get; set; } = string.Empty; + + public string CertificatePassword { get; set; } = string.Empty; + + public string BaseUrl + { + get => _baseUrl; + set + { + // Normalize the start of the string + if (string.IsNullOrWhiteSpace(value)) + { + // If baseUrl is empty, set an empty prefix string + _baseUrl = string.Empty; + return; + } + + if (value[0] != '/') + { + // If baseUrl was not configured with a leading slash, append one for consistency + value = "/" + value; + } + + // Normalize the end of the string + if (value[^1] == '/') + { + // If baseUrl was configured with a trailing slash, remove it for consistency + value = value.Remove(value.Length - 1); + } + + _baseUrl = value; + } + } + + public int PublicHttpsPort { get; set; } = DefaultHttpsPort; + + public int HttpServerPortNumber { get; set; } = DefaultHttpPort; + + public int HttpsPortNumber { get; set; } = DefaultHttpsPort; + + public bool EnableHttps { get; set; } + + public int PublicPort { get; set; } = DefaultHttpPort; + + public bool UPnPCreateHttpPortMap { get; set; } + + public string UDPPortRange { get; set; } = string.Empty; + + public bool EnableIPV6 { get; set; } + + public bool EnableIPV4 { get; set; } = true; + + public bool EnableSSDPTracing { get; set; } + + public string SSDPTracingFilter { get; set; } = string.Empty; + + public int UDPSendCount { get; set; } = 2; + + public int UDPSendDelay { get; set; } = 100; + + public bool IgnoreVirtualInterfaces { get; set; } = true; + + public string VirtualInterfaceNames { get; set; } = "vEthernet*"; + + public int GatewayMonitorPeriod { get; set; } = 60; + + public bool EnableMultiSocketBinding { get; } = true; + + public bool TrustAllIP6Interfaces { get; set; } + + public string HDHomerunPortRange { get; set; } = string.Empty; + + public string[] PublishedServerUriBySubnet { get; set; } = Array.Empty<string>(); + + public bool AutoDiscoveryTracing { get; set; } + + public bool AutoDiscovery { get; set; } = true; + + public string[] RemoteIPFilter { get; set; } = Array.Empty<string>(); + + public bool IsRemoteIPFilterBlacklist { get; set; } + + public bool EnableUPnP { get; set; } + + public bool EnableRemoteAccess { get; set; } = true; + + public string[] LocalNetworkSubnets { get; set; } = Array.Empty<string>(); + + public string[] LocalNetworkAddresses { get; set; } = Array.Empty<string>(); + public string[] KnownProxies { get; set; } = Array.Empty<string>(); + + public bool EnablePublishedServerUriByRequest { get; set; } = false; + } +#pragma warning restore +} From e233a3b074d7696e4c05846aaf04434dafaf4031 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Mon, 3 Jul 2023 15:59:39 +0200 Subject: [PATCH 433/858] Apply review suggestions --- .../ApplicationHost.cs | 8 +++---- .../EntryPoints/ExternalPortForwarding.cs | 8 +++---- .../Configuration/NetworkConfiguration.cs | 24 +++++++++---------- .../MigrateNetworkConfiguration.cs | 8 +++---- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 8dc54ecfb2..dd90a89505 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -475,8 +475,8 @@ namespace Emby.Server.Implementations } var networkConfiguration = ConfigurationManager.GetNetworkConfiguration(); - HttpPort = networkConfiguration.ServerPortNumberHttp; - HttpsPort = networkConfiguration.ServerPortNumberHttps; + HttpPort = networkConfiguration.InternalHttpPort; + HttpsPort = networkConfiguration.InternalHttpsPort; // Safeguard against invalid configuration if (HttpPort == HttpsPort) @@ -785,8 +785,8 @@ namespace Emby.Server.Implementations if (HttpPort != 0 && HttpsPort != 0) { // Need to restart if ports have changed - if (networkConfiguration.ServerPortNumberHttp != HttpPort - || networkConfiguration.ServerPortNumberHttps != HttpsPort) + if (networkConfiguration.InternalHttpPort != HttpPort + || networkConfiguration.InternalHttpsPort != HttpsPort) { if (ConfigurationManager.Configuration.IsPortAuthorized) { diff --git a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs index 6e23c5f46d..d6da597b8b 100644 --- a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs +++ b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs @@ -57,8 +57,8 @@ namespace Emby.Server.Implementations.EntryPoints return new StringBuilder(32) .Append(config.EnableUPnP).Append(Separator) - .Append(config.PublicPortHttp).Append(Separator) - .Append(config.PublicPortHttps).Append(Separator) + .Append(config.PublicHttpPort).Append(Separator) + .Append(config.PublicHttpsPort).Append(Separator) .Append(_appHost.HttpPort).Append(Separator) .Append(_appHost.HttpsPort).Append(Separator) .Append(_appHost.ListenWithHttps).Append(Separator) @@ -146,11 +146,11 @@ namespace Emby.Server.Implementations.EntryPoints private IEnumerable<Task> CreatePortMaps(INatDevice device) { var config = _config.GetNetworkConfiguration(); - yield return CreatePortMap(device, _appHost.HttpPort, config.PublicPortHttp); + yield return CreatePortMap(device, _appHost.HttpPort, config.PublicHttpPort); if (_appHost.ListenWithHttps) { - yield return CreatePortMap(device, _appHost.HttpsPort, config.PublicPortHttps); + yield return CreatePortMap(device, _appHost.HttpsPort, config.PublicHttpsPort); } } diff --git a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs index 90c7718ce2..573c36be79 100644 --- a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs +++ b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs @@ -10,12 +10,12 @@ namespace Jellyfin.Networking.Configuration public class NetworkConfiguration { /// <summary> - /// The default value for <see cref="ServerPortNumberHttp"/>. + /// The default value for <see cref="InternalHttpPort"/>. /// </summary> public const int DefaultHttpPort = 8096; /// <summary> - /// The default value for <see cref="PublicPortHttps"/> and <see cref="ServerPortNumberHttps"/>. + /// The default value for <see cref="PublicHttpsPort"/> and <see cref="InternalHttpsPort"/>. /// </summary> public const int DefaultHttpsPort = 8920; @@ -79,28 +79,28 @@ namespace Jellyfin.Networking.Configuration public string CertificatePassword { get; set; } = string.Empty; /// <summary> - /// Gets or sets the HTTP server port number. + /// Gets or sets the internal HTTP server port. /// </summary> - /// <value>The HTTP server port number.</value> - public int ServerPortNumberHttp { get; set; } = DefaultHttpPort; + /// <value>The HTTP server port.</value> + public int InternalHttpPort { get; set; } = DefaultHttpPort; /// <summary> - /// Gets or sets the HTTPS server port number. + /// Gets or sets the internal HTTPS server port. /// </summary> - /// <value>The HTTPS server port number.</value> - public int ServerPortNumberHttps { get; set; } = DefaultHttpsPort; + /// <value>The HTTPS server port.</value> + public int InternalHttpsPort { get; set; } = DefaultHttpsPort; /// <summary> - /// Gets or sets the public mapped port. + /// Gets or sets the public HTTP port. /// </summary> - /// <value>The public mapped port.</value> - public int PublicPortHttp { get; set; } = DefaultHttpPort; + /// <value>The public HTTP port.</value> + public int PublicHttpPort { get; set; } = DefaultHttpPort; /// <summary> /// Gets or sets the public HTTPS port. /// </summary> /// <value>The public HTTPS port.</value> - public int PublicPortHttps { get; set; } = DefaultHttpsPort; + public int PublicHttpsPort { get; set; } = DefaultHttpsPort; /// <summary> /// Gets or sets a value indicating whether Autodiscovery is enabled. diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs index afcf5436c2..3b32e60437 100644 --- a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs +++ b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs @@ -57,17 +57,17 @@ public class MigrateNetworkConfiguration : IMigrationRoutine networkConfiguration.EnableRemoteAccess = oldNetworkConfiguration.EnableRemoteAccess; networkConfiguration.EnableUPnP = oldNetworkConfiguration.EnableUPnP; networkConfiguration.IgnoreVirtualInterfaces = oldNetworkConfiguration.IgnoreVirtualInterfaces; + networkConfiguration.InternalHttpPort = oldNetworkConfiguration.HttpServerPortNumber; + networkConfiguration.InternalHttpsPort = oldNetworkConfiguration.HttpsPortNumber; networkConfiguration.IsRemoteIPFilterBlacklist = oldNetworkConfiguration.IsRemoteIPFilterBlacklist; networkConfiguration.KnownProxies = oldNetworkConfiguration.KnownProxies; networkConfiguration.LocalNetworkAddresses = oldNetworkConfiguration.LocalNetworkAddresses; networkConfiguration.LocalNetworkSubnets = oldNetworkConfiguration.LocalNetworkSubnets; - networkConfiguration.PublicPortHttp = oldNetworkConfiguration.PublicPort; - networkConfiguration.PublicPortHttps = oldNetworkConfiguration.PublicHttpsPort; + networkConfiguration.PublicHttpPort = oldNetworkConfiguration.PublicPort; + networkConfiguration.PublicHttpsPort = oldNetworkConfiguration.PublicHttpsPort; networkConfiguration.PublishedServerUriBySubnet = oldNetworkConfiguration.PublishedServerUriBySubnet; networkConfiguration.RemoteIPFilter = oldNetworkConfiguration.RemoteIPFilter; networkConfiguration.RequireHttps = oldNetworkConfiguration.RequireHttps; - networkConfiguration.ServerPortNumberHttp = oldNetworkConfiguration.HttpServerPortNumber; - networkConfiguration.ServerPortNumberHttps = oldNetworkConfiguration.HttpsPortNumber; // Migrate old virtual interface name schema var oldVirtualInterfaceNames = oldNetworkConfiguration.VirtualInterfaceNames; From e56275fb46296781798a9af4a42fc3fb623c15cd Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Mon, 3 Jul 2023 21:51:36 +0200 Subject: [PATCH 434/858] Extract networking constants --- Emby.Dlna/Main/DlnaEntryPoint.cs | 1 + .../EntryPoints/UdpServerEntryPoint.cs | 1 + Jellyfin.Networking/Constants/Network.cs | 75 ++++ .../Extensions/NetworkExtensions.cs | 351 ++++++++++++++++++ Jellyfin.Networking/Manager/NetworkManager.cs | 36 +- .../ApiServiceCollectionExtensions.cs | 19 +- MediaBrowser.Common/Net/NetworkExtensions.cs | 350 ----------------- .../NetworkExtensionsTests.cs | 2 +- .../NetworkParseTests.cs | 2 +- 9 files changed, 459 insertions(+), 378 deletions(-) create mode 100644 Jellyfin.Networking/Constants/Network.cs create mode 100644 Jellyfin.Networking/Extensions/NetworkExtensions.cs delete mode 100644 MediaBrowser.Common/Net/NetworkExtensions.cs diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index f6ec9574b6..1a4bec398c 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using Emby.Dlna.PlayTo; using Emby.Dlna.Ssdp; using Jellyfin.Networking.Configuration; +using Jellyfin.Networking.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs index 2839e163e3..54191649da 100644 --- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs +++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using Emby.Server.Implementations.Udp; using Jellyfin.Networking.Configuration; +using Jellyfin.Networking.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller; diff --git a/Jellyfin.Networking/Constants/Network.cs b/Jellyfin.Networking/Constants/Network.cs new file mode 100644 index 0000000000..7fadc74bbc --- /dev/null +++ b/Jellyfin.Networking/Constants/Network.cs @@ -0,0 +1,75 @@ +using System.Net; +using Microsoft.AspNetCore.HttpOverrides; + +namespace Jellyfin.Networking.Constants; + +/// <summary> +/// Networking constants. +/// </summary> +public static class Network +{ + /// <summary> + /// IPv4 mask bytes. + /// </summary> + public const int IPv4MaskBytes = 4; + + /// <summary> + /// IPv6 mask bytes. + /// </summary> + public const int IPv6MaskBytes = 16; + + /// <summary> + /// Minimum IPv4 prefix size. + /// </summary> + public const int MinimumIPv4PrefixSize = 32; + + /// <summary> + /// Minimum IPv6 prefix size. + /// </summary> + public const int MinimumIPv6PrefixSize = 128; + + /// <summary> + /// Whole IPv4 address space. + /// </summary> + public static readonly IPNetwork IPv4Any = new IPNetwork(IPAddress.Any, 0); + + /// <summary> + /// Whole IPv6 address space. + /// </summary> + public static readonly IPNetwork IPv6Any = new IPNetwork(IPAddress.IPv6Any, 0); + + /// <summary> + /// IPv4 Loopback as defined in RFC 5735. + /// </summary> + public static readonly IPNetwork IPv4RFC5735Loopback = new IPNetwork(IPAddress.Loopback, 8); + + /// <summary> + /// IPv4 private class A as defined in RFC 1918. + /// </summary> + public static readonly IPNetwork IPv4RFC1918PrivateClassA = new IPNetwork(IPAddress.Parse("10.0.0.0"), 8); + + /// <summary> + /// IPv4 private class B as defined in RFC 1918. + /// </summary> + public static readonly IPNetwork IPv4RFC1918PrivateClassB = new IPNetwork(IPAddress.Parse("172.16.0.0"), 12); + + /// <summary> + /// IPv4 private class C as defined in RFC 1918. + /// </summary> + public static readonly IPNetwork IPv4RFC1918PrivateClassC = new IPNetwork(IPAddress.Parse("192.168.0.0"), 16); + + /// <summary> + /// IPv6 loopback as defined in RFC 4291. + /// </summary> + public static readonly IPNetwork IPv6RFC4291Loopback = new IPNetwork(IPAddress.IPv6Loopback, 128); + + /// <summary> + /// IPv6 site local as defined in RFC 4291. + /// </summary> + public static readonly IPNetwork IPv6RFC4291SiteLocal = new IPNetwork(IPAddress.Parse("fe80::"), 10); + + /// <summary> + /// IPv6 unique local as defined in RFC 4193. + /// </summary> + public static readonly IPNetwork IPv6RFC4193UniqueLocal = new IPNetwork(IPAddress.Parse("fc00::"), 7); +} diff --git a/Jellyfin.Networking/Extensions/NetworkExtensions.cs b/Jellyfin.Networking/Extensions/NetworkExtensions.cs new file mode 100644 index 0000000000..2ad6bae622 --- /dev/null +++ b/Jellyfin.Networking/Extensions/NetworkExtensions.cs @@ -0,0 +1,351 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text.RegularExpressions; +using Jellyfin.Extensions; +using Jellyfin.Networking.Constants; +using Microsoft.AspNetCore.HttpOverrides; + +namespace Jellyfin.Networking.Extensions; + +/// <summary> +/// Defines the <see cref="NetworkExtensions" />. +/// </summary> +public static partial class NetworkExtensions +{ + // Use regular expression as CheckHostName isn't RFC5892 compliant. + // Modified from gSkinner's expression at https://stackoverflow.com/questions/11809631/fully-qualified-domain-name-validation + [GeneratedRegex(@"(?im)^(?!:\/\/)(?=.{1,255}$)((.{1,63}\.){0,127}(?![0-9]*$)[a-z0-9-]+\.?)(:(\d){1,5}){0,1}$", RegexOptions.IgnoreCase, "en-US")] + private static partial Regex fqdnGeneratedRegex(); + + /// <summary> + /// Returns true if the IPAddress contains an IP6 Local link address. + /// </summary> + /// <param name="address">IPAddress object to check.</param> + /// <returns>True if it is a local link address.</returns> + /// <remarks> + /// See https://stackoverflow.com/questions/6459928/explain-the-instance-properties-of-system-net-ipaddress + /// it appears that the IPAddress.IsIPv6LinkLocal is out of date. + /// </remarks> + public static bool IsIPv6LinkLocal(IPAddress address) + { + ArgumentNullException.ThrowIfNull(address); + + if (address.IsIPv4MappedToIPv6) + { + address = address.MapToIPv4(); + } + + if (address.AddressFamily != AddressFamily.InterNetworkV6) + { + return false; + } + + // GetAddressBytes + Span<byte> octet = stackalloc byte[16]; + address.TryWriteBytes(octet, out _); + uint word = (uint)(octet[0] << 8) + octet[1]; + + return word >= 0xfe80 && word <= 0xfebf; // fe80::/10 :Local link. + } + + /// <summary> + /// Convert a subnet mask in CIDR notation to a dotted decimal string value. IPv4 only. + /// </summary> + /// <param name="cidr">Subnet mask in CIDR notation.</param> + /// <param name="family">IPv4 or IPv6 family.</param> + /// <returns>String value of the subnet mask in dotted decimal notation.</returns> + public static IPAddress CidrToMask(byte cidr, AddressFamily family) + { + uint addr = 0xFFFFFFFF << ((family == AddressFamily.InterNetwork ? Network.MinimumIPv4PrefixSize : Network.MinimumIPv6PrefixSize) - cidr); + addr = ((addr & 0xff000000) >> 24) + | ((addr & 0x00ff0000) >> 8) + | ((addr & 0x0000ff00) << 8) + | ((addr & 0x000000ff) << 24); + return new IPAddress(addr); + } + + /// <summary> + /// Convert a subnet mask in CIDR notation to a dotted decimal string value. IPv4 only. + /// </summary> + /// <param name="cidr">Subnet mask in CIDR notation.</param> + /// <param name="family">IPv4 or IPv6 family.</param> + /// <returns>String value of the subnet mask in dotted decimal notation.</returns> + public static IPAddress CidrToMask(int cidr, AddressFamily family) + { + uint addr = 0xFFFFFFFF << ((family == AddressFamily.InterNetwork ? Network.MinimumIPv4PrefixSize : Network.MinimumIPv6PrefixSize) - cidr); + addr = ((addr & 0xff000000) >> 24) + | ((addr & 0x00ff0000) >> 8) + | ((addr & 0x0000ff00) << 8) + | ((addr & 0x000000ff) << 24); + return new IPAddress(addr); + } + + /// <summary> + /// Convert a subnet mask to a CIDR. IPv4 only. + /// https://stackoverflow.com/questions/36954345/get-cidr-from-netmask. + /// </summary> + /// <param name="mask">Subnet mask.</param> + /// <returns>Byte CIDR representing the mask.</returns> + public static byte MaskToCidr(IPAddress mask) + { + ArgumentNullException.ThrowIfNull(mask); + + byte cidrnet = 0; + if (mask.Equals(IPAddress.Any)) + { + return cidrnet; + } + + // GetAddressBytes + Span<byte> bytes = stackalloc byte[mask.AddressFamily == AddressFamily.InterNetwork ? Network.IPv4MaskBytes : Network.IPv6MaskBytes]; + if (!mask.TryWriteBytes(bytes, out var bytesWritten)) + { + Console.WriteLine("Unable to write address bytes, only {bytesWritten} bytes written."); + } + + var zeroed = false; + for (var i = 0; i < bytes.Length; i++) + { + for (int v = bytes[i]; (v & 0xFF) != 0; v <<= 1) + { + if (zeroed) + { + // Invalid netmask. + return (byte)~cidrnet; + } + + if ((v & 0x80) == 0) + { + zeroed = true; + } + else + { + cidrnet++; + } + } + } + + return cidrnet; + } + + /// <summary> + /// Converts an IPAddress into a string. + /// IPv6 addresses are returned in [ ], with their scope removed. + /// </summary> + /// <param name="address">Address to convert.</param> + /// <returns>URI safe conversion of the address.</returns> + public static string FormatIPString(IPAddress? address) + { + if (address is null) + { + return string.Empty; + } + + var str = address.ToString(); + if (address.AddressFamily == AddressFamily.InterNetworkV6) + { + int i = str.IndexOf('%', StringComparison.Ordinal); + if (i != -1) + { + str = str.Substring(0, i); + } + + return $"[{str}]"; + } + + return str; + } + + /// <summary> + /// Try parsing an array of strings into <see cref="IPNetwork"/> objects, respecting exclusions. + /// Elements without a subnet mask will be represented as <see cref="IPNetwork"/> with a single IP. + /// </summary> + /// <param name="values">Input string array to be parsed.</param> + /// <param name="result">Collection of <see cref="IPNetwork"/>.</param> + /// <param name="negated">Boolean signaling if negated or not negated values should be parsed.</param> + /// <returns><c>True</c> if parsing was successful.</returns> + public static bool TryParseToSubnets(string[] values, [NotNullWhen(true)] out IReadOnlyList<IPNetwork>? result, bool negated = false) + { + if (values is null || values.Length == 0) + { + result = null; + return false; + } + + var tmpResult = new List<IPNetwork>(); + for (int a = 0; a < values.Length; a++) + { + if (TryParseToSubnet(values[a], out var innerResult, negated)) + { + tmpResult.Add(innerResult); + } + } + + result = tmpResult; + return tmpResult.Count > 0; + } + + /// <summary> + /// Try parsing a string into an <see cref="IPNetwork"/>, respecting exclusions. + /// Inputs without a subnet mask will be represented as <see cref="IPNetwork"/> with a single IP. + /// </summary> + /// <param name="value">Input string to be parsed.</param> + /// <param name="result">An <see cref="IPNetwork"/>.</param> + /// <param name="negated">Boolean signaling if negated or not negated values should be parsed.</param> + /// <returns><c>True</c> if parsing was successful.</returns> + public static bool TryParseToSubnet(ReadOnlySpan<char> value, [NotNullWhen(true)] out IPNetwork? result, bool negated = false) + { + var splitString = value.Trim().Split('/'); + if (splitString.MoveNext()) + { + var ipBlock = splitString.Current; + var address = IPAddress.None; + if (negated && ipBlock.StartsWith<char>("!") && IPAddress.TryParse(ipBlock[1..], out var tmpAddress)) + { + address = tmpAddress; + } + else if (!negated && IPAddress.TryParse(ipBlock, out tmpAddress)) + { + address = tmpAddress; + } + + if (address != IPAddress.None) + { + if (splitString.MoveNext()) + { + var subnetBlock = splitString.Current; + if (int.TryParse(subnetBlock, out var netmask)) + { + result = new IPNetwork(address, netmask); + return true; + } + else if (IPAddress.TryParse(subnetBlock, out var netmaskAddress)) + { + result = new IPNetwork(address, NetworkExtensions.MaskToCidr(netmaskAddress)); + return true; + } + } + else if (address.AddressFamily == AddressFamily.InterNetwork) + { + result = new IPNetwork(address, Network.MinimumIPv4PrefixSize); + return true; + } + else if (address.AddressFamily == AddressFamily.InterNetworkV6) + { + result = new IPNetwork(address, Network.MinimumIPv6PrefixSize); + return true; + } + } + } + + result = null; + return false; + } + + /// <summary> + /// Attempts to parse a host span. + /// </summary> + /// <param name="host">Host name to parse.</param> + /// <param name="addresses">Object representing the span, if it has successfully been parsed.</param> + /// <param name="isIPv4Enabled"><c>true</c> if IPv4 is enabled.</param> + /// <param name="isIPv6Enabled"><c>true</c> if IPv6 is enabled.</param> + /// <returns><c>true</c> if the parsing is successful, <c>false</c> if not.</returns> + public static bool TryParseHost(ReadOnlySpan<char> host, [NotNullWhen(true)] out IPAddress[]? addresses, bool isIPv4Enabled = true, bool isIPv6Enabled = false) + { + if (host.IsEmpty) + { + addresses = null; + return false; + } + + host = host.Trim(); + + // See if it's an IPv6 with port address e.g. [::1] or [::1]:120. + if (host[0] == '[') + { + int i = host.IndexOf(']'); + if (i != -1) + { + return TryParseHost(host[1..(i - 1)], out addresses); + } + + addresses = Array.Empty<IPAddress>(); + return false; + } + + var hosts = new List<string>(); + foreach (var splitSpan in host.Split(':')) + { + hosts.Add(splitSpan.ToString()); + } + + if (hosts.Count <= 2) + { + // Is hostname or hostname:port + if (fqdnGeneratedRegex().IsMatch(hosts[0])) + { + try + { + addresses = Dns.GetHostAddresses(hosts[0]); + return true; + } + catch (SocketException) + { + // Log and then ignore socket errors, as the result value will just be an empty array. + Console.WriteLine("GetHostAddresses failed."); + } + } + + // Is an IPv4 or IPv4:port + if (IPAddress.TryParse(hosts[0].AsSpan().LeftPart('/'), out var address)) + { + if (((address.AddressFamily == AddressFamily.InterNetwork) && (!isIPv4Enabled && isIPv6Enabled)) + || ((address.AddressFamily == AddressFamily.InterNetworkV6) && (isIPv4Enabled && !isIPv6Enabled))) + { + addresses = Array.Empty<IPAddress>(); + return false; + } + + addresses = new[] { address }; + + // Host name is an IPv4 address, so fake resolve. + return true; + } + } + else if (hosts.Count > 0 && hosts.Count <= 9) // 8 octets + port + { + if (IPAddress.TryParse(host.LeftPart('/'), out var address)) + { + addresses = new[] { address }; + return true; + } + } + + addresses = Array.Empty<IPAddress>(); + return false; + } + + /// <summary> + /// Gets the broadcast address for a <see cref="IPNetwork"/>. + /// </summary> + /// <param name="network">The <see cref="IPNetwork"/>.</param> + /// <returns>The broadcast address.</returns> + public static IPAddress GetBroadcastAddress(IPNetwork network) + { + var addressBytes = network.Prefix.GetAddressBytes(); + if (BitConverter.IsLittleEndian) + { + addressBytes = addressBytes.Reverse().ToArray(); + } + + uint iPAddress = BitConverter.ToUInt32(addressBytes, 0); + uint ipMaskV4 = BitConverter.ToUInt32(CidrToMask(network.PrefixLength, AddressFamily.InterNetwork).GetAddressBytes(), 0); + uint broadCastIPAddress = iPAddress | ~ipMaskV4; + + return new IPAddress(BitConverter.GetBytes(broadCastIPAddress)); + } +} diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index c80038e7d0..f20e285263 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -8,6 +8,8 @@ using System.Net.NetworkInformation; using System.Net.Sockets; using System.Threading; using Jellyfin.Networking.Configuration; +using Jellyfin.Networking.Constants; +using Jellyfin.Networking.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Model.Net; @@ -316,17 +318,17 @@ namespace Jellyfin.Networking.Manager var fallbackLanSubnets = new List<IPNetwork>(); if (IsIPv6Enabled) { - fallbackLanSubnets.Add(new IPNetwork(IPAddress.IPv6Loopback, 128)); // RFC 4291 (Loopback) - fallbackLanSubnets.Add(new IPNetwork(IPAddress.Parse("fe80::"), 10)); // RFC 4291 (Site local) - fallbackLanSubnets.Add(new IPNetwork(IPAddress.Parse("fc00::"), 7)); // RFC 4193 (Unique local) + fallbackLanSubnets.Add(Network.IPv6RFC4291Loopback); // RFC 4291 (Loopback) + fallbackLanSubnets.Add(Network.IPv6RFC4291SiteLocal); // RFC 4291 (Site local) + fallbackLanSubnets.Add(Network.IPv6RFC4193UniqueLocal); // RFC 4193 (Unique local) } if (IsIPv4Enabled) { - fallbackLanSubnets.Add(new IPNetwork(IPAddress.Loopback, 8)); // RFC 5735 (Loopback) - fallbackLanSubnets.Add(new IPNetwork(IPAddress.Parse("10.0.0.0"), 8)); // RFC 1918 (private) - fallbackLanSubnets.Add(new IPNetwork(IPAddress.Parse("172.16.0.0"), 12)); // RFC 1918 (private) - fallbackLanSubnets.Add(new IPNetwork(IPAddress.Parse("192.168.0.0"), 16)); // RFC 1918 (private) + fallbackLanSubnets.Add(Network.IPv4RFC5735Loopback); // RFC 5735 (Loopback) + fallbackLanSubnets.Add(Network.IPv4RFC1918PrivateClassA); // RFC 1918 (private Class A) + fallbackLanSubnets.Add(Network.IPv4RFC1918PrivateClassB); // RFC 1918 (private Class B) + fallbackLanSubnets.Add(Network.IPv4RFC1918PrivateClassC); // RFC 1918 (private Class C) } _lanSubnets = fallbackLanSubnets; @@ -369,12 +371,12 @@ namespace Jellyfin.Networking.Manager if (bindAddresses.Contains(IPAddress.Loopback)) { - interfaces.Add(new IPData(IPAddress.Loopback, new IPNetwork(IPAddress.Loopback, 8), "lo")); + interfaces.Add(new IPData(IPAddress.Loopback, Network.IPv4RFC5735Loopback, "lo")); } if (bindAddresses.Contains(IPAddress.IPv6Loopback)) { - interfaces.Add(new IPData(IPAddress.IPv6Loopback, new IPNetwork(IPAddress.IPv6Loopback, 128), "lo")); + interfaces.Add(new IPData(IPAddress.IPv6Loopback, Network.IPv6RFC4291Loopback, "lo")); } } @@ -437,7 +439,7 @@ namespace Jellyfin.Networking.Manager { if (IPAddress.TryParse(ip, out var ipp)) { - remoteAddressFilter.Add(new IPNetwork(ipp, ipp.AddressFamily == AddressFamily.InterNetwork ? 32 : 128)); + remoteAddressFilter.Add(new IPNetwork(ipp, ipp.AddressFamily == AddressFamily.InterNetwork ? Network.MinimumIPv4PrefixSize : Network.MinimumIPv6PrefixSize)); } } @@ -477,8 +479,8 @@ namespace Jellyfin.Networking.Manager } else if (string.Equals(identifier, "external", StringComparison.OrdinalIgnoreCase)) { - publishedServerUrls[new IPData(IPAddress.Any, new IPNetwork(IPAddress.Any, 0))] = replacement; - publishedServerUrls[new IPData(IPAddress.IPv6Any, new IPNetwork(IPAddress.IPv6Any, 0))] = replacement; + publishedServerUrls[new IPData(IPAddress.Any, Network.IPv4Any)] = replacement; + publishedServerUrls[new IPData(IPAddress.IPv6Any, Network.IPv6Any)] = replacement; } else if (string.Equals(identifier, "internal", StringComparison.OrdinalIgnoreCase)) { @@ -656,12 +658,12 @@ namespace Jellyfin.Networking.Manager var loopbackNetworks = new List<IPData>(); if (IsIPv4Enabled) { - loopbackNetworks.Add(new IPData(IPAddress.Loopback, new IPNetwork(IPAddress.Loopback, 8), "lo")); + loopbackNetworks.Add(new IPData(IPAddress.Loopback, Network.IPv4RFC5735Loopback, "lo")); } if (IsIPv6Enabled) { - loopbackNetworks.Add(new IPData(IPAddress.IPv6Loopback, new IPNetwork(IPAddress.IPv6Loopback, 128), "lo")); + loopbackNetworks.Add(new IPData(IPAddress.IPv6Loopback, Network.IPv6RFC4291Loopback, "lo")); } return loopbackNetworks; @@ -687,11 +689,11 @@ namespace Jellyfin.Networking.Manager if (IsIPv4Enabled && IsIPv6Enabled) { // Kestrel source code shows it uses Sockets.DualMode - so this also covers IPAddress.Any by default - result.Add(new IPData(IPAddress.IPv6Any, new IPNetwork(IPAddress.IPv6Any, 0))); + result.Add(new IPData(IPAddress.IPv6Any, Network.IPv6Any)); } else if (IsIPv4Enabled) { - result.Add(new IPData(IPAddress.Any, new IPNetwork(IPAddress.Any, 0))); + result.Add(new IPData(IPAddress.Any, Network.IPv4Any)); } else if (IsIPv6Enabled) { @@ -1047,7 +1049,7 @@ namespace Jellyfin.Networking.Manager } // Fallback to first external interface. - result = NetworkExtensions.FormatIPString(extResult.First().Address); + result = NetworkExtensions.FormatIPString(extResult[0].Address); _logger.LogDebug("{Source}: Using first external interface as bind address: {Result}", source, result); return true; } diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index ea3c92011f..e1dfa1d315 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -21,9 +21,10 @@ using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; using Jellyfin.Extensions.Json; using Jellyfin.Networking.Configuration; +using Jellyfin.Networking.Constants; +using Jellyfin.Networking.Extensions; using Jellyfin.Server.Configuration; using Jellyfin.Server.Filters; -using MediaBrowser.Common.Net; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Session; using Microsoft.AspNetCore.Authentication; @@ -271,7 +272,7 @@ namespace Jellyfin.Server.Extensions { if (IPAddress.TryParse(allowedProxies[i], out var addr)) { - AddIPAddress(config, options, addr, addr.AddressFamily == AddressFamily.InterNetwork ? 32 : 128); + AddIPAddress(config, options, addr, addr.AddressFamily == AddressFamily.InterNetwork ? Network.MinimumIPv4PrefixSize : Network.MinimumIPv6PrefixSize); } else if (NetworkExtensions.TryParseToSubnet(allowedProxies[i], out var subnet)) { @@ -284,7 +285,7 @@ namespace Jellyfin.Server.Extensions { foreach (var address in addresses) { - AddIPAddress(config, options, address, address.AddressFamily == AddressFamily.InterNetwork ? 32 : 128); + AddIPAddress(config, options, address, address.AddressFamily == AddressFamily.InterNetwork ? Network.MinimumIPv4PrefixSize : Network.MinimumIPv6PrefixSize); } } } @@ -292,17 +293,17 @@ namespace Jellyfin.Server.Extensions private static void AddIPAddress(NetworkConfiguration config, ForwardedHeadersOptions options, IPAddress addr, int prefixLength) { - if ((!config.EnableIPv4 && addr.AddressFamily == AddressFamily.InterNetwork) || (!config.EnableIPv6 && addr.AddressFamily == AddressFamily.InterNetworkV6)) - { - return; - } - if (addr.IsIPv4MappedToIPv6) { addr = addr.MapToIPv4(); } - if (prefixLength == 32) + if ((!config.EnableIPv4 && addr.AddressFamily == AddressFamily.InterNetwork) || (!config.EnableIPv6 && addr.AddressFamily == AddressFamily.InterNetworkV6)) + { + return; + } + + if (prefixLength == Network.MinimumIPv4PrefixSize) { options.KnownProxies.Add(addr); } diff --git a/MediaBrowser.Common/Net/NetworkExtensions.cs b/MediaBrowser.Common/Net/NetworkExtensions.cs deleted file mode 100644 index 47475b3da9..0000000000 --- a/MediaBrowser.Common/Net/NetworkExtensions.cs +++ /dev/null @@ -1,350 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Net; -using System.Net.Sockets; -using System.Text.RegularExpressions; -using Jellyfin.Extensions; -using Microsoft.AspNetCore.HttpOverrides; - -namespace MediaBrowser.Common.Net -{ - /// <summary> - /// Defines the <see cref="NetworkExtensions" />. - /// </summary> - public static class NetworkExtensions - { - // Use regular expression as CheckHostName isn't RFC5892 compliant. - // Modified from gSkinner's expression at https://stackoverflow.com/questions/11809631/fully-qualified-domain-name-validation - private static readonly Regex _fqdnRegex = new Regex(@"(?im)^(?!:\/\/)(?=.{1,255}$)((.{1,63}\.){0,127}(?![0-9]*$)[a-z0-9-]+\.?)(:(\d){1,5}){0,1}$"); - - /// <summary> - /// Returns true if the IPAddress contains an IP6 Local link address. - /// </summary> - /// <param name="address">IPAddress object to check.</param> - /// <returns>True if it is a local link address.</returns> - /// <remarks> - /// See https://stackoverflow.com/questions/6459928/explain-the-instance-properties-of-system-net-ipaddress - /// it appears that the IPAddress.IsIPv6LinkLocal is out of date. - /// </remarks> - public static bool IsIPv6LinkLocal(IPAddress address) - { - ArgumentNullException.ThrowIfNull(address); - - if (address.IsIPv4MappedToIPv6) - { - address = address.MapToIPv4(); - } - - if (address.AddressFamily != AddressFamily.InterNetworkV6) - { - return false; - } - - // GetAddressBytes - Span<byte> octet = stackalloc byte[16]; - address.TryWriteBytes(octet, out _); - uint word = (uint)(octet[0] << 8) + octet[1]; - - return word >= 0xfe80 && word <= 0xfebf; // fe80::/10 :Local link. - } - - /// <summary> - /// Convert a subnet mask in CIDR notation to a dotted decimal string value. IPv4 only. - /// </summary> - /// <param name="cidr">Subnet mask in CIDR notation.</param> - /// <param name="family">IPv4 or IPv6 family.</param> - /// <returns>String value of the subnet mask in dotted decimal notation.</returns> - public static IPAddress CidrToMask(byte cidr, AddressFamily family) - { - uint addr = 0xFFFFFFFF << ((family == AddressFamily.InterNetwork ? 32 : 128) - cidr); - addr = ((addr & 0xff000000) >> 24) - | ((addr & 0x00ff0000) >> 8) - | ((addr & 0x0000ff00) << 8) - | ((addr & 0x000000ff) << 24); - return new IPAddress(addr); - } - - /// <summary> - /// Convert a subnet mask in CIDR notation to a dotted decimal string value. IPv4 only. - /// </summary> - /// <param name="cidr">Subnet mask in CIDR notation.</param> - /// <param name="family">IPv4 or IPv6 family.</param> - /// <returns>String value of the subnet mask in dotted decimal notation.</returns> - public static IPAddress CidrToMask(int cidr, AddressFamily family) - { - uint addr = 0xFFFFFFFF << ((family == AddressFamily.InterNetwork ? 32 : 128) - cidr); - addr = ((addr & 0xff000000) >> 24) - | ((addr & 0x00ff0000) >> 8) - | ((addr & 0x0000ff00) << 8) - | ((addr & 0x000000ff) << 24); - return new IPAddress(addr); - } - - /// <summary> - /// Convert a subnet mask to a CIDR. IPv4 only. - /// https://stackoverflow.com/questions/36954345/get-cidr-from-netmask. - /// </summary> - /// <param name="mask">Subnet mask.</param> - /// <returns>Byte CIDR representing the mask.</returns> - public static byte MaskToCidr(IPAddress mask) - { - ArgumentNullException.ThrowIfNull(mask); - - byte cidrnet = 0; - if (mask.Equals(IPAddress.Any)) - { - return cidrnet; - } - - // GetAddressBytes - Span<byte> bytes = stackalloc byte[mask.AddressFamily == AddressFamily.InterNetwork ? 4 : 16]; - if (!mask.TryWriteBytes(bytes, out var bytesWritten)) - { - Console.WriteLine("Unable to write address bytes, only {Bytes} bytes written.", bytesWritten); - } - - var zeroed = false; - for (var i = 0; i < bytes.Length; i++) - { - for (int v = bytes[i]; (v & 0xFF) != 0; v <<= 1) - { - if (zeroed) - { - // Invalid netmask. - return (byte)~cidrnet; - } - - if ((v & 0x80) == 0) - { - zeroed = true; - } - else - { - cidrnet++; - } - } - } - - return cidrnet; - } - - /// <summary> - /// Converts an IPAddress into a string. - /// IPv6 addresses are returned in [ ], with their scope removed. - /// </summary> - /// <param name="address">Address to convert.</param> - /// <returns>URI safe conversion of the address.</returns> - public static string FormatIPString(IPAddress? address) - { - if (address is null) - { - return string.Empty; - } - - var str = address.ToString(); - if (address.AddressFamily == AddressFamily.InterNetworkV6) - { - int i = str.IndexOf('%', StringComparison.Ordinal); - if (i != -1) - { - str = str.Substring(0, i); - } - - return $"[{str}]"; - } - - return str; - } - - /// <summary> - /// Try parsing an array of strings into <see cref="IPNetwork"/> objects, respecting exclusions. - /// Elements without a subnet mask will be represented as <see cref="IPNetwork"/> with a single IP. - /// </summary> - /// <param name="values">Input string array to be parsed.</param> - /// <param name="result">Collection of <see cref="IPNetwork"/>.</param> - /// <param name="negated">Boolean signaling if negated or not negated values should be parsed.</param> - /// <returns><c>True</c> if parsing was successful.</returns> - public static bool TryParseToSubnets(string[] values, [NotNullWhen(true)] out IReadOnlyList<IPNetwork>? result, bool negated = false) - { - if (values is null || values.Length == 0) - { - result = null; - return false; - } - - var tmpResult = new List<IPNetwork>(); - for (int a = 0; a < values.Length; a++) - { - if (TryParseToSubnet(values[a], out var innerResult, negated)) - { - tmpResult.Add(innerResult); - } - } - - result = tmpResult; - return tmpResult.Count > 0; - } - - /// <summary> - /// Try parsing a string into an <see cref="IPNetwork"/>, respecting exclusions. - /// Inputs without a subnet mask will be represented as <see cref="IPNetwork"/> with a single IP. - /// </summary> - /// <param name="value">Input string to be parsed.</param> - /// <param name="result">An <see cref="IPNetwork"/>.</param> - /// <param name="negated">Boolean signaling if negated or not negated values should be parsed.</param> - /// <returns><c>True</c> if parsing was successful.</returns> - public static bool TryParseToSubnet(ReadOnlySpan<char> value, [NotNullWhen(true)] out IPNetwork? result, bool negated = false) - { - var splitString = value.Trim().Split('/'); - if (splitString.MoveNext()) - { - var ipBlock = splitString.Current; - var address = IPAddress.None; - if (negated && ipBlock.StartsWith("!") && IPAddress.TryParse(ipBlock[1..], out var tmpAddress)) - { - address = tmpAddress; - } - else if (!negated && IPAddress.TryParse(ipBlock, out tmpAddress)) - { - address = tmpAddress; - } - - if (address != IPAddress.None) - { - if (splitString.MoveNext()) - { - var subnetBlock = splitString.Current; - if (int.TryParse(subnetBlock, out var netmask)) - { - result = new IPNetwork(address, netmask); - return true; - } - else if (IPAddress.TryParse(subnetBlock, out var netmaskAddress)) - { - result = new IPNetwork(address, NetworkExtensions.MaskToCidr(netmaskAddress)); - return true; - } - } - else if (address.AddressFamily == AddressFamily.InterNetwork) - { - result = new IPNetwork(address, 32); - return true; - } - else if (address.AddressFamily == AddressFamily.InterNetworkV6) - { - result = new IPNetwork(address, 128); - return true; - } - } - } - - result = null; - return false; - } - - /// <summary> - /// Attempts to parse a host span. - /// </summary> - /// <param name="host">Host name to parse.</param> - /// <param name="addresses">Object representing the span, if it has successfully been parsed.</param> - /// <param name="isIPv4Enabled"><c>true</c> if IPv4 is enabled.</param> - /// <param name="isIPv6Enabled"><c>true</c> if IPv6 is enabled.</param> - /// <returns><c>true</c> if the parsing is successful, <c>false</c> if not.</returns> - public static bool TryParseHost(ReadOnlySpan<char> host, [NotNullWhen(true)] out IPAddress[]? addresses, bool isIPv4Enabled = true, bool isIPv6Enabled = false) - { - if (host.IsEmpty) - { - addresses = null; - return false; - } - - host = host.Trim(); - - // See if it's an IPv6 with port address e.g. [::1] or [::1]:120. - if (host[0] == '[') - { - int i = host.IndexOf("]", StringComparison.Ordinal); - if (i != -1) - { - return TryParseHost(host[1..(i - 1)], out addresses); - } - - addresses = Array.Empty<IPAddress>(); - return false; - } - - var hosts = new List<string>(); - foreach (var splitSpan in host.Split(':')) - { - hosts.Add(splitSpan.ToString()); - } - - if (hosts.Count <= 2) - { - // Is hostname or hostname:port - if (_fqdnRegex.IsMatch(hosts[0])) - { - try - { - addresses = Dns.GetHostAddresses(hosts[0]); - return true; - } - catch (SocketException) - { - // Log and then ignore socket errors, as the result value will just be an empty array. - Console.WriteLine("GetHostAddresses failed."); - } - } - - // Is an IP4 or IP4:port - if (IPAddress.TryParse(hosts[0].AsSpan().LeftPart('/'), out var address)) - { - if (((address.AddressFamily == AddressFamily.InterNetwork) && (!isIPv4Enabled && isIPv6Enabled)) - || ((address.AddressFamily == AddressFamily.InterNetworkV6) && (isIPv4Enabled && !isIPv6Enabled))) - { - addresses = Array.Empty<IPAddress>(); - return false; - } - - addresses = new[] { address }; - - // Host name is an ip4 address, so fake resolve. - return true; - } - } - else if (hosts.Count > 0 && hosts.Count <= 9) // 8 octets + port - { - if (IPAddress.TryParse(host.LeftPart('/'), out var address)) - { - addresses = new[] { address }; - return true; - } - } - - addresses = Array.Empty<IPAddress>(); - return false; - } - - /// <summary> - /// Gets the broadcast address for a <see cref="IPNetwork"/>. - /// </summary> - /// <param name="network">The <see cref="IPNetwork"/>.</param> - /// <returns>The broadcast address.</returns> - public static IPAddress GetBroadcastAddress(IPNetwork network) - { - var addressBytes = network.Prefix.GetAddressBytes(); - if (BitConverter.IsLittleEndian) - { - addressBytes.Reverse(); - } - - uint iPAddress = BitConverter.ToUInt32(addressBytes, 0); - uint ipMaskV4 = BitConverter.ToUInt32(CidrToMask(network.PrefixLength, AddressFamily.InterNetwork).GetAddressBytes(), 0); - uint broadCastIPAddress = iPAddress | ~ipMaskV4; - - return new IPAddress(BitConverter.GetBytes(broadCastIPAddress)); - } - } -} diff --git a/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs b/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs index c81fdefe95..2ff7c7de4f 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs @@ -1,6 +1,6 @@ using FsCheck; using FsCheck.Xunit; -using MediaBrowser.Common.Net; +using Jellyfin.Networking.Extensions; using Xunit; namespace Jellyfin.Networking.Tests diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs index 77f18c5445..cb8092ae9a 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs @@ -3,9 +3,9 @@ using System.Collections.Generic; using System.Linq; using System.Net; using Jellyfin.Networking.Configuration; +using Jellyfin.Networking.Extensions; using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Net; using MediaBrowser.Model.Net; using Microsoft.Extensions.Logging.Abstractions; using Moq; From 3ab82353587a3479c0109592faeba7a8283d53f3 Mon Sep 17 00:00:00 2001 From: Oskari Lavinto <olavinto@protonmail.com> Date: Mon, 3 Jul 2023 04:36:03 +0000 Subject: [PATCH 435/858] Translated using Weblate (Finnish) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fi/ --- .../Localization/Core/fi.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json index 8672cfb9ff..08344abeb7 100644 --- a/Emby.Server.Implementations/Localization/Core/fi.json +++ b/Emby.Server.Implementations/Localization/Core/fi.json @@ -74,16 +74,16 @@ "Shows": "Sarjat", "ServerNameNeedsToBeRestarted": "\"{0}\" on käynnistettävä uudelleen", "ProviderValue": "Lähde: {0}", - "Plugin": "Laajennus", + "Plugin": "Lisäosa", "NotificationOptionVideoPlaybackStopped": "Videon toisto lopetettu", "NotificationOptionVideoPlayback": "Videon toisto aloitettu", "NotificationOptionUserLockedOut": "Käyttäjä on lukittu", "NotificationOptionTaskFailed": "Ajoitettu tehtävä epäonnistui", "NotificationOptionServerRestartRequired": "Tarvitaan palvelimen uudelleenkäynnistys", - "NotificationOptionPluginUpdateInstalled": "Laajennus on päivitetty", - "NotificationOptionPluginUninstalled": "Laajennus on poistettu", - "NotificationOptionPluginInstalled": "Laajennus on asennettu", - "NotificationOptionPluginError": "Laajennuksen virhe", + "NotificationOptionPluginUpdateInstalled": "Lisäosa päivitettiin", + "NotificationOptionPluginUninstalled": "Lisäosa poistettiin", + "NotificationOptionPluginInstalled": "Lisäosa asennettiin", + "NotificationOptionPluginError": "Lisäosan virhe", "NotificationOptionNewLibraryContent": "Sisältöä on lisätty", "NotificationOptionInstallationFailed": "Asennus epäonnistui", "NotificationOptionCameraImageUploaded": "Kameran kuva on tallennettu", @@ -98,8 +98,8 @@ "TaskRefreshChannels": "Päivitä kanavat", "TaskCleanTranscodeDescription": "Poistaa päivää vanhemmat transkoodaustiedostot.", "TaskCleanTranscode": "Puhdista transkoodauskansio", - "TaskUpdatePluginsDescription": "Lataa ja asentaa päivitykset laajennuksille, jotka on määritetty päivittymään automaattisesti.", - "TaskUpdatePlugins": "Päivitä laajennukset", + "TaskUpdatePluginsDescription": "Lataa ja asentaa päivitykset lisäosille, jotka on määritetty päivittymään automaattisesti.", + "TaskUpdatePlugins": "Päivitä lisäosat", "TaskRefreshPeopleDescription": "Päivittää mediakirjaston näyttelijöiden ja ohjaajien metatiedot.", "TaskRefreshPeople": "Päivitä henkilöt", "TaskCleanLogsDescription": "Poistaa {0} päivää vanhemmat lokitiedostot.", From 2bc2848b8ecbcdd37077cefb54bf9d3ecfc4da11 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Tue, 4 Jul 2023 14:35:51 +0200 Subject: [PATCH 436/858] Apply review suggestions --- .../Extensions/NetworkExtensions.cs | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/Jellyfin.Networking/Extensions/NetworkExtensions.cs b/Jellyfin.Networking/Extensions/NetworkExtensions.cs index 2ad6bae622..d55f78135b 100644 --- a/Jellyfin.Networking/Extensions/NetworkExtensions.cs +++ b/Jellyfin.Networking/Extensions/NetworkExtensions.cs @@ -19,7 +19,7 @@ public static partial class NetworkExtensions // Use regular expression as CheckHostName isn't RFC5892 compliant. // Modified from gSkinner's expression at https://stackoverflow.com/questions/11809631/fully-qualified-domain-name-validation [GeneratedRegex(@"(?im)^(?!:\/\/)(?=.{1,255}$)((.{1,63}\.){0,127}(?![0-9]*$)[a-z0-9-]+\.?)(:(\d){1,5}){0,1}$", RegexOptions.IgnoreCase, "en-US")] - private static partial Regex fqdnGeneratedRegex(); + private static partial Regex FqdnGeneratedRegex(); /// <summary> /// Returns true if the IPAddress contains an IP6 Local link address. @@ -256,14 +256,13 @@ public static partial class NetworkExtensions /// <returns><c>true</c> if the parsing is successful, <c>false</c> if not.</returns> public static bool TryParseHost(ReadOnlySpan<char> host, [NotNullWhen(true)] out IPAddress[]? addresses, bool isIPv4Enabled = true, bool isIPv6Enabled = false) { + host = host.Trim(); if (host.IsEmpty) { addresses = null; return false; } - host = host.Trim(); - // See if it's an IPv6 with port address e.g. [::1] or [::1]:120. if (host[0] == '[') { @@ -286,7 +285,7 @@ public static partial class NetworkExtensions if (hosts.Count <= 2) { // Is hostname or hostname:port - if (fqdnGeneratedRegex().IsMatch(hosts[0])) + if (FqdnGeneratedRegex().IsMatch(hosts[0])) { try { @@ -295,8 +294,7 @@ public static partial class NetworkExtensions } catch (SocketException) { - // Log and then ignore socket errors, as the result value will just be an empty array. - Console.WriteLine("GetHostAddresses failed."); + // Ignore socket errors, as the result value will just be an empty array. } } @@ -337,14 +335,9 @@ public static partial class NetworkExtensions public static IPAddress GetBroadcastAddress(IPNetwork network) { var addressBytes = network.Prefix.GetAddressBytes(); - if (BitConverter.IsLittleEndian) - { - addressBytes = addressBytes.Reverse().ToArray(); - } - - uint iPAddress = BitConverter.ToUInt32(addressBytes, 0); + uint ipAddress = BitConverter.ToUInt32(addressBytes, 0); uint ipMaskV4 = BitConverter.ToUInt32(CidrToMask(network.PrefixLength, AddressFamily.InterNetwork).GetAddressBytes(), 0); - uint broadCastIPAddress = iPAddress | ~ipMaskV4; + uint broadCastIPAddress = ipAddress | ~ipMaskV4; return new IPAddress(BitConverter.GetBytes(broadCastIPAddress)); } From fd56296270e52014f25ef1ec482f00fa2e2206d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Mesk=C3=B3?= <meskobalazs@mailbox.org> Date: Wed, 5 Jul 2023 13:32:39 +0000 Subject: [PATCH 437/858] Translated using Weblate (Hungarian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hu/ --- .../Localization/Core/hu.json | 82 +++++++++---------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json index 62d48cebd8..5a4a02d80d 100644 --- a/Emby.Server.Implementations/Localization/Core/hu.json +++ b/Emby.Server.Implementations/Localization/Core/hu.json @@ -1,11 +1,11 @@ { "Albums": "Albumok", - "AppDeviceValues": "Program: {0}, Eszköz: {1}", + "AppDeviceValues": "Program: {0}, eszköz: {1}", "Application": "Alkalmazás", "Artists": "Előadók", - "AuthenticationSucceededWithUserName": "{0} sikeresen azonosítva", + "AuthenticationSucceededWithUserName": "{0} sikeresen hitelesítve", "Books": "Könyvek", - "CameraImageUploadedFrom": "Új kamerakép került feltöltésre innen: {0}", + "CameraImageUploadedFrom": "Új kamerakép feltöltve innen: {0}", "Channels": "Csatornák", "ChapterNameValue": "{0}. jelenet", "Collections": "Gyűjtemények", @@ -15,13 +15,13 @@ "Favorites": "Kedvencek", "Folders": "Könyvtárak", "Genres": "Műfajok", - "HeaderAlbumArtists": "Album előadó(k)", + "HeaderAlbumArtists": "Albumelőadók", "HeaderContinueWatching": "Megtekintés folytatása", "HeaderFavoriteAlbums": "Kedvenc albumok", "HeaderFavoriteArtists": "Kedvenc előadók", "HeaderFavoriteEpisodes": "Kedvenc epizódok", "HeaderFavoriteShows": "Kedvenc sorozatok", - "HeaderFavoriteSongs": "Kedvenc dalok", + "HeaderFavoriteSongs": "Kedvenc számok", "HeaderLiveTV": "Élő TV", "HeaderNextUp": "Következik", "HeaderRecordingGroups": "Felvételi csoportok", @@ -29,37 +29,37 @@ "Inherit": "Örökölt", "ItemAddedWithName": "{0} hozzáadva a könyvtárhoz", "ItemRemovedWithName": "{0} eltávolítva a könyvtárból", - "LabelIpAddressValue": "IP cím: {0}", - "LabelRunningTimeValue": "Futási idő: {0}", + "LabelIpAddressValue": "IP-cím: {0}", + "LabelRunningTimeValue": "Lejátszási idő: {0}", "Latest": "Legújabb", - "MessageApplicationUpdated": "Jellyfin Szerver frissítve", - "MessageApplicationUpdatedTo": "Jellyfin Szerver frissítve lett a következőre: {0}", - "MessageNamedServerConfigurationUpdatedWithValue": "Szerver konfigurációs rész frissítve: {0}", - "MessageServerConfigurationUpdated": "Szerver konfiguráció frissítve", + "MessageApplicationUpdated": "A Jellyfin kiszolgáló frissítve", + "MessageApplicationUpdatedTo": "A Jellyfin kiszolgáló frissítve lett a következőre: {0}", + "MessageNamedServerConfigurationUpdatedWithValue": "A kiszolgálókonfigurációs rész frissítve: {0}", + "MessageServerConfigurationUpdated": "Kiszolgálókonfiguráció frissítve", "MixedContent": "Vegyes tartalom", "Movies": "Filmek", - "Music": "Zene", + "Music": "Zenék", "MusicVideos": "Zenei videóklippek", "NameInstallFailed": "{0} sikertelen telepítés", "NameSeasonNumber": "{0}. évad", "NameSeasonUnknown": "Ismeretlen évad", - "NewVersionIsAvailable": "Letölthető a Jellyfin Szerver új verziója.", + "NewVersionIsAvailable": "Letölthető a Jellyfin kiszolgáló új verziója.", "NotificationOptionApplicationUpdateAvailable": "Frissítés érhető el az alkalmazáshoz", "NotificationOptionApplicationUpdateInstalled": "Alkalmazásfrissítés telepítve", - "NotificationOptionAudioPlayback": "Audió lejátszás elkezdve", - "NotificationOptionAudioPlaybackStopped": "Audió lejátszás leállítva", - "NotificationOptionCameraImageUploaded": "Kamera kép feltöltve", - "NotificationOptionInstallationFailed": "Telepítés sikertelen", + "NotificationOptionAudioPlayback": "Hanglejátszás elkezdve", + "NotificationOptionAudioPlaybackStopped": "Hanglejátszás leállítva", + "NotificationOptionCameraImageUploaded": "Kamerakép feltöltve", + "NotificationOptionInstallationFailed": "Telepítési hiba", "NotificationOptionNewLibraryContent": "Új tartalom hozzáadva", - "NotificationOptionPluginError": "Bővítmény hiba", + "NotificationOptionPluginError": "Bővítményhiba", "NotificationOptionPluginInstalled": "Bővítmény telepítve", "NotificationOptionPluginUninstalled": "Bővítmény eltávolítva", - "NotificationOptionPluginUpdateInstalled": "Bővítmény frissítés telepítve", - "NotificationOptionServerRestartRequired": "Szerver újraindítás szükséges", + "NotificationOptionPluginUpdateInstalled": "Bővítményfrissítés telepítve", + "NotificationOptionServerRestartRequired": "A kiszolgáló újraindítása szükséges", "NotificationOptionTaskFailed": "Ütemezett feladat hiba", "NotificationOptionUserLockedOut": "Felhasználó tiltva", - "NotificationOptionVideoPlayback": "Videó lejátszás elkezdve", - "NotificationOptionVideoPlaybackStopped": "Videó lejátszás leállítva", + "NotificationOptionVideoPlayback": "Videólejátszás elkezdve", + "NotificationOptionVideoPlaybackStopped": "Videólejátszás leállítva", "Photos": "Fényképek", "Playlists": "Lejátszási listák", "Plugin": "Bővítmény", @@ -69,47 +69,47 @@ "ProviderValue": "Szolgáltató: {0}", "ScheduledTaskFailedWithName": "{0} sikertelen", "ScheduledTaskStartedWithName": "{0} elkezdve", - "ServerNameNeedsToBeRestarted": "{0}-t újra kell indítani", + "ServerNameNeedsToBeRestarted": "A(z) {0} újraindítása szükséges", "Shows": "Sorozatok", - "Songs": "Dalok", - "StartupEmbyServerIsLoading": "A Jellyfin Szerver betöltődik. Kérlek, próbáld újra hamarosan.", + "Songs": "Számok", + "StartupEmbyServerIsLoading": "A Jellyfin kiszolgáló betöltődik. Próbálja újra hamarosan.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", - "SubtitleDownloadFailureFromForItem": "Nem sikerült a felirat letöltése innen: {0} ehhez: {1}", - "Sync": "Szinkronizál", + "SubtitleDownloadFailureFromForItem": "Nem sikerült a felirat letöltése innen: {0}, ehhez: {1}", + "Sync": "Szinkronizálás", "System": "Rendszer", "TvShows": "TV műsorok", "User": "Felhasználó", "UserCreatedWithName": "{0} felhasználó létrehozva", "UserDeletedWithName": "{0} felhasználó törölve", - "UserDownloadingItemWithValues": "{0} letölti {1}", + "UserDownloadingItemWithValues": "{0} letölti: {1}", "UserLockedOutWithName": "{0} felhasználó zárolva van", "UserOfflineFromDevice": "{0} kijelentkezett innen: {1}", "UserOnlineFromDevice": "{0} online innen: {1}", - "UserPasswordChangedWithName": "Jelszó megváltozott a következő felhasználó számára: {0}", - "UserPolicyUpdatedWithName": "A felhasználói házirend frissítve lett neki: {0}", - "UserStartedPlayingItemWithValues": "{0} elkezdte játszani a következőt: {1} itt: {2}", - "UserStoppedPlayingItemWithValues": "{0} befejezte {1} lejátászását itt: {2}", + "UserPasswordChangedWithName": "{0} jelszava megváltozott", + "UserPolicyUpdatedWithName": "{0} felhasználói házirendje frissült", + "UserStartedPlayingItemWithValues": "{0} elkezdte lejátszani a következőt: {1}, itt: {2}", + "UserStoppedPlayingItemWithValues": "{0} befejezte a következő lejátszását: {1}, itt: {2}", "ValueHasBeenAddedToLibrary": "{0} hozzáadva a médiatárhoz", - "ValueSpecialEpisodeName": "Special - {0}", + "ValueSpecialEpisodeName": "Különkiadás – {0}", "VersionNumber": "Verzió: {0}", "TaskCleanTranscode": "Átkódolási könyvtár ürítése", "TaskUpdatePluginsDescription": "Letölti és telepíti a frissítéseket azokhoz a bővítményekhez, amelyeknél az automatikus frissítés engedélyezve van.", "TaskUpdatePlugins": "Bővítmények frissítése", - "TaskRefreshPeopleDescription": "Frissíti a szereplők és a stábok metaadatait a könyvtáradban.", + "TaskRefreshPeopleDescription": "Frissíti a szereplők és a stábok metaadatait a médiatárban.", "TaskRefreshPeople": "Személyek frissítése", "TaskCleanLogsDescription": "Törli azokat a naplófájlokat, amelyek {0} napnál régebbiek.", "TaskCleanLogs": "Naplózási könyvtár ürítése", - "TaskRefreshLibraryDescription": "Átvizsgálja a könyvtáraidat új fájlokért és frissíti a metaadatokat.", - "TaskRefreshLibrary": "Média könyvtár beolvasása", - "TaskRefreshChapterImagesDescription": "Miniatűröket generál olyan videókhoz, amely tartalmaz fejezeteket.", - "TaskRefreshChapterImages": "Fejezetek képeinek generálása", + "TaskRefreshLibraryDescription": "Átvizsgálja a médiatárat új fájlokat keresve, és frissíti a metaadatokat.", + "TaskRefreshLibrary": "Médiatár átvizsgálása", + "TaskRefreshChapterImagesDescription": "Miniatűröket hoz létre az olyan videókhoz, amely tartalmaz fejezeteket.", + "TaskRefreshChapterImages": "Fejezetképek kinyerése", "TaskCleanCacheDescription": "Törli azokat a gyorsítótárazott fájlokat, amikre a rendszernek már nincs szüksége.", "TaskCleanCache": "Gyorsítótár könyvtárának ürítése", "TasksChannelsCategory": "Internetes csatornák", "TasksApplicationCategory": "Alkalmazás", "TasksLibraryCategory": "Könyvtár", "TasksMaintenanceCategory": "Karbantartás", - "TaskDownloadMissingSubtitlesDescription": "A metaadat konfiguráció alapján ellenőrzi és letölti a hiányzó feliratokat az internetről.", + "TaskDownloadMissingSubtitlesDescription": "A metaadat-konfiguráció alapján ellenőrzi és letölti a hiányzó feliratokat az internetről.", "TaskDownloadMissingSubtitles": "Hiányzó feliratok letöltése", "TaskRefreshChannelsDescription": "Frissíti az internetes csatornák adatait.", "TaskRefreshChannels": "Csatornák frissítése", @@ -121,8 +121,8 @@ "Default": "Alapértelmezett", "TaskOptimizeDatabaseDescription": "Tömöríti az adatbázist és csonkolja a szabad helyet. A feladat futtatása a könyvtár beolvasása után, vagy egyéb, adatbázis-módosítást igénylő változtatások végrehajtása javíthatja a teljesítményt.", "TaskOptimizeDatabase": "Adatbázis optimalizálása", - "TaskKeyframeExtractor": "Kulcskockák kibontása", - "TaskKeyframeExtractorDescription": "Kulcskockákat bont ki a videofájlokból, hogy pontosabb HLS lejátszási listákat hozzon létre. Ez a feladat hosszú ideig tarthat.", + "TaskKeyframeExtractor": "Kulcsképkockák kibontása", + "TaskKeyframeExtractorDescription": "Kibontja a kulcsképkockákat a videófájlokból, hogy pontosabb HLS lejátszási listákat hozzon létre. Ez a feladat hosszú ideig tarthat.", "External": "Külső", "HearingImpaired": "Hallássérült" } From 99d3f9c4b295e1f577b1b0c7d045563e5a29f2eb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 6 Jul 2023 14:03:31 +0000 Subject: [PATCH 438/858] chore(deps): update github/codeql-action action to v2.20.3 --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index eea2381891..51cbad360c 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@004c5de30b6423267685b897a3d595e944f7fed5 # v2.20.2 + uses: github/codeql-action/init@46ed16ded91731b2df79a2893d3aea8e9f03b5c4 # v2.20.3 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@004c5de30b6423267685b897a3d595e944f7fed5 # v2.20.2 + uses: github/codeql-action/autobuild@46ed16ded91731b2df79a2893d3aea8e9f03b5c4 # v2.20.3 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@004c5de30b6423267685b897a3d595e944f7fed5 # v2.20.2 + uses: github/codeql-action/analyze@46ed16ded91731b2df79a2893d3aea8e9f03b5c4 # v2.20.3 From 5f2568d9ed55e916112816983beec0430a5d9757 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 6 Jul 2023 19:07:57 +0000 Subject: [PATCH 439/858] chore(deps): update dependency blurhashsharp to v1.3.0 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 0bd7b6e9c2..89b3ba9a18 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -11,7 +11,7 @@ <PackageVersion Include="AutoFixture" Version="4.18.0" /> <PackageVersion Include="BDInfo" Version="0.7.6.2" /> <PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.2.0" /> - <PackageVersion Include="BlurHashSharp" Version="1.2.0" /> + <PackageVersion Include="BlurHashSharp" Version="1.3.0" /> <PackageVersion Include="CommandLineParser" Version="2.9.1" /> <PackageVersion Include="coverlet.collector" Version="6.0.0" /> <PackageVersion Include="Diacritics" Version="3.3.18" /> From 60685e9c639d900f960de35eb9bc5a7779c5c7fe Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 6 Jul 2023 23:28:51 +0000 Subject: [PATCH 440/858] chore(deps): update dependency xunit.runner.visualstudio to v2.5.0 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 0bd7b6e9c2..1c0b9dd2ff 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -86,7 +86,7 @@ <PackageVersion Include="TMDbLib" Version="2.0.0" /> <PackageVersion Include="UTF.Unknown" Version="2.5.1" /> <PackageVersion Include="Xunit.Priority" Version="1.1.6" /> - <PackageVersion Include="xunit.runner.visualstudio" Version="2.4.5" /> + <PackageVersion Include="xunit.runner.visualstudio" Version="2.5.0" /> <PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" /> <PackageVersion Include="xunit" Version="2.4.2" /> </ItemGroup> From 958f8f71e8174aa3884cbafa54d07bf247517014 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Thu, 8 Jun 2023 10:36:04 +0200 Subject: [PATCH 441/858] Add wrapper object for authentication event information --- .../Session/SessionManager.cs | 5 +- .../Security/AuthenticationFailedLogger.cs | 6 +- .../Security/AuthenticationSucceededLogger.cs | 8 +-- .../EventingServiceCollectionExtensions.cs | 7 +-- .../AuthenticationRequestEventArgs.cs | 60 +++++++++++++++++++ .../AuthenticationResultEventArgs.cs | 37 ++++++++++++ 6 files changed, 110 insertions(+), 13 deletions(-) create mode 100644 MediaBrowser.Controller/Events/Authentication/AuthenticationRequestEventArgs.cs create mode 100644 MediaBrowser.Controller/Events/Authentication/AuthenticationResultEventArgs.cs diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 5f6dc93fb3..f26cc56636 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -24,6 +24,7 @@ using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Events; +using MediaBrowser.Controller.Events.Authentication; using MediaBrowser.Controller.Events.Session; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; @@ -1462,7 +1463,7 @@ namespace Emby.Server.Implementations.Session if (user is null) { - await _eventManager.PublishAsync(new GenericEventArgs<AuthenticationRequest>(request)).ConfigureAwait(false); + await _eventManager.PublishAsync(new GenericEventArgs<AuthenticationRequestEventArgs>(new AuthenticationRequestEventArgs(request))).ConfigureAwait(false); throw new AuthenticationException("Invalid username or password entered."); } @@ -1498,7 +1499,7 @@ namespace Emby.Server.Implementations.Session ServerId = _appHost.SystemId }; - await _eventManager.PublishAsync(new GenericEventArgs<AuthenticationResult>(returnResult)).ConfigureAwait(false); + await _eventManager.PublishAsync(new GenericEventArgs<AuthenticationResultEventArgs>(new AuthenticationResultEventArgs(returnResult))).ConfigureAwait(false); return returnResult; } diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationFailedLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationFailedLogger.cs index f899b4497a..f4835a585f 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationFailedLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationFailedLogger.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Events; using MediaBrowser.Controller.Events; -using MediaBrowser.Controller.Session; +using MediaBrowser.Controller.Events.Authentication; using MediaBrowser.Model.Activity; using MediaBrowser.Model.Globalization; using Microsoft.Extensions.Logging; @@ -14,7 +14,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Security /// <summary> /// Creates an entry in the activity log when there is a failed login attempt. /// </summary> - public class AuthenticationFailedLogger : IEventConsumer<GenericEventArgs<AuthenticationRequest>> + public class AuthenticationFailedLogger : IEventConsumer<GenericEventArgs<AuthenticationRequestEventArgs>> { private readonly ILocalizationManager _localizationManager; private readonly IActivityManager _activityManager; @@ -31,7 +31,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Security } /// <inheritdoc /> - public async Task OnEvent(GenericEventArgs<AuthenticationRequest> eventArgs) + public async Task OnEvent(GenericEventArgs<AuthenticationRequestEventArgs> eventArgs) { await _activityManager.CreateAsync(new ActivityLog( string.Format( diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs index 8b0bd84c66..fe2737bafc 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs @@ -2,8 +2,8 @@ using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Events; -using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Events; +using MediaBrowser.Controller.Events.Authentication; using MediaBrowser.Model.Activity; using MediaBrowser.Model.Globalization; @@ -12,7 +12,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Security /// <summary> /// Creates an entry in the activity log when there is a successful login attempt. /// </summary> - public class AuthenticationSucceededLogger : IEventConsumer<GenericEventArgs<AuthenticationResult>> + public class AuthenticationSucceededLogger : IEventConsumer<GenericEventArgs<AuthenticationResultEventArgs>> { private readonly ILocalizationManager _localizationManager; private readonly IActivityManager _activityManager; @@ -29,7 +29,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Security } /// <inheritdoc /> - public async Task OnEvent(GenericEventArgs<AuthenticationResult> eventArgs) + public async Task OnEvent(GenericEventArgs<AuthenticationResultEventArgs> eventArgs) { await _activityManager.CreateAsync(new ActivityLog( string.Format( @@ -42,7 +42,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Security ShortOverview = string.Format( CultureInfo.InvariantCulture, _localizationManager.GetLocalizedString("LabelIpAddressValue"), - eventArgs.Argument.SessionInfo.RemoteEndPoint), + eventArgs.Argument.SessionInfo?.RemoteEndPoint), }).ConfigureAwait(false); } } diff --git a/Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs b/Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs index 5d558189b1..93ab15e24b 100644 --- a/Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs +++ b/Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs @@ -8,12 +8,11 @@ using Jellyfin.Server.Implementations.Events.Consumers.System; using Jellyfin.Server.Implementations.Events.Consumers.Updates; using Jellyfin.Server.Implementations.Events.Consumers.Users; using MediaBrowser.Common.Updates; -using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Events; +using MediaBrowser.Controller.Events.Authentication; using MediaBrowser.Controller.Events.Session; using MediaBrowser.Controller.Events.Updates; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Session; using MediaBrowser.Controller.Subtitles; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.DependencyInjection; @@ -35,8 +34,8 @@ namespace Jellyfin.Server.Implementations.Events collection.AddScoped<IEventConsumer<SubtitleDownloadFailureEventArgs>, SubtitleDownloadFailureLogger>(); // Security consumers - collection.AddScoped<IEventConsumer<GenericEventArgs<AuthenticationRequest>>, AuthenticationFailedLogger>(); - collection.AddScoped<IEventConsumer<GenericEventArgs<AuthenticationResult>>, AuthenticationSucceededLogger>(); + collection.AddScoped<IEventConsumer<GenericEventArgs<AuthenticationRequestEventArgs>>, AuthenticationFailedLogger>(); + collection.AddScoped<IEventConsumer<GenericEventArgs<AuthenticationResultEventArgs>>, AuthenticationSucceededLogger>(); // Session consumers collection.AddScoped<IEventConsumer<PlaybackStartEventArgs>, PlaybackStartLogger>(); diff --git a/MediaBrowser.Controller/Events/Authentication/AuthenticationRequestEventArgs.cs b/MediaBrowser.Controller/Events/Authentication/AuthenticationRequestEventArgs.cs new file mode 100644 index 0000000000..c0d8a277bd --- /dev/null +++ b/MediaBrowser.Controller/Events/Authentication/AuthenticationRequestEventArgs.cs @@ -0,0 +1,60 @@ +using System; +using MediaBrowser.Controller.Session; + +namespace MediaBrowser.Controller.Events.Authentication; + +/// <summary> +/// A class representing an authentication result event. +/// </summary> +public class AuthenticationRequestEventArgs +{ + /// <summary> + /// Initializes a new instance of the <see cref="AuthenticationRequestEventArgs"/> class. + /// </summary> + /// <param name="request">The <see cref="AuthenticationRequest"/>.</param> + public AuthenticationRequestEventArgs(AuthenticationRequest request) + { + Username = request.Username; + UserId = request.UserId; + App = request.App; + AppVersion = request.AppVersion; + DeviceId = request.DeviceId; + DeviceName = request.DeviceName; + RemoteEndPoint = request.RemoteEndPoint; + } + + /// <summary> + /// Gets or sets the user name. + /// </summary> + public string? Username { get; set; } + + /// <summary> + /// Gets or sets the user id. + /// </summary> + public Guid? UserId { get; set; } + + /// <summary> + /// Gets or sets the app. + /// </summary> + public string? App { get; set; } + + /// <summary> + /// Gets or sets the app version. + /// </summary> + public string? AppVersion { get; set; } + + /// <summary> + /// Gets or sets the device id. + /// </summary> + public string? DeviceId { get; set; } + + /// <summary> + /// Gets or sets the device name. + /// </summary> + public string? DeviceName { get; set; } + + /// <summary> + /// Gets or sets the remote endpoint. + /// </summary> + public string? RemoteEndPoint { get; set; } +} diff --git a/MediaBrowser.Controller/Events/Authentication/AuthenticationResultEventArgs.cs b/MediaBrowser.Controller/Events/Authentication/AuthenticationResultEventArgs.cs new file mode 100644 index 0000000000..5564fa1cef --- /dev/null +++ b/MediaBrowser.Controller/Events/Authentication/AuthenticationResultEventArgs.cs @@ -0,0 +1,37 @@ +using MediaBrowser.Controller.Authentication; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Dto; + +namespace MediaBrowser.Controller.Events.Authentication; + +/// <summary> +/// A class representing an authentication result event. +/// </summary> +public class AuthenticationResultEventArgs +{ + /// <summary> + /// Initializes a new instance of the <see cref="AuthenticationResultEventArgs"/> class. + /// </summary> + /// <param name="result">The <see cref="AuthenticationResult"/>.</param> + public AuthenticationResultEventArgs(AuthenticationResult result) + { + User = result.User; + SessionInfo = result.SessionInfo; + ServerId = result.ServerId; + } + + /// <summary> + /// Gets or sets the user. + /// </summary> + public UserDto User { get; set; } + + /// <summary> + /// Gets or sets the session information. + /// </summary> + public SessionInfo? SessionInfo { get; set; } + + /// <summary> + /// Gets or sets the server id. + /// </summary> + public string? ServerId { get; set; } +} From 46a6755e65c9587fd1ae33ee4ffdb3cd406fd72b Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Thu, 8 Jun 2023 10:36:45 +0200 Subject: [PATCH 442/858] Add item id to playback start/stop events --- .../Consumers/Session/PlaybackStartLogger.cs | 21 +++++++++++-------- .../Consumers/Session/PlaybackStopLogger.cs | 5 ++++- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs index aeb62e814c..8ed76565cb 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs @@ -58,15 +58,18 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session var user = eventArgs.Users[0]; await _activityManager.CreateAsync(new ActivityLog( - string.Format( - CultureInfo.InvariantCulture, - _localizationManager.GetLocalizedString("UserStartedPlayingItemWithValues"), - user.Username, - GetItemName(eventArgs.MediaInfo), - eventArgs.DeviceName), - GetPlaybackNotificationType(eventArgs.MediaInfo.MediaType), - user.Id)) - .ConfigureAwait(false); + string.Format( + CultureInfo.InvariantCulture, + _localizationManager.GetLocalizedString("UserStartedPlayingItemWithValues"), + user.Username, + GetItemName(eventArgs.MediaInfo), + eventArgs.DeviceName), + GetPlaybackNotificationType(eventArgs.MediaInfo.MediaType), + user.Id) + { + ItemId = eventArgs.Item?.Id.ToString() + }) + .ConfigureAwait(false); } private static string GetItemName(BaseItemDto item) diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs index dd7290fb84..da9c2b8a2f 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs @@ -73,7 +73,10 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session GetItemName(item), eventArgs.DeviceName), notificationType, - user.Id)) + user.Id) + { + ItemId = eventArgs.Item?.Id.ToString() + }) .ConfigureAwait(false); } From 05d98fe24c594ae43de4cd9f54139f8b04324119 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Fri, 9 Jun 2023 17:11:22 +0200 Subject: [PATCH 443/858] Enforce permissions on websocket connections --- .../HttpServer/WebSocketConnection.cs | 28 ++++++++----------- .../HttpServer/WebSocketManager.cs | 3 +- .../ActivityLogWebSocketListener.cs | 14 +++++++++- .../SessionInfoWebSocketListener.cs | 12 ++++++++ .../Net/BasePeriodicWebSocketListener.cs | 2 +- .../Net/IWebSocketConnection.cs | 17 +++++++++-- .../HttpServer/WebSocketConnectionTests.cs | 8 +++--- 7 files changed, 58 insertions(+), 26 deletions(-) diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs index fd7653a32d..7f620d666d 100644 --- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs +++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs @@ -12,6 +12,7 @@ using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net.WebSocketMessages; using MediaBrowser.Controller.Net.WebSocketMessages.Outbound; using MediaBrowser.Model.Session; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.HttpServer @@ -43,14 +44,17 @@ namespace Emby.Server.Implementations.HttpServer /// </summary> /// <param name="logger">The logger.</param> /// <param name="socket">The socket.</param> + /// <param name="authorizationInfo">The authorization information.</param> /// <param name="remoteEndPoint">The remote end point.</param> public WebSocketConnection( ILogger<WebSocketConnection> logger, WebSocket socket, + AuthorizationInfo authorizationInfo, IPAddress? remoteEndPoint) { _logger = logger; _socket = socket; + AuthorizationInfo = authorizationInfo; RemoteEndPoint = remoteEndPoint; _jsonOptions = JsonDefaults.Options; @@ -60,30 +64,22 @@ namespace Emby.Server.Implementations.HttpServer /// <inheritdoc /> public event EventHandler<EventArgs>? Closed; - /// <summary> - /// Gets the remote end point. - /// </summary> + /// <inheritdoc /> + public AuthorizationInfo AuthorizationInfo { get; } + + /// <inheritdoc /> public IPAddress? RemoteEndPoint { get; } - /// <summary> - /// Gets or sets the receive action. - /// </summary> - /// <value>The receive action.</value> + /// <inheritdoc /> public Func<WebSocketMessageInfo, Task>? OnReceive { get; set; } - /// <summary> - /// Gets the last activity date. - /// </summary> - /// <value>The last activity date.</value> + /// <inheritdoc /> public DateTime LastActivityDate { get; private set; } /// <inheritdoc /> public DateTime LastKeepAliveDate { get; set; } - /// <summary> - /// Gets the state. - /// </summary> - /// <value>The state.</value> + /// <inheritdoc /> public WebSocketState State => _socket.State; /// <inheritdoc /> @@ -101,7 +97,7 @@ namespace Emby.Server.Implementations.HttpServer } /// <inheritdoc /> - public async Task ProcessAsync(CancellationToken cancellationToken = default) + public async Task ReceiveAsync(CancellationToken cancellationToken = default) { var pipe = new Pipe(); var writer = pipe.Writer; diff --git a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs index ecfb242f6f..52f14b0b10 100644 --- a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs +++ b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs @@ -51,6 +51,7 @@ namespace Emby.Server.Implementations.HttpServer using var connection = new WebSocketConnection( _loggerFactory.CreateLogger<WebSocketConnection>(), webSocket, + authorizationInfo, context.GetNormalizedRemoteIP()) { OnReceive = ProcessWebSocketMessageReceived @@ -64,7 +65,7 @@ namespace Emby.Server.Implementations.HttpServer await Task.WhenAll(tasks).ConfigureAwait(false); - await connection.ProcessAsync().ConfigureAwait(false); + await connection.ReceiveAsync().ConfigureAwait(false); _logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress); } catch (Exception ex) // Otherwise ASP.Net will ignore the exception diff --git a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs index 4a5e0ecd4f..33d391f296 100644 --- a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs +++ b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs @@ -1,6 +1,8 @@ using System; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using Jellyfin.Data.Events; +using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Activity; using MediaBrowser.Model.Session; @@ -9,7 +11,7 @@ using Microsoft.Extensions.Logging; namespace Jellyfin.Api.WebSocketListeners; /// <summary> -/// Class SessionInfoWebSocketListener. +/// Class ActivityLogWebSocketListener. /// </summary> public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<ActivityLogEntry[], WebSocketListenerState> { @@ -56,6 +58,16 @@ public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<Activi base.Dispose(dispose); } + private new void Start(WebSocketMessageInfo message) + { + if (!message.Connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator)) + { + throw new AuthenticationException("Only admin users can retrieve the activity log."); + } + + base.Start(message); + } + private async void OnEntryCreated(object? sender, GenericEventArgs<ActivityLogEntry> e) { await SendData(true).ConfigureAwait(false); diff --git a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs index 0d8bf205c9..0d614ba4f2 100644 --- a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs +++ b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; using System.Threading.Tasks; +using Jellyfin.Data.Enums; +using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Session; @@ -66,6 +68,16 @@ public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnume base.Dispose(dispose); } + private new void Start(WebSocketMessageInfo message) + { + if (!message.Connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator)) + { + throw new AuthenticationException("Only admin users can subscribe to session information."); + } + + base.Start(message); + } + private async void OnSessionManagerSessionActivity(object? sender, SessionEventArgs e) { await SendData(false).ConfigureAwait(false); diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs index 8f38d4976b..0ad1e4f50b 100644 --- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs +++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs @@ -96,7 +96,7 @@ namespace MediaBrowser.Controller.Net /// Starts sending messages over a web socket. /// </summary> /// <param name="message">The message.</param> - private void Start(WebSocketMessageInfo message) + protected void Start(WebSocketMessageInfo message) { var vals = message.Data.Split(','); diff --git a/MediaBrowser.Controller/Net/IWebSocketConnection.cs b/MediaBrowser.Controller/Net/IWebSocketConnection.cs index 79f0846b4a..bba5a6b851 100644 --- a/MediaBrowser.Controller/Net/IWebSocketConnection.cs +++ b/MediaBrowser.Controller/Net/IWebSocketConnection.cs @@ -1,5 +1,3 @@ -#pragma warning disable CS1591 - using System; using System.Net; using System.Net.WebSockets; @@ -9,6 +7,9 @@ using MediaBrowser.Controller.Net.WebSocketMessages; namespace MediaBrowser.Controller.Net { + /// <summary> + /// Interface for WebSocket connections. + /// </summary> public interface IWebSocketConnection : IAsyncDisposable, IDisposable { /// <summary> @@ -40,6 +41,11 @@ namespace MediaBrowser.Controller.Net /// <value>The state.</value> WebSocketState State { get; } + /// <summary> + /// Gets the authorization information. + /// </summary> + public AuthorizationInfo AuthorizationInfo { get; } + /// <summary> /// Gets the remote end point. /// </summary> @@ -65,6 +71,11 @@ namespace MediaBrowser.Controller.Net /// <exception cref="ArgumentNullException">The message is null.</exception> Task SendAsync<T>(OutboundWebSocketMessage<T> message, CancellationToken cancellationToken); - Task ProcessAsync(CancellationToken cancellationToken = default); + /// <summary> + /// Receives a message asynchronously. + /// </summary> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task ReceiveAsync(CancellationToken cancellationToken = default); } } diff --git a/tests/Jellyfin.Server.Implementations.Tests/HttpServer/WebSocketConnectionTests.cs b/tests/Jellyfin.Server.Implementations.Tests/HttpServer/WebSocketConnectionTests.cs index f016118192..22667ee82d 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/HttpServer/WebSocketConnectionTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/HttpServer/WebSocketConnectionTests.cs @@ -13,7 +13,7 @@ namespace Jellyfin.Server.Implementations.Tests.HttpServer [Fact] public void DeserializeWebSocketMessage_SingleSegment_Success() { - var con = new WebSocketConnection(new NullLogger<WebSocketConnection>(), null!, null!); + var con = new WebSocketConnection(new NullLogger<WebSocketConnection>(), null!, null!, null!); var bytes = File.ReadAllBytes("Test Data/HttpServer/ForceKeepAlive.json"); con.DeserializeWebSocketMessage(new ReadOnlySequence<byte>(bytes), out var bytesConsumed); Assert.Equal(109, bytesConsumed); @@ -23,7 +23,7 @@ namespace Jellyfin.Server.Implementations.Tests.HttpServer public void DeserializeWebSocketMessage_MultipleSegments_Success() { const int SplitPos = 64; - var con = new WebSocketConnection(new NullLogger<WebSocketConnection>(), null!, null!); + var con = new WebSocketConnection(new NullLogger<WebSocketConnection>(), null!, null!, null!); var bytes = File.ReadAllBytes("Test Data/HttpServer/ForceKeepAlive.json"); var seg1 = new BufferSegment(new Memory<byte>(bytes, 0, SplitPos)); var seg2 = seg1.Append(new Memory<byte>(bytes, SplitPos, bytes.Length - SplitPos)); @@ -34,7 +34,7 @@ namespace Jellyfin.Server.Implementations.Tests.HttpServer [Fact] public void DeserializeWebSocketMessage_ValidPartial_Success() { - var con = new WebSocketConnection(new NullLogger<WebSocketConnection>(), null!, null!); + var con = new WebSocketConnection(new NullLogger<WebSocketConnection>(), null!, null!, null!); var bytes = File.ReadAllBytes("Test Data/HttpServer/ValidPartial.json"); con.DeserializeWebSocketMessage(new ReadOnlySequence<byte>(bytes), out var bytesConsumed); Assert.Equal(109, bytesConsumed); @@ -43,7 +43,7 @@ namespace Jellyfin.Server.Implementations.Tests.HttpServer [Fact] public void DeserializeWebSocketMessage_Partial_ThrowJsonException() { - var con = new WebSocketConnection(new NullLogger<WebSocketConnection>(), null!, null!); + var con = new WebSocketConnection(new NullLogger<WebSocketConnection>(), null!, null!, null!); var bytes = File.ReadAllBytes("Test Data/HttpServer/Partial.json"); Assert.Throws<JsonException>(() => con.DeserializeWebSocketMessage(new ReadOnlySequence<byte>(bytes), out var bytesConsumed)); } From a0d13a241891bbf832cc86909f0dbe20c979be52 Mon Sep 17 00:00:00 2001 From: Shadowghost <Shadowghost@users.noreply.github.com> Date: Sat, 10 Jun 2023 18:06:22 +0200 Subject: [PATCH 444/858] Apply suggestions from code review Co-authored-by: Cody Robibero <cody@robibe.ro> --- .../Events/Consumers/Session/PlaybackStartLogger.cs | 2 +- .../Events/Consumers/Session/PlaybackStopLogger.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs index 8ed76565cb..27726a57a6 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs @@ -67,7 +67,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session GetPlaybackNotificationType(eventArgs.MediaInfo.MediaType), user.Id) { - ItemId = eventArgs.Item?.Id.ToString() + ItemId = eventArgs.Item?.Id.ToString("N", CultureInfo.InvariantCulture), }) .ConfigureAwait(false); } diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs index da9c2b8a2f..6b16477aa7 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs @@ -75,7 +75,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session notificationType, user.Id) { - ItemId = eventArgs.Item?.Id.ToString() + ItemId = eventArgs.Item?.Id.ToString("N", CultureInfo.InvariantCulture), }) .ConfigureAwait(false); } From 266d55b7aab84133bdc66c28b628f2ddd4ad7c9e Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Fri, 7 Jul 2023 08:52:14 +0200 Subject: [PATCH 445/858] Fix bad string interpolation in MaskToCidr --- Jellyfin.Networking/Extensions/NetworkExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Networking/Extensions/NetworkExtensions.cs b/Jellyfin.Networking/Extensions/NetworkExtensions.cs index d55f78135b..e45fa3bcb7 100644 --- a/Jellyfin.Networking/Extensions/NetworkExtensions.cs +++ b/Jellyfin.Networking/Extensions/NetworkExtensions.cs @@ -104,7 +104,7 @@ public static partial class NetworkExtensions Span<byte> bytes = stackalloc byte[mask.AddressFamily == AddressFamily.InterNetwork ? Network.IPv4MaskBytes : Network.IPv6MaskBytes]; if (!mask.TryWriteBytes(bytes, out var bytesWritten)) { - Console.WriteLine("Unable to write address bytes, only {bytesWritten} bytes written."); + Console.WriteLine("Unable to write address bytes, only ${bytesWritten} bytes written."); } var zeroed = false; From 4f9dc53e6ed3dd09bd0b8bbee20947a31561c2d1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 7 Jul 2023 13:12:02 +0000 Subject: [PATCH 446/858] chore(deps): update dependency blurhashsharp.skiasharp to v1.3.0 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 3bc989d4a8..4c236b2586 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -10,7 +10,7 @@ <PackageVersion Include="AutoFixture.Xunit2" Version="4.18.0" /> <PackageVersion Include="AutoFixture" Version="4.18.0" /> <PackageVersion Include="BDInfo" Version="0.7.6.2" /> - <PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.2.0" /> + <PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.3.0" /> <PackageVersion Include="BlurHashSharp" Version="1.3.0" /> <PackageVersion Include="CommandLineParser" Version="2.9.1" /> <PackageVersion Include="coverlet.collector" Version="6.0.0" /> From 1ea8150b0ce815ffe0936e3c2629645b2afa9525 Mon Sep 17 00:00:00 2001 From: Aran Chananar <cloverink@gmail.com> Date: Sun, 9 Jul 2023 05:35:46 +0000 Subject: [PATCH 447/858] Translated using Weblate (Thai) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/th/ --- Emby.Server.Implementations/Localization/Core/th.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/th.json b/Emby.Server.Implementations/Localization/Core/th.json index 1a4fef64e8..3cdf743d55 100644 --- a/Emby.Server.Implementations/Localization/Core/th.json +++ b/Emby.Server.Implementations/Localization/Core/th.json @@ -121,5 +121,7 @@ "TaskOptimizeDatabase": "ปรับปรุงประสิทธิภาพฐานข้อมูล", "TaskOptimizeDatabaseDescription": "ลดขนาดการจัดเก็บฐานข้อมูล ใช้งานคำสั่งนี้หลังจากสแกนไลบรารีหรือหลังจากการเปลี่ยนแปลงฐานข้อมูล อาจจะทำให้ระบบทำงานเร็วขึ้น", "External": "ภายนอก", - "HearingImpaired": "บกพร่องทางการได้ยิน" + "HearingImpaired": "บกพร่องทางการได้ยิน", + "TaskKeyframeExtractor": "ตัวแยกคีย์เฟรม", + "TaskKeyframeExtractorDescription": "แยกคีย์เฟรมจากไฟล์วีดีโอเพื่อสร้างรายการ HLS ให้ถูกต้อง. กระบวนการนี้อาจใช้ระยะเวลานาน" } From 78c17ba895b16a063e30171a5f2c663d65e37c2c Mon Sep 17 00:00:00 2001 From: Sky-High <el.bakkum@gmail.com> Date: Tue, 11 Jul 2023 02:13:55 +0200 Subject: [PATCH 448/858] fix #9983 MigrateNetworkConfiguration error --- Jellyfin.Server/Migrations/MigrationRunner.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs index 33c02f41c6..2db0b77cd9 100644 --- a/Jellyfin.Server/Migrations/MigrationRunner.cs +++ b/Jellyfin.Server/Migrations/MigrationRunner.cs @@ -93,7 +93,7 @@ namespace Jellyfin.Server.Migrations private static void HandleStartupWizardCondition(IEnumerable<IMigrationRoutine> migrations, MigrationOptions migrationOptions, bool isStartWizardCompleted, ILogger logger) { - if (isStartWizardCompleted || migrationOptions.Applied.Count != 0) + if (isStartWizardCompleted) { return; } @@ -106,6 +106,8 @@ namespace Jellyfin.Server.Migrations private static void PerformMigrations(IMigrationRoutine[] migrations, MigrationOptions migrationOptions, Action<MigrationOptions> saveConfiguration, ILogger logger) { + // save already applied migrations, and skip them thereafter + saveConfiguration(migrationOptions); var appliedMigrationIds = migrationOptions.Applied.Select(m => m.Id).ToHashSet(); for (var i = 0; i < migrations.Length; i++) From 06b80a8cededab5020ba23c36e62855252ae3b0d Mon Sep 17 00:00:00 2001 From: Sky-High <el.bakkum@gmail.com> Date: Tue, 11 Jul 2023 20:28:52 +0200 Subject: [PATCH 449/858] fix for MigrateNetworkConfiguration.cs --- .../MigrateNetworkConfiguration.cs | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs index 3b32e60437..a4379197cd 100644 --- a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs +++ b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Xml; using System.Xml.Serialization; @@ -39,8 +39,23 @@ public class MigrateNetworkConfiguration : IMigrationRoutine { string path = Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "network.xml"); var oldNetworkConfigSerializer = new XmlSerializer(typeof(OldNetworkConfiguration), new XmlRootAttribute("NetworkConfiguration")); - using var xmlReader = XmlReader.Create(path); - var oldNetworkConfiguration = (OldNetworkConfiguration?)oldNetworkConfigSerializer.Deserialize(xmlReader); + OldNetworkConfiguration? oldNetworkConfiguration = null; + + try + { + using (var xmlReader = XmlReader.Create(path)) + { + oldNetworkConfiguration = (OldNetworkConfiguration?)oldNetworkConfigSerializer.Deserialize(xmlReader); + } + } + catch (InvalidOperationException ex) + { + _logger.LogError(ex, "Migrate NetworkConfiguration deserialize Invalid Operation error"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Migrate NetworkConfiguration deserialize error"); + } if (oldNetworkConfiguration is not null) { @@ -82,8 +97,10 @@ public class MigrateNetworkConfiguration : IMigrationRoutine var networkConfigSerializer = new XmlSerializer(typeof(NetworkConfiguration)); var xmlWriterSettings = new XmlWriterSettings { Indent = true }; - using var xmlWriter = XmlWriter.Create(path, xmlWriterSettings); - networkConfigSerializer.Serialize(xmlWriter, networkConfiguration); + using (var xmlWriter = XmlWriter.Create(path, xmlWriterSettings)) + { + networkConfigSerializer.Serialize(xmlWriter, networkConfiguration); + } } } From bfc005642684402f82062868f2137f040fe3780a Mon Sep 17 00:00:00 2001 From: Christoph Landsdorf <Blackskyliner@users.noreply.github.com> Date: Wed, 12 Jul 2023 20:32:43 +0200 Subject: [PATCH 450/858] Add TinyMediaManager compatibility for german parental rating This commit adds the format which gets written by the tool TinyMediaManager which can be used to manage large media databases comfortably. TMM writes wither multiple Codes and/or space divided german FSK codes, but can't be configured to only use the number or the slash delimited variant. After this change the parental control for Libraries managed with TMM and presented/loaded into Jellyfin should work again. --- Emby.Server.Implementations/Localization/Ratings/de.csv | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Emby.Server.Implementations/Localization/Ratings/de.csv b/Emby.Server.Implementations/Localization/Ratings/de.csv index d633a5dab7..f6181575e2 100644 --- a/Emby.Server.Implementations/Localization/Ratings/de.csv +++ b/Emby.Server.Implementations/Localization/Ratings/de.csv @@ -1,12 +1,17 @@ Educational,0 Infoprogramm,0 FSK-0,0 +FSK 0,0 0,0 FSK-6,6 +FSK 6,6 6,6 FSK-12,12 +FSK 12,12 12,12 FSK-16,16 +FSK 16,16 16,16 FSK-18,18 +FSK 18,18 18,18 From 4d2e612693df34a1b6e56d00ea1c56381c5cb81a Mon Sep 17 00:00:00 2001 From: Osa <gzydes@gmail.com> Date: Thu, 13 Jul 2023 07:42:46 +0000 Subject: [PATCH 451/858] Translated using Weblate (Arabic) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ar/ --- Emby.Server.Implementations/Localization/Core/ar.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json index 93d50e6e3b..0e27dafe1b 100644 --- a/Emby.Server.Implementations/Localization/Core/ar.json +++ b/Emby.Server.Implementations/Localization/Core/ar.json @@ -16,7 +16,7 @@ "Folders": "المجلدات", "Genres": "التصنيفات", "HeaderAlbumArtists": "فناني الألبوم", - "HeaderContinueWatching": "استئناف المشاهدة", + "HeaderContinueWatching": "أستئناف المشاهدة", "HeaderFavoriteAlbums": "الألبومات المفضلة", "HeaderFavoriteArtists": "الفنانون المفضلون", "HeaderFavoriteEpisodes": "الحلقات المفضلة", From ce2520e0cf2d4d558be17c2f0ba3642fbfd6a0bd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 14 Jul 2023 18:47:32 -0600 Subject: [PATCH 452/858] chore(deps): update github/codeql-action action to v2.20.4 (#9997) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 51cbad360c..e1ee15c74a 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@46ed16ded91731b2df79a2893d3aea8e9f03b5c4 # v2.20.3 + uses: github/codeql-action/init@489225d82a57396c6f426a40e66d461b16b3461d # v2.20.4 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@46ed16ded91731b2df79a2893d3aea8e9f03b5c4 # v2.20.3 + uses: github/codeql-action/autobuild@489225d82a57396c6f426a40e66d461b16b3461d # v2.20.4 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@46ed16ded91731b2df79a2893d3aea8e9f03b5c4 # v2.20.3 + uses: github/codeql-action/analyze@489225d82a57396c6f426a40e66d461b16b3461d # v2.20.4 From 368f9202ce0048b92b3b43ea78a96486f9cd23c4 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Sat, 15 Jul 2023 20:14:31 +0200 Subject: [PATCH 453/858] Apply review suggestions --- .../WebSocketListeners/ActivityLogWebSocketListener.cs | 6 +++++- .../WebSocketListeners/SessionInfoWebSocketListener.cs | 6 +++++- .../Net/BasePeriodicWebSocketListener.cs | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs index 33d391f296..5b90d65d84 100644 --- a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs +++ b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs @@ -58,7 +58,11 @@ public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<Activi base.Dispose(dispose); } - private new void Start(WebSocketMessageInfo message) + /// <summary> + /// Starts sending messages over an activity log web socket. + /// </summary> + /// <param name="message">The message.</param> + protected override void Start(WebSocketMessageInfo message) { if (!message.Connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator)) { diff --git a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs index 0d614ba4f2..b403ff46d0 100644 --- a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs +++ b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs @@ -68,7 +68,11 @@ public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnume base.Dispose(dispose); } - private new void Start(WebSocketMessageInfo message) + /// <summary> + /// Starts sending messages over a session info web socket. + /// </summary> + /// <param name="message">The message.</param> + protected override void Start(WebSocketMessageInfo message) { if (!message.Connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator)) { diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs index 0ad1e4f50b..e0942e490b 100644 --- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs +++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs @@ -96,7 +96,7 @@ namespace MediaBrowser.Controller.Net /// Starts sending messages over a web socket. /// </summary> /// <param name="message">The message.</param> - protected void Start(WebSocketMessageInfo message) + protected virtual void Start(WebSocketMessageInfo message) { var vals = message.Data.Split(','); From a17f8c56b6d1e12d3800698fb3aa7594db8ac396 Mon Sep 17 00:00:00 2001 From: Yaron Shahrabani <sh.yaron@gmail.com> Date: Sun, 16 Jul 2023 12:57:03 +0000 Subject: [PATCH 454/858] Translated using Weblate (Hebrew) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/he/ --- .../Localization/Core/he.json | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json index 694a3d688c..68e9fe8339 100644 --- a/Emby.Server.Implementations/Localization/Core/he.json +++ b/Emby.Server.Implementations/Localization/Core/he.json @@ -5,18 +5,18 @@ "Artists": "אומנים", "AuthenticationSucceededWithUserName": "{0} אומת בהצלחה", "Books": "ספרים", - "CameraImageUploadedFrom": "תמונת מצלמה חדשה הועלתה מ {0}", + "CameraImageUploadedFrom": "תמונת מצלמה חדשה הועלתה מתוך {0}", "Channels": "ערוצים", "ChapterNameValue": "פרק {0}", "Collections": "אוספים", "DeviceOfflineWithName": "{0} התנתק", "DeviceOnlineWithName": "{0} מחובר", - "FailedLoginAttemptWithUserName": "ניסיון כניסה שגוי מ{0}", + "FailedLoginAttemptWithUserName": "ניסיון כניסה שגוי דרך {0}", "Favorites": "מועדפים", "Folders": "תיקיות", - "Genres": "ז'אנרים", + "Genres": "ז׳אנרים", "HeaderAlbumArtists": "אמני האלבום", - "HeaderContinueWatching": "המשך לצפות", + "HeaderContinueWatching": "להמשיך לצפות", "HeaderFavoriteAlbums": "אלבומים מועדפים", "HeaderFavoriteArtists": "אמנים מועדפים", "HeaderFavoriteEpisodes": "פרקים מועדפים", @@ -27,14 +27,14 @@ "HeaderRecordingGroups": "קבוצות הקלטה", "HomeVideos": "סרטונים בייתים", "Inherit": "הורש", - "ItemAddedWithName": "{0} הוסף לספרייה", + "ItemAddedWithName": "{0} נוסף לספרייה", "ItemRemovedWithName": "{0} נמחק מהספרייה", "LabelIpAddressValue": "Ip כתובת: {0}", "LabelRunningTimeValue": "משך צפייה: {0}", "Latest": "אחרון", "MessageApplicationUpdated": "שרת הJellyfin עודכן", - "MessageApplicationUpdatedTo": "שרת הJellyfin עודכן לגרסא {0}", - "MessageNamedServerConfigurationUpdatedWithValue": "הגדרת השרת {0} שונתה", + "MessageApplicationUpdatedTo": "שרת ה־Jellyfin עודכן לגרסה {0}", + "MessageNamedServerConfigurationUpdatedWithValue": "סעיף הגדרת השרת {0} עודכן", "MessageServerConfigurationUpdated": "תצורת השרת עודכנה", "MixedContent": "תוכן מעורב", "Movies": "סרטים", @@ -50,7 +50,7 @@ "NotificationOptionAudioPlaybackStopped": "ניגון שמע הופסק", "NotificationOptionCameraImageUploaded": "תמונת מצלמה הועלתה", "NotificationOptionInstallationFailed": "התקנה נכשלה", - "NotificationOptionNewLibraryContent": "תוכן חדש הוסף", + "NotificationOptionNewLibraryContent": "תוכן חדש נוסף", "NotificationOptionPluginError": "כשלון בתוסף", "NotificationOptionPluginInstalled": "התוסף הותקן", "NotificationOptionPluginUninstalled": "התוסף הוסר", @@ -61,41 +61,41 @@ "NotificationOptionVideoPlayback": "ניגון וידאו החל", "NotificationOptionVideoPlaybackStopped": "ניגון וידאו הופסק", "Photos": "תמונות", - "Playlists": "רשימות הפעלה", - "Plugin": "Plugin", + "Playlists": "רשימות נגינה", + "Plugin": "תוסף", "PluginInstalledWithName": "{0} הותקן", "PluginUninstalledWithName": "{0} הוסר", "PluginUpdatedWithName": "{0} עודכן", - "ProviderValue": "Provider: {0}", + "ProviderValue": "ספק: {0}", "ScheduledTaskFailedWithName": "{0} נכשל", "ScheduledTaskStartedWithName": "{0} החל", "ServerNameNeedsToBeRestarted": "{0} דורש הפעלה מחדש", "Shows": "סדרות", "Songs": "שירים", - "StartupEmbyServerIsLoading": "שרת Jellyfin בהליכי טעינה. אנא נסה שנית בעוד זמן קצר.", + "StartupEmbyServerIsLoading": "שרת Jellyfin בהליכי טעינה. נא לנסות שנית בהקדם.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", - "SubtitleDownloadFailureFromForItem": "הורדת כתוביות נכשלה מ-{0} עבור {1}", - "Sync": "סנכרן", - "System": "System", + "SubtitleDownloadFailureFromForItem": "הורדת כתוביות מ־{0} עבור {1} נכשלה", + "Sync": "סנכרון", + "System": "מערכת", "TvShows": "סדרות טלוויזיה", - "User": "User", + "User": "משתמש", "UserCreatedWithName": "המשתמש {0} נוצר", "UserDeletedWithName": "המשתמש {0} הוסר", "UserDownloadingItemWithValues": "{0} מוריד את {1}", "UserLockedOutWithName": "המשתמש {0} ננעל", - "UserOfflineFromDevice": "{0} התנתק מ-{1}", - "UserOnlineFromDevice": "{0} מחובר מ-{1}", + "UserOfflineFromDevice": "{0} התנתק מ־{1}", + "UserOnlineFromDevice": "{0} מחובר מ־{1}", "UserPasswordChangedWithName": "הסיסמה שונתה עבור המשתמש {0}", "UserPolicyUpdatedWithName": "מדיניות המשתמש {0} עודכנה", "UserStartedPlayingItemWithValues": "{0} מנגן את {1} על {2}", "UserStoppedPlayingItemWithValues": "{0} סיים לנגן את {1} על {2}", "ValueHasBeenAddedToLibrary": "{0} התווסף לספריית המדיה שלך", "ValueSpecialEpisodeName": "מיוחד- {0}", - "VersionNumber": "Version {0}", + "VersionNumber": "גרסה {0}", "TaskRefreshLibrary": "סרוק ספריית מדיה", "TaskRefreshChapterImages": "חלץ תמונות פרקים", "TaskCleanCacheDescription": "מחק קבצי מטמון שלא בשימוש המערכת.", - "TaskCleanCache": "נקה תיקיית מטמון", + "TaskCleanCache": "ניקוי תיקיית מטמון", "TasksApplicationCategory": "יישום", "TasksLibraryCategory": "ספרייה", "TasksMaintenanceCategory": "תחזוקה", @@ -103,7 +103,7 @@ "TaskRefreshPeopleDescription": "מעדכן מטא נתונים עבור שחקנים ובמאים בספריית המדיה שלך.", "TaskRefreshPeople": "רענן אנשים", "TaskCleanLogsDescription": "מוחק קבצי יומן בני יותר מ- {0} ימים.", - "TaskCleanLogs": "נקה תיקיית יומן", + "TaskCleanLogs": "ניקוי תיקיית יומן", "TaskRefreshLibraryDescription": "סורק את ספריית המדיה שלך אחר קבצים חדשים ומרענן מטא נתונים.", "TaskRefreshChapterImagesDescription": "יוצר תמונות ממוזערות לסרטונים שיש להם פרקים.", "TasksChannelsCategory": "ערוצי אינטרנט", From fbe8388f823cf8d3f09e0acdecc4286edc91eec4 Mon Sep 17 00:00:00 2001 From: Jothi Prasath <jothiprasath2@gmail.com> Date: Sun, 16 Jul 2023 13:01:19 +0000 Subject: [PATCH 455/858] Translated using Weblate (Tamil) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ta/ --- Emby.Server.Implementations/Localization/Core/ta.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/ta.json b/Emby.Server.Implementations/Localization/Core/ta.json index dfce6bd25c..770624a8df 100644 --- a/Emby.Server.Implementations/Localization/Core/ta.json +++ b/Emby.Server.Implementations/Localization/Core/ta.json @@ -122,5 +122,6 @@ "TaskOptimizeDatabase": "தரவுத்தளத்தை மேம்படுத்தவும்", "TaskKeyframeExtractorDescription": "மிகவும் துல்லியமான HLS பிளேலிஸ்ட்களை உருவாக்க வீடியோ கோப்புகளிலிருந்து கீஃப்ரேம்களைப் பிரித்தெடுக்கிறது. இந்த பணி நீண்ட காலமாக இருக்கலாம்.", "TaskKeyframeExtractor": "கீஃப்ரேம் எக்ஸ்ட்ராக்டர்", - "External": "வெளி" + "External": "வெளி", + "HearingImpaired": "செவித்திறன் குறைபாடுடையவர்" } From b0cd1a4bf409e765f9c771345da0012b15abfc05 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 18 Jul 2023 08:10:08 +0000 Subject: [PATCH 456/858] chore(deps): update dependency prometheus-net to v8.0.1 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 4c236b2586..08a61cc938 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -56,7 +56,7 @@ <PackageVersion Include="PlaylistsNET" Version="1.4.0" /> <PackageVersion Include="prometheus-net.AspNetCore" Version="8.0.0" /> <PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" /> - <PackageVersion Include="prometheus-net" Version="8.0.0" /> + <PackageVersion Include="prometheus-net" Version="8.0.1" /> <PackageVersion Include="Serilog.AspNetCore" Version="7.0.0" /> <PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" /> <PackageVersion Include="Serilog.Settings.Configuration" Version="7.0.0" /> From 4b25d65c02ae27403c75632f33bba2cdd7d0b26f Mon Sep 17 00:00:00 2001 From: cvium <clausvium@gmail.com> Date: Wed, 19 Jul 2023 12:12:58 +0200 Subject: [PATCH 457/858] fix: set memorystream position after copying --- Emby.Dlna/PlayTo/DlnaHttpClient.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Emby.Dlna/PlayTo/DlnaHttpClient.cs b/Emby.Dlna/PlayTo/DlnaHttpClient.cs index 8454c1afd3..220aa1a8dc 100644 --- a/Emby.Dlna/PlayTo/DlnaHttpClient.cs +++ b/Emby.Dlna/PlayTo/DlnaHttpClient.cs @@ -57,6 +57,7 @@ namespace Emby.Dlna.PlayTo response.EnsureSuccessStatusCode(); await using MemoryStream ms = new MemoryStream(); await response.Content.CopyToAsync(ms, cancellationToken).ConfigureAwait(false); + ms.Position = 0; try { return await XDocument.LoadAsync( From 356803edf465d1792aa58d000965ffc533ee90ac Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 19 Jul 2023 15:42:01 +0000 Subject: [PATCH 458/858] chore(deps): update dependency serilog.sinks.graylog to v3.0.2 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 08a61cc938..8cf0ff8797 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -63,7 +63,7 @@ <PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" /> <PackageVersion Include="Serilog.Sinks.Console" Version="4.1.0" /> <PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" /> - <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.0.1" /> + <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.0.2" /> <PackageVersion Include="SerilogAnalyzer" Version="0.15.0" /> <PackageVersion Include="SharpFuzz" Version="2.1.1" /> <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.3" /> From 8243a7911514de29a2e28ea05aac40d0ce0f3951 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 19 Jul 2023 15:42:07 +0000 Subject: [PATCH 459/858] chore(deps): update github/codeql-action action to v2.21.0 --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index e1ee15c74a..0e3c14bba6 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@489225d82a57396c6f426a40e66d461b16b3461d # v2.20.4 + uses: github/codeql-action/init@1813ca74c3faaa3a2da2070b9b8a0b3e7373a0d8 # v2.21.0 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@489225d82a57396c6f426a40e66d461b16b3461d # v2.20.4 + uses: github/codeql-action/autobuild@1813ca74c3faaa3a2da2070b9b8a0b3e7373a0d8 # v2.21.0 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@489225d82a57396c6f426a40e66d461b16b3461d # v2.20.4 + uses: github/codeql-action/analyze@1813ca74c3faaa3a2da2070b9b8a0b3e7373a0d8 # v2.21.0 From 48eb6f655b3b8a1743846c941c7031d14ff24a67 Mon Sep 17 00:00:00 2001 From: Christoph Landsdorf <Blackskyliner@users.noreply.github.com> Date: Thu, 20 Jul 2023 10:10:02 +0000 Subject: [PATCH 460/858] Change: Update parental code count for DE in test --- .../Localization/LocalizationManagerTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs index 7fabe99045..09e4709da3 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs @@ -100,7 +100,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization await localizationManager.LoadAll(); var ratings = localizationManager.GetParentalRatings().ToList(); - Assert.Equal(19, ratings.Count); + Assert.Equal(24, ratings.Count); var fsk = ratings.FirstOrDefault(x => x.Name.Equals("FSK-12", StringComparison.Ordinal)); Assert.NotNull(fsk); From b8e5afbc10150c852ce80a960bb902b0e08c30fe Mon Sep 17 00:00:00 2001 From: tallbl0nde <40382856+tallbl0nde@users.noreply.github.com> Date: Fri, 21 Jul 2023 19:16:01 +0930 Subject: [PATCH 461/858] Enable recursive query in BaseFolderImageProvider Fixes album art not being extracted for multi-disc albums --- CONTRIBUTORS.md | 1 + Emby.Server.Implementations/Images/BaseFolderImageProvider.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index dfb61df0a1..d5a87d2692 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -166,6 +166,7 @@ - [RealGreenDragon](https://github.com/RealGreenDragon) - [ipitio](https://github.com/ipitio) - [TheTyrius](https://github.com/TheTyrius) + - [tallbl0nde](https://github.com/tallbl0nde) # Emby Contributors diff --git a/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs index 84c21931c3..539d4a63af 100644 --- a/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs +++ b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs @@ -31,6 +31,7 @@ namespace Emby.Server.Implementations.Images return _libraryManager.GetItemList(new InternalItemsQuery { Parent = item, + Recursive = true, DtoOptions = new DtoOptions(true), ImageTypes = new ImageType[] { ImageType.Primary }, OrderBy = new (string, SortOrder)[] From 82d79d2dff3df5032f66c75713445f48911c811d Mon Sep 17 00:00:00 2001 From: MBR#0001 <mbr@mbr.pw> Date: Sun, 23 Jul 2023 21:11:15 +0200 Subject: [PATCH 462/858] Add support for more remote subtitle info --- MediaBrowser.Model/Providers/RemoteSubtitleInfo.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/MediaBrowser.Model/Providers/RemoteSubtitleInfo.cs b/MediaBrowser.Model/Providers/RemoteSubtitleInfo.cs index a8d88d8a1b..9f9c922a7c 100644 --- a/MediaBrowser.Model/Providers/RemoteSubtitleInfo.cs +++ b/MediaBrowser.Model/Providers/RemoteSubtitleInfo.cs @@ -25,8 +25,18 @@ namespace MediaBrowser.Model.Providers public float? CommunityRating { get; set; } + public float? FrameRate { get; set; } + public int? DownloadCount { get; set; } public bool? IsHashMatch { get; set; } + + public bool? AiTranslated { get; set; } + + public bool? MachineTranslated { get; set; } + + public bool? Forced { get; set; } + + public bool? HearingImpaired { get; set; } } } From 0bf92186a577c14922be5952e4c0994d6d8d5d4a Mon Sep 17 00:00:00 2001 From: Hugues Larrive <hlarrive@pm.me> Date: Tue, 14 Dec 2021 01:07:08 +0100 Subject: [PATCH 463/858] Add program directories to JELLYFIN_ARGS for sysvinit compatibility --- debian/conf/jellyfin | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/conf/jellyfin b/debian/conf/jellyfin index 9129967559..aec1d4d103 100644 --- a/debian/conf/jellyfin +++ b/debian/conf/jellyfin @@ -47,4 +47,4 @@ JELLYFIN_ADDITIONAL_OPTS="" # Application username JELLYFIN_USER="jellyfin" # Full application command -JELLYFIN_ARGS="$JELLYFIN_WEB_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT $JELLFIN_ADDITIONAL_OPTS" +JELLYFIN_ARGS="$JELLYFIN_WEB_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT $JELLFIN_ADDITIONAL_OPTS --datadir $JELLYFIN_DATA_DIR --configdir $JELLYFIN_CONFIG_DIR --logdir $JELLYFIN_LOG_DIR --cachedir $JELLYFIN_CACHE_DIR" From 2b5774ccf3bd61fdd7b807035002b70e0c41ba06 Mon Sep 17 00:00:00 2001 From: scatter-dev <christian@christianlegge.dev> Date: Fri, 28 Jul 2023 09:54:28 -0400 Subject: [PATCH 464/858] add parsing for date with spaces, fix for underscores --- Emby.Naming/Common/NamingOptions.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index a069da1022..0872024f54 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -318,22 +318,24 @@ namespace Emby.Naming.Common new EpisodeExpression(@"[\._ -]()[Ee][Pp]_?([0-9]+)([^\\/]*)$"), // <!-- foo.E01., foo.e01. --> new EpisodeExpression(@"[^\\/]*?()\.?[Ee]([0-9]+)\.([^\\/]*)$"), - new EpisodeExpression("(?<year>[0-9]{4})[\\.-](?<month>[0-9]{2})[\\.-](?<day>[0-9]{2})", true) + new EpisodeExpression("(?<year>[0-9]{4})[\\.-_ ](?<month>[0-9]{2})[\\.-_ ](?<day>[0-9]{2})", true) { DateTimeFormats = new[] { "yyyy.MM.dd", "yyyy-MM-dd", - "yyyy_MM_dd" + "yyyy_MM_dd", + "yyyy MM dd" } }, - new EpisodeExpression(@"(?<day>[0-9]{2})[.-](?<month>[0-9]{2})[.-](?<year>[0-9]{4})", true) + new EpisodeExpression(@"(?<day>[0-9]{2})[.-_ ](?<month>[0-9]{2})[.-_ ](?<year>[0-9]{4})", true) { DateTimeFormats = new[] { "dd.MM.yyyy", "dd-MM-yyyy", - "dd_MM_yyyy" + "dd_MM_yyyy", + "dd MM yyyy" } }, From 14a762b2f1d5e9e6c66f86ab91e6684c6f3ff8ba Mon Sep 17 00:00:00 2001 From: scatter-dev <christian@christianlegge.dev> Date: Fri, 28 Jul 2023 16:06:55 -0400 Subject: [PATCH 465/858] added test case, fixed regexes --- Emby.Naming/Common/NamingOptions.cs | 4 ++-- tests/Jellyfin.Naming.Tests/TV/DailyEpisodeTests.cs | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index 0872024f54..5cf0d90832 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -318,7 +318,7 @@ namespace Emby.Naming.Common new EpisodeExpression(@"[\._ -]()[Ee][Pp]_?([0-9]+)([^\\/]*)$"), // <!-- foo.E01., foo.e01. --> new EpisodeExpression(@"[^\\/]*?()\.?[Ee]([0-9]+)\.([^\\/]*)$"), - new EpisodeExpression("(?<year>[0-9]{4})[\\.-_ ](?<month>[0-9]{2})[\\.-_ ](?<day>[0-9]{2})", true) + new EpisodeExpression(@"(?<year>[0-9]{4})[\.\-_ ](?<month>[0-9]{2})[\.\-_ ](?<day>[0-9]{2})", true) { DateTimeFormats = new[] { @@ -328,7 +328,7 @@ namespace Emby.Naming.Common "yyyy MM dd" } }, - new EpisodeExpression(@"(?<day>[0-9]{2})[.-_ ](?<month>[0-9]{2})[.-_ ](?<year>[0-9]{4})", true) + new EpisodeExpression(@"(?<day>[0-9]{2})[\.\-_ ](?<month>[0-9]{2})[\.\-_ ](?<year>[0-9]{4})", true) { DateTimeFormats = new[] { diff --git a/tests/Jellyfin.Naming.Tests/TV/DailyEpisodeTests.cs b/tests/Jellyfin.Naming.Tests/TV/DailyEpisodeTests.cs index 72052a23c1..d0d3d82928 100644 --- a/tests/Jellyfin.Naming.Tests/TV/DailyEpisodeTests.cs +++ b/tests/Jellyfin.Naming.Tests/TV/DailyEpisodeTests.cs @@ -13,6 +13,7 @@ namespace Jellyfin.Naming.Tests.TV [InlineData(@"/server/anything_1996-11-14.mp4", "anything", 1996, 11, 14)] [InlineData(@"/server/james.corden.2017.04.20.anne.hathaway.720p.hdtv.x264-crooks.mkv", "james.corden", 2017, 04, 20)] [InlineData(@"/server/ABC News 2018_03_24_19_00_00.mkv", "ABC News", 2018, 03, 24)] + [InlineData(@"/server/Jeopardy 2023 07 14 HDTV x264 AC3.mkv", "Jeopardy", 2023, 07, 14)] // TODO: [InlineData(@"/server/anything_14.11.1996.mp4", "anything", 1996, 11, 14)] // TODO: [InlineData(@"/server/A Daily Show - (2015-01-15) - Episode Name - [720p].mkv", "A Daily Show", 2015, 01, 15)] // TODO: [InlineData(@"/server/Last Man Standing_KTLADT_2018_05_25_01_28_00.wtv", "Last Man Standing", 2018, 05, 25)] From f20856411e28f852c376db96e25d573113771bc0 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Fri, 28 Jul 2023 07:16:45 +0200 Subject: [PATCH 466/858] Fix format normalizer for multiple input formats --- .../Probing/ProbeResultNormalizer.cs | 55 +++++++++++++------ 1 file changed, 38 insertions(+), 17 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 7d655240b7..aeb08cea35 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -1,5 +1,4 @@ #nullable disable -#pragma warning disable CS1591 using System; using System.Collections.Generic; @@ -20,6 +19,9 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.MediaEncoding.Probing { + /// <summary> + /// Class responsible for normalizing FFprobe output. + /// </summary> public class ProbeResultNormalizer { // When extracting subtitles, the maximum length to consider (to avoid invalid filenames) @@ -36,6 +38,11 @@ namespace MediaBrowser.MediaEncoding.Probing private string[] _splitWhiteList; + /// <summary> + /// Initializes a new instance of the <see cref="ProbeResultNormalizer"/> class. + /// </summary> + /// <param name="logger">The <see cref="ILogger{ProbeResultNormalizer}"/> for use with the <see cref="ProbeResultNormalizer"/> instance.</param> + /// <param name="localization">The <see cref="ILocalizationManager"/> for use with the <see cref="ProbeResultNormalizer"/> instance.</param> public ProbeResultNormalizer(ILogger logger, ILocalizationManager localization) { _logger = logger; @@ -73,6 +80,15 @@ namespace MediaBrowser.MediaEncoding.Probing "Smith/Kotzen", }; + /// <summary> + /// Transforms a FFprobe response into its <see cref="MediaInfo"/> equivalent. + /// </summary> + /// <param name="data">The <see cref="InternalMediaInfoResult"/>.</param> + /// <param name="videoType">The <see cref="VideoType"/>.</param> + /// <param name="isAudio">A boolean indicating whether the media is audio.</param> + /// <param name="path">Path to media file.</param> + /// <param name="protocol">Path media protocol.</param> + /// <returns>The <see cref="MediaInfo"/>.</returns> public MediaInfo GetMediaInfo(InternalMediaInfoResult data, VideoType? videoType, bool isAudio, string path, MediaProtocol protocol) { var info = new MediaInfo @@ -252,25 +268,30 @@ namespace MediaBrowser.MediaEncoding.Probing return null; } - // Handle MPEG-1 container - if (string.Equals(format, "mpegvideo", StringComparison.OrdinalIgnoreCase)) + // Input can be a list of multiple, comma-delimited formats - each of them needs to be checked + var splitFormat = format.Split(','); + for (var i = 0; i < splitFormat.Length; i++) { - return "mpeg"; + // Handle MPEG-1 container + if (string.Equals(splitFormat[i], "mpegvideo", StringComparison.OrdinalIgnoreCase)) + { + splitFormat[i] = "mpeg"; + } + + // Handle MPEG-2 container + else if (string.Equals(splitFormat[i], "mpeg", StringComparison.OrdinalIgnoreCase)) + { + splitFormat[i] = "ts"; + } + + // Handle matroska container + else if (string.Equals(splitFormat[i], "matroska", StringComparison.OrdinalIgnoreCase)) + { + splitFormat[i] = "mkv"; + } } - // Handle MPEG-2 container - if (string.Equals(format, "mpeg", StringComparison.OrdinalIgnoreCase)) - { - return "ts"; - } - - // Handle matroska container - if (string.Equals(format, "matroska", StringComparison.OrdinalIgnoreCase)) - { - return "mkv"; - } - - return format; + return string.Join(',', splitFormat); } private int? GetEstimatedAudioBitrate(string codec, int? channels) From c5783b42616a1cb8875aad6354ee48c91290a3f0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 28 Jul 2023 22:47:10 -0600 Subject: [PATCH 467/858] chore(deps): update dotnet monorepo to v7.0.9 (#9989) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Cody Robibero <cody@robibe.ro> --- Directory.Packages.props | 16 ++++++++-------- deployment/Dockerfile.centos.amd64 | 2 +- deployment/Dockerfile.fedora.amd64 | 2 +- deployment/Dockerfile.ubuntu.amd64 | 2 +- deployment/Dockerfile.ubuntu.arm64 | 2 +- deployment/Dockerfile.ubuntu.armhf | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 8cf0ff8797..d207bdff3d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -23,14 +23,14 @@ <PackageVersion Include="libse" Version="3.6.13" /> <PackageVersion Include="LrcParser" Version="2023.524.0" /> <PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" /> - <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.8" /> + <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.9" /> <PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" /> - <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.8" /> + <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.9" /> <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.8" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.8" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.8" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.8" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.9" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.9" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.9" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.9" /> <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" /> @@ -39,8 +39,8 @@ <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" /> - <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.8" /> - <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.8" /> + <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.9" /> + <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.9" /> <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Http" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" /> diff --git a/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64 index 771675519d..da986a07ec 100644 --- a/deployment/Dockerfile.centos.amd64 +++ b/deployment/Dockerfile.centos.amd64 @@ -13,7 +13,7 @@ RUN yum update -yq \ && yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget # Install DotNET SDK -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/87a55ae3-917d-449e-a4e8-776f82976e91/03380e598c326c2f9465d262c6a88c45/dotnet-sdk-7.0.305-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/0be7a87e-3a3f-4500-8301-49ccd6f24887/e9e36f35dbaf6625fec3e18f5c2b613f/dotnet-sdk-7.0.306-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.fedora.amd64 b/deployment/Dockerfile.fedora.amd64 index c552f06b0b..09f93d41bf 100644 --- a/deployment/Dockerfile.fedora.amd64 +++ b/deployment/Dockerfile.fedora.amd64 @@ -12,7 +12,7 @@ RUN dnf update -yq \ && dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd wget make # Install DotNET SDK -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/87a55ae3-917d-449e-a4e8-776f82976e91/03380e598c326c2f9465d262c6a88c45/dotnet-sdk-7.0.305-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/0be7a87e-3a3f-4500-8301-49ccd6f24887/e9e36f35dbaf6625fec3e18f5c2b613f/dotnet-sdk-7.0.306-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64 index 30100d20d9..9910773da5 100644 --- a/deployment/Dockerfile.ubuntu.amd64 +++ b/deployment/Dockerfile.ubuntu.amd64 @@ -17,7 +17,7 @@ RUN apt-get update -yqq \ libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/87a55ae3-917d-449e-a4e8-776f82976e91/03380e598c326c2f9465d262c6a88c45/dotnet-sdk-7.0.305-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/0be7a87e-3a3f-4500-8301-49ccd6f24887/e9e36f35dbaf6625fec3e18f5c2b613f/dotnet-sdk-7.0.306-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.arm64 b/deployment/Dockerfile.ubuntu.arm64 index bac2adfafb..aa69b27f49 100644 --- a/deployment/Dockerfile.ubuntu.arm64 +++ b/deployment/Dockerfile.ubuntu.arm64 @@ -16,7 +16,7 @@ RUN apt-get update -yqq \ mmv build-essential lsb-release # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/87a55ae3-917d-449e-a4e8-776f82976e91/03380e598c326c2f9465d262c6a88c45/dotnet-sdk-7.0.305-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/0be7a87e-3a3f-4500-8301-49ccd6f24887/e9e36f35dbaf6625fec3e18f5c2b613f/dotnet-sdk-7.0.306-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.armhf b/deployment/Dockerfile.ubuntu.armhf index 37a1ed5ff3..bb8597b41f 100644 --- a/deployment/Dockerfile.ubuntu.armhf +++ b/deployment/Dockerfile.ubuntu.armhf @@ -16,7 +16,7 @@ RUN apt-get update -yqq \ mmv build-essential lsb-release # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/87a55ae3-917d-449e-a4e8-776f82976e91/03380e598c326c2f9465d262c6a88c45/dotnet-sdk-7.0.305-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/0be7a87e-3a3f-4500-8301-49ccd6f24887/e9e36f35dbaf6625fec3e18f5c2b613f/dotnet-sdk-7.0.306-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet From 11b3ae2a03b8c3f8489502942cd392f00a45005d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 28 Jul 2023 22:47:20 -0600 Subject: [PATCH 468/858] chore(deps): update github/codeql-action action to v2.21.2 (#10043) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 0e3c14bba6..fa32c1c0e9 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@1813ca74c3faaa3a2da2070b9b8a0b3e7373a0d8 # v2.21.0 + uses: github/codeql-action/init@0ba4244466797eb048eb91a6cd43d5c03ca8bd05 # v2.21.2 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@1813ca74c3faaa3a2da2070b9b8a0b3e7373a0d8 # v2.21.0 + uses: github/codeql-action/autobuild@0ba4244466797eb048eb91a6cd43d5c03ca8bd05 # v2.21.2 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@1813ca74c3faaa3a2da2070b9b8a0b3e7373a0d8 # v2.21.0 + uses: github/codeql-action/analyze@0ba4244466797eb048eb91a6cd43d5c03ca8bd05 # v2.21.2 From b355ee43275139b0208c29b92f9f97b9a340cc17 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 29 Jul 2023 04:48:13 +0000 Subject: [PATCH 469/858] chore(deps): update dependency prometheus-net.aspnetcore to v8.0.1 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index d207bdff3d..de347f3a06 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -54,7 +54,7 @@ <PackageVersion Include="NEbml" Version="0.11.0" /> <PackageVersion Include="Newtonsoft.Json" Version="13.0.3" /> <PackageVersion Include="PlaylistsNET" Version="1.4.0" /> - <PackageVersion Include="prometheus-net.AspNetCore" Version="8.0.0" /> + <PackageVersion Include="prometheus-net.AspNetCore" Version="8.0.1" /> <PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" /> <PackageVersion Include="prometheus-net" Version="8.0.1" /> <PackageVersion Include="Serilog.AspNetCore" Version="7.0.0" /> From 048cb208b29f903fefb4b29d6627d17dc7b7920c Mon Sep 17 00:00:00 2001 From: Shadowghost <Shadowghost@users.noreply.github.com> Date: Sat, 29 Jul 2023 06:55:54 +0200 Subject: [PATCH 470/858] Fix Australian parental rating system (#10006) --- Emby.Server.Implementations/Localization/Ratings/au.csv | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Ratings/au.csv b/Emby.Server.Implementations/Localization/Ratings/au.csv index 4ab808ae9a..6881259172 100644 --- a/Emby.Server.Implementations/Localization/Ratings/au.csv +++ b/Emby.Server.Implementations/Localization/Ratings/au.csv @@ -4,10 +4,14 @@ G,0 M,15 MA,15 MA15+,15 +MA 15+,15 PG,16 16+,16 R,18 R18+,18 -X18+,18 +R 18+,18 18+,18 +X18+,1000 +X 18+,1000 X,1000 +RC,1001 From 4bb17039d70683e8a159db92823fed6d992e2fe4 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Sat, 29 Jul 2023 13:50:55 +0200 Subject: [PATCH 471/858] Apply review suggestions --- Emby.Server.Implementations/Session/SessionManager.cs | 4 ++-- .../Consumers/Security/AuthenticationFailedLogger.cs | 9 ++++----- .../Security/AuthenticationSucceededLogger.cs | 10 +++++----- .../Events/EventingServiceCollectionExtensions.cs | 4 ++-- .../Authentication/AuthenticationRequestEventArgs.cs | 2 +- .../Authentication/AuthenticationResultEventArgs.cs | 3 ++- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index f26cc56636..03ff96b19a 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1463,7 +1463,7 @@ namespace Emby.Server.Implementations.Session if (user is null) { - await _eventManager.PublishAsync(new GenericEventArgs<AuthenticationRequestEventArgs>(new AuthenticationRequestEventArgs(request))).ConfigureAwait(false); + await _eventManager.PublishAsync(new AuthenticationRequestEventArgs(request)).ConfigureAwait(false); throw new AuthenticationException("Invalid username or password entered."); } @@ -1499,7 +1499,7 @@ namespace Emby.Server.Implementations.Session ServerId = _appHost.SystemId }; - await _eventManager.PublishAsync(new GenericEventArgs<AuthenticationResultEventArgs>(new AuthenticationResultEventArgs(returnResult))).ConfigureAwait(false); + await _eventManager.PublishAsync(new AuthenticationResultEventArgs(returnResult)).ConfigureAwait(false); return returnResult; } diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationFailedLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationFailedLogger.cs index f4835a585f..b5f18d9834 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationFailedLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationFailedLogger.cs @@ -2,7 +2,6 @@ using System.Globalization; using System.Threading.Tasks; using Jellyfin.Data.Entities; -using Jellyfin.Data.Events; using MediaBrowser.Controller.Events; using MediaBrowser.Controller.Events.Authentication; using MediaBrowser.Model.Activity; @@ -14,7 +13,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Security /// <summary> /// Creates an entry in the activity log when there is a failed login attempt. /// </summary> - public class AuthenticationFailedLogger : IEventConsumer<GenericEventArgs<AuthenticationRequestEventArgs>> + public class AuthenticationFailedLogger : IEventConsumer<AuthenticationRequestEventArgs> { private readonly ILocalizationManager _localizationManager; private readonly IActivityManager _activityManager; @@ -31,13 +30,13 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Security } /// <inheritdoc /> - public async Task OnEvent(GenericEventArgs<AuthenticationRequestEventArgs> eventArgs) + public async Task OnEvent(AuthenticationRequestEventArgs eventArgs) { await _activityManager.CreateAsync(new ActivityLog( string.Format( CultureInfo.InvariantCulture, _localizationManager.GetLocalizedString("FailedLoginAttemptWithUserName"), - eventArgs.Argument.Username), + eventArgs.Username), "AuthenticationFailed", Guid.Empty) { @@ -45,7 +44,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Security ShortOverview = string.Format( CultureInfo.InvariantCulture, _localizationManager.GetLocalizedString("LabelIpAddressValue"), - eventArgs.Argument.RemoteEndPoint), + eventArgs.RemoteEndPoint), }).ConfigureAwait(false); } } diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs index fe2737bafc..2ee5b4e88d 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs @@ -12,7 +12,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Security /// <summary> /// Creates an entry in the activity log when there is a successful login attempt. /// </summary> - public class AuthenticationSucceededLogger : IEventConsumer<GenericEventArgs<AuthenticationResultEventArgs>> + public class AuthenticationSucceededLogger : IEventConsumer<AuthenticationResultEventArgs> { private readonly ILocalizationManager _localizationManager; private readonly IActivityManager _activityManager; @@ -29,20 +29,20 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Security } /// <inheritdoc /> - public async Task OnEvent(GenericEventArgs<AuthenticationResultEventArgs> eventArgs) + public async Task OnEvent(AuthenticationResultEventArgs eventArgs) { await _activityManager.CreateAsync(new ActivityLog( string.Format( CultureInfo.InvariantCulture, _localizationManager.GetLocalizedString("AuthenticationSucceededWithUserName"), - eventArgs.Argument.User.Name), + eventArgs.User.Name), "AuthenticationSucceeded", - eventArgs.Argument.User.Id) + eventArgs.User.Id) { ShortOverview = string.Format( CultureInfo.InvariantCulture, _localizationManager.GetLocalizedString("LabelIpAddressValue"), - eventArgs.Argument.SessionInfo?.RemoteEndPoint), + eventArgs.SessionInfo?.RemoteEndPoint), }).ConfigureAwait(false); } } diff --git a/Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs b/Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs index 93ab15e24b..9a473de52d 100644 --- a/Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs +++ b/Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs @@ -34,8 +34,8 @@ namespace Jellyfin.Server.Implementations.Events collection.AddScoped<IEventConsumer<SubtitleDownloadFailureEventArgs>, SubtitleDownloadFailureLogger>(); // Security consumers - collection.AddScoped<IEventConsumer<GenericEventArgs<AuthenticationRequestEventArgs>>, AuthenticationFailedLogger>(); - collection.AddScoped<IEventConsumer<GenericEventArgs<AuthenticationResultEventArgs>>, AuthenticationSucceededLogger>(); + collection.AddScoped<IEventConsumer<AuthenticationRequestEventArgs>, AuthenticationFailedLogger>(); + collection.AddScoped<IEventConsumer<AuthenticationResultEventArgs>, AuthenticationSucceededLogger>(); // Session consumers collection.AddScoped<IEventConsumer<PlaybackStartEventArgs>, PlaybackStartLogger>(); diff --git a/MediaBrowser.Controller/Events/Authentication/AuthenticationRequestEventArgs.cs b/MediaBrowser.Controller/Events/Authentication/AuthenticationRequestEventArgs.cs index c0d8a277bd..2143c69986 100644 --- a/MediaBrowser.Controller/Events/Authentication/AuthenticationRequestEventArgs.cs +++ b/MediaBrowser.Controller/Events/Authentication/AuthenticationRequestEventArgs.cs @@ -6,7 +6,7 @@ namespace MediaBrowser.Controller.Events.Authentication; /// <summary> /// A class representing an authentication result event. /// </summary> -public class AuthenticationRequestEventArgs +public class AuthenticationRequestEventArgs : EventArgs { /// <summary> /// Initializes a new instance of the <see cref="AuthenticationRequestEventArgs"/> class. diff --git a/MediaBrowser.Controller/Events/Authentication/AuthenticationResultEventArgs.cs b/MediaBrowser.Controller/Events/Authentication/AuthenticationResultEventArgs.cs index 5564fa1cef..357ef9406d 100644 --- a/MediaBrowser.Controller/Events/Authentication/AuthenticationResultEventArgs.cs +++ b/MediaBrowser.Controller/Events/Authentication/AuthenticationResultEventArgs.cs @@ -1,3 +1,4 @@ +using System; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; @@ -7,7 +8,7 @@ namespace MediaBrowser.Controller.Events.Authentication; /// <summary> /// A class representing an authentication result event. /// </summary> -public class AuthenticationResultEventArgs +public class AuthenticationResultEventArgs : EventArgs { /// <summary> /// Initializes a new instance of the <see cref="AuthenticationResultEventArgs"/> class. From 9564248b10829ed21c62b19aa84e50806d35fd59 Mon Sep 17 00:00:00 2001 From: MBR-0001 <55142207+MBR-0001@users.noreply.github.com> Date: Sat, 29 Jul 2023 14:52:35 +0200 Subject: [PATCH 472/858] Add ability to upload (and save) SDH subtitles (#10036) --- Jellyfin.Api/Controllers/SubtitleController.cs | 5 +++-- Jellyfin.Api/Models/SubtitleDtos/UploadSubtitleDto.cs | 6 ++++++ MediaBrowser.Controller/Subtitles/SubtitleResponse.cs | 2 ++ MediaBrowser.Providers/Subtitles/SubtitleManager.cs | 5 +++++ 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index b3e9d62972..7d02550b68 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -90,7 +90,7 @@ public class SubtitleController : BaseJellyfinApiController [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult<Task> DeleteSubtitle( + public async Task<ActionResult> DeleteSubtitle( [FromRoute, Required] Guid itemId, [FromRoute, Required] int index) { @@ -101,7 +101,7 @@ public class SubtitleController : BaseJellyfinApiController return NotFound(); } - _subtitleManager.DeleteSubtitles(item, index); + await _subtitleManager.DeleteSubtitles(item, index).ConfigureAwait(false); return NoContent(); } @@ -416,6 +416,7 @@ public class SubtitleController : BaseJellyfinApiController Format = body.Format, Language = body.Language, IsForced = body.IsForced, + IsHearingImpaired = body.IsHearingImpaired, Stream = memoryStream }).ConfigureAwait(false); _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High); diff --git a/Jellyfin.Api/Models/SubtitleDtos/UploadSubtitleDto.cs b/Jellyfin.Api/Models/SubtitleDtos/UploadSubtitleDto.cs index 3c903ea6b5..2c45e704bc 100644 --- a/Jellyfin.Api/Models/SubtitleDtos/UploadSubtitleDto.cs +++ b/Jellyfin.Api/Models/SubtitleDtos/UploadSubtitleDto.cs @@ -25,6 +25,12 @@ public class UploadSubtitleDto [Required] public bool IsForced { get; set; } + /// <summary> + /// Gets or sets a value indicating whether the subtitle is for hearing impaired. + /// </summary> + [Required] + public bool IsHearingImpaired { get; set; } + /// <summary> /// Gets or sets the subtitle data. /// </summary> diff --git a/MediaBrowser.Controller/Subtitles/SubtitleResponse.cs b/MediaBrowser.Controller/Subtitles/SubtitleResponse.cs index 85b3e6fbd7..51c29c7a24 100644 --- a/MediaBrowser.Controller/Subtitles/SubtitleResponse.cs +++ b/MediaBrowser.Controller/Subtitles/SubtitleResponse.cs @@ -14,6 +14,8 @@ namespace MediaBrowser.Controller.Subtitles public bool IsForced { get; set; } + public bool IsHearingImpaired { get; set; } + public Stream Stream { get; set; } } } diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs index 0c01c50317..87fd2a3cda 100644 --- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs +++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs @@ -200,6 +200,11 @@ namespace MediaBrowser.Providers.Subtitles saveFileName += ".forced"; } + if (response.IsHearingImpaired) + { + saveFileName += ".sdh"; + } + saveFileName += "." + response.Format.ToLowerInvariant(); if (saveInMediaFolder) From 148c86ee0db643b5278dcbd68133bcc64a2f617e Mon Sep 17 00:00:00 2001 From: Nyanmisaka <nst799610810@gmail.com> Date: Sat, 29 Jul 2023 20:52:58 +0800 Subject: [PATCH 473/858] Only disable the global_header for AMD HEVC encoder (#10045) --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 7507136941..a702e1003a 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -1457,8 +1457,8 @@ namespace MediaBrowser.Controller.MediaEncoding args += keyFrameArg + gopArg; } - // global_header produced by AMD VA-API encoder causes non-playable fMP4 on iOS - if (codec.Contains("vaapi", StringComparison.OrdinalIgnoreCase) + // global_header produced by AMD HEVC VA-API encoder causes non-playable fMP4 on iOS + if (string.Equals(codec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase) && _mediaEncoder.IsVaapiDeviceAmd) { args += " -flags:v -global_header"; From 5677566a41638f4c62f107f3540363457c099019 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Sat, 29 Jul 2023 21:35:38 +0200 Subject: [PATCH 474/858] Enable nullable for more files --- Emby.Dlna/Didl/DidlBuilder.cs | 64 +++++----- Emby.Dlna/PlayTo/Device.cs | 116 +++++++----------- Emby.Dlna/PlayTo/PlayToController.cs | 4 +- Emby.Dlna/PlayTo/PlayToManager.cs | 10 +- .../Plugins/PluginManager.cs | 2 +- .../Drawing/ImageProcessorExtensions.cs | 6 +- MediaBrowser.Model/Dlna/DeviceProfile.cs | 8 +- MediaBrowser.Model/Dlna/StreamBuilder.cs | 31 ++--- MediaBrowser.Model/Dlna/StreamInfo.cs | 90 ++++++++------ .../Dlna/StreamBuilderTests.cs | 20 +-- 10 files changed, 161 insertions(+), 190 deletions(-) diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs index f668dc829a..5ed982876d 100644 --- a/Emby.Dlna/Didl/DidlBuilder.cs +++ b/Emby.Dlna/Didl/DidlBuilder.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -45,8 +43,8 @@ namespace Emby.Dlna.Didl private readonly DeviceProfile _profile; private readonly IImageProcessor _imageProcessor; private readonly string _serverAddress; - private readonly string _accessToken; - private readonly User _user; + private readonly string? _accessToken; + private readonly User? _user; private readonly IUserDataManager _userDataManager; private readonly ILocalizationManager _localization; private readonly IMediaSourceManager _mediaSourceManager; @@ -56,10 +54,10 @@ namespace Emby.Dlna.Didl public DidlBuilder( DeviceProfile profile, - User user, + User? user, IImageProcessor imageProcessor, string serverAddress, - string accessToken, + string? accessToken, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, @@ -85,7 +83,7 @@ namespace Emby.Dlna.Didl return url + "&dlnaheaders=true"; } - public string GetItemDidl(BaseItem item, User user, BaseItem context, string deviceId, Filter filter, StreamInfo streamInfo) + public string GetItemDidl(BaseItem item, User? user, BaseItem? context, string deviceId, Filter filter, StreamInfo streamInfo) { var settings = new XmlWriterSettings { @@ -140,12 +138,12 @@ namespace Emby.Dlna.Didl public void WriteItemElement( XmlWriter writer, BaseItem item, - User user, - BaseItem context, + User? user, + BaseItem? context, StubType? contextStubType, string deviceId, Filter filter, - StreamInfo streamInfo = null) + StreamInfo? streamInfo = null) { var clientId = GetClientId(item, null); @@ -190,7 +188,7 @@ namespace Emby.Dlna.Didl writer.WriteFullEndElement(); } - private void AddVideoResource(XmlWriter writer, BaseItem video, string deviceId, Filter filter, StreamInfo streamInfo = null) + private void AddVideoResource(XmlWriter writer, BaseItem video, string deviceId, Filter filter, StreamInfo? streamInfo = null) { if (streamInfo is null) { @@ -203,7 +201,7 @@ namespace Emby.Dlna.Didl Profile = _profile, DeviceId = deviceId, MaxBitrate = _profile.MaxStreamingBitrate - }); + }) ?? throw new InvalidOperationException("No optimal video stream found"); } var targetWidth = streamInfo.TargetWidth; @@ -315,7 +313,7 @@ namespace Emby.Dlna.Didl var mediaSource = streamInfo.MediaSource; - if (mediaSource.RunTimeTicks.HasValue) + if (mediaSource?.RunTimeTicks.HasValue == true) { writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture)); } @@ -410,7 +408,7 @@ namespace Emby.Dlna.Didl writer.WriteFullEndElement(); } - private string GetDisplayName(BaseItem item, StubType? itemStubType, BaseItem context) + private string GetDisplayName(BaseItem item, StubType? itemStubType, BaseItem? context) { if (itemStubType.HasValue) { @@ -452,7 +450,7 @@ namespace Emby.Dlna.Didl /// <param name="episode">The episode.</param> /// <param name="context">Current context.</param> /// <returns>Formatted name of the episode.</returns> - private string GetEpisodeDisplayName(Episode episode, BaseItem context) + private string GetEpisodeDisplayName(Episode episode, BaseItem? context) { string[] components; @@ -530,7 +528,7 @@ namespace Emby.Dlna.Didl private bool NotNullOrWhiteSpace(string s) => !string.IsNullOrWhiteSpace(s); - private void AddAudioResource(XmlWriter writer, BaseItem audio, string deviceId, Filter filter, StreamInfo streamInfo = null) + private void AddAudioResource(XmlWriter writer, BaseItem audio, string deviceId, Filter filter, StreamInfo? streamInfo = null) { writer.WriteStartElement(string.Empty, "res", NsDidl); @@ -544,14 +542,14 @@ namespace Emby.Dlna.Didl MediaSources = sources.ToArray(), Profile = _profile, DeviceId = deviceId - }); + }) ?? throw new InvalidOperationException("No optimal audio stream found"); } var url = NormalizeDlnaMediaUrl(streamInfo.ToUrl(_serverAddress, _accessToken)); var mediaSource = streamInfo.MediaSource; - if (mediaSource.RunTimeTicks.HasValue) + if (mediaSource?.RunTimeTicks is not null) { writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture)); } @@ -634,7 +632,7 @@ namespace Emby.Dlna.Didl // Samsung sometimes uses 1 as root || string.Equals(id, "1", StringComparison.OrdinalIgnoreCase); - public void WriteFolderElement(XmlWriter writer, BaseItem folder, StubType? stubType, BaseItem context, int childCount, Filter filter, string requestedId = null) + public void WriteFolderElement(XmlWriter writer, BaseItem folder, StubType? stubType, BaseItem context, int childCount, Filter filter, string? requestedId = null) { writer.WriteStartElement(string.Empty, "container", NsDidl); @@ -678,14 +676,14 @@ namespace Emby.Dlna.Didl writer.WriteFullEndElement(); } - private void AddSamsungBookmarkInfo(BaseItem item, User user, XmlWriter writer, StreamInfo streamInfo) + private void AddSamsungBookmarkInfo(BaseItem item, User? user, XmlWriter writer, StreamInfo? streamInfo) { if (!item.SupportsPositionTicksResume || item is Folder) { return; } - XmlAttribute secAttribute = null; + XmlAttribute? secAttribute = null; foreach (var attribute in _profile.XmlRootAttributes) { if (string.Equals(attribute.Name, "xmlns:sec", StringComparison.OrdinalIgnoreCase)) @@ -695,8 +693,8 @@ namespace Emby.Dlna.Didl } } - // Not a samsung device - if (secAttribute is null) + // Not a samsung device or no user data + if (secAttribute is null || user is null) { return; } @@ -717,7 +715,7 @@ namespace Emby.Dlna.Didl /// <summary> /// Adds fields used by both items and folders. /// </summary> - private void AddCommonFields(BaseItem item, StubType? itemStubType, BaseItem context, XmlWriter writer, Filter filter) + private void AddCommonFields(BaseItem item, StubType? itemStubType, BaseItem? context, XmlWriter writer, Filter filter) { // Don't filter on dc:title because not all devices will include it in the filter // MediaMonkey for example won't display content without a title @@ -795,7 +793,7 @@ namespace Emby.Dlna.Didl if (item.IsDisplayedAsFolder || stubType.HasValue) { - string classType = null; + string? classType = null; if (!_profile.RequiresPlainFolders) { @@ -899,7 +897,7 @@ namespace Emby.Dlna.Didl } } - private void AddGeneralProperties(BaseItem item, StubType? itemStubType, BaseItem context, XmlWriter writer, Filter filter) + private void AddGeneralProperties(BaseItem item, StubType? itemStubType, BaseItem? context, XmlWriter writer, Filter filter) { AddCommonFields(item, itemStubType, context, writer, filter); @@ -975,7 +973,7 @@ namespace Emby.Dlna.Didl private void AddCover(BaseItem item, StubType? stubType, XmlWriter writer) { - ImageDownloadInfo imageInfo = GetImageInfo(item); + ImageDownloadInfo? imageInfo = GetImageInfo(item); if (imageInfo is null) { @@ -1073,7 +1071,7 @@ namespace Emby.Dlna.Didl writer.WriteFullEndElement(); } - private ImageDownloadInfo GetImageInfo(BaseItem item) + private ImageDownloadInfo? GetImageInfo(BaseItem item) { if (item.HasImage(ImageType.Primary)) { @@ -1118,7 +1116,7 @@ namespace Emby.Dlna.Didl return null; } - private BaseItem GetFirstParentWithImageBelowUserRoot(BaseItem item) + private BaseItem? GetFirstParentWithImageBelowUserRoot(BaseItem item) { if (item is null) { @@ -1148,7 +1146,7 @@ namespace Emby.Dlna.Didl private ImageDownloadInfo GetImageInfo(BaseItem item, ImageType type) { var imageInfo = item.GetImageInfo(type, 0); - string tag = null; + string? tag = null; try { @@ -1250,7 +1248,7 @@ namespace Emby.Dlna.Didl { internal Guid ItemId { get; set; } - internal string ImageTag { get; set; } + internal string? ImageTag { get; set; } internal ImageType Type { get; set; } @@ -1260,9 +1258,9 @@ namespace Emby.Dlna.Didl internal bool IsDirectStream { get; set; } - internal string Format { get; set; } + internal required string Format { get; set; } - internal ItemImageInfo ItemImageInfo { get; set; } + internal required ItemImageInfo ItemImageInfo { get; set; } } } } diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs index 9c476119df..d21cc69132 100644 --- a/Emby.Dlna/PlayTo/Device.cs +++ b/Emby.Dlna/PlayTo/Device.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -25,7 +23,7 @@ namespace Emby.Dlna.PlayTo private readonly ILogger _logger; private readonly object _timerLock = new object(); - private Timer _timer; + private Timer? _timer; private int _muteVol; private int _volume; private DateTime _lastVolumeRefresh; @@ -40,13 +38,13 @@ namespace Emby.Dlna.PlayTo _logger = logger; } - public event EventHandler<PlaybackStartEventArgs> PlaybackStart; + public event EventHandler<PlaybackStartEventArgs>? PlaybackStart; - public event EventHandler<PlaybackProgressEventArgs> PlaybackProgress; + public event EventHandler<PlaybackProgressEventArgs>? PlaybackProgress; - public event EventHandler<PlaybackStoppedEventArgs> PlaybackStopped; + public event EventHandler<PlaybackStoppedEventArgs>? PlaybackStopped; - public event EventHandler<MediaChangedEventArgs> MediaChanged; + public event EventHandler<MediaChangedEventArgs>? MediaChanged; public DeviceInfo Properties { get; set; } @@ -75,13 +73,13 @@ namespace Emby.Dlna.PlayTo public bool IsStopped => TransportState == TransportState.STOPPED; - public Action OnDeviceUnavailable { get; set; } + public Action? OnDeviceUnavailable { get; set; } - private TransportCommands AvCommands { get; set; } + private TransportCommands? AvCommands { get; set; } - private TransportCommands RendererCommands { get; set; } + private TransportCommands? RendererCommands { get; set; } - public UBaseObject CurrentMediaInfo { get; private set; } + public UBaseObject? CurrentMediaInfo { get; private set; } public void Start() { @@ -131,7 +129,7 @@ namespace Emby.Dlna.PlayTo _volumeRefreshActive = true; var time = immediate ? 100 : 10000; - _timer.Change(time, Timeout.Infinite); + _timer?.Change(time, Timeout.Infinite); } } @@ -149,7 +147,7 @@ namespace Emby.Dlna.PlayTo _volumeRefreshActive = false; - _timer.Change(Timeout.Infinite, Timeout.Infinite); + _timer?.Change(Timeout.Infinite, Timeout.Infinite); } } @@ -199,7 +197,7 @@ namespace Emby.Dlna.PlayTo } } - private DeviceService GetServiceRenderingControl() + private DeviceService? GetServiceRenderingControl() { var services = Properties.Services; @@ -207,7 +205,7 @@ namespace Emby.Dlna.PlayTo services.FirstOrDefault(s => (s.ServiceType ?? string.Empty).StartsWith("urn:schemas-upnp-org:service:RenderingControl", StringComparison.OrdinalIgnoreCase)); } - private DeviceService GetAvTransportService() + private DeviceService? GetAvTransportService() { var services = Properties.Services; @@ -240,7 +238,7 @@ namespace Emby.Dlna.PlayTo Properties.BaseUrl, service, command.Name, - rendererCommands.BuildPost(command, service.ServiceType, value), + rendererCommands!.BuildPost(command, service.ServiceType, value), // null checked above cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -265,12 +263,7 @@ namespace Emby.Dlna.PlayTo return; } - var service = GetServiceRenderingControl(); - - if (service is null) - { - throw new InvalidOperationException("Unable to find service"); - } + var service = GetServiceRenderingControl() ?? throw new InvalidOperationException("Unable to find service"); // Set it early and assume it will succeed // Remote control will perform better @@ -281,7 +274,7 @@ namespace Emby.Dlna.PlayTo Properties.BaseUrl, service, command.Name, - rendererCommands.BuildPost(command, service.ServiceType, value), + rendererCommands!.BuildPost(command, service.ServiceType, value), // null checked above cancellationToken: cancellationToken) .ConfigureAwait(false); } @@ -296,26 +289,20 @@ namespace Emby.Dlna.PlayTo return; } - var service = GetAvTransportService(); - - if (service is null) - { - throw new InvalidOperationException("Unable to find service"); - } - + var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service"); await new DlnaHttpClient(_logger, _httpClientFactory) .SendCommandAsync( Properties.BaseUrl, service, command.Name, - avCommands.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"), + avCommands!.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"), // null checked above cancellationToken: cancellationToken) .ConfigureAwait(false); RestartTimer(true); } - public async Task SetAvTransport(string url, string header, string metaData, CancellationToken cancellationToken) + public async Task SetAvTransport(string url, string? header, string metaData, CancellationToken cancellationToken) { var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); @@ -335,14 +322,8 @@ namespace Emby.Dlna.PlayTo { "CurrentURIMetaData", CreateDidlMeta(metaData) } }; - var service = GetAvTransportService(); - - if (service is null) - { - throw new InvalidOperationException("Unable to find service"); - } - - var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary); + var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service"); + var post = avCommands!.BuildPost(command, service.ServiceType, url, dictionary); // null checked above await new DlnaHttpClient(_logger, _httpClientFactory) .SendCommandAsync( Properties.BaseUrl, @@ -372,7 +353,7 @@ namespace Emby.Dlna.PlayTo * SetNextAvTransport is used to specify to the DLNA device what is the next track to play. * Without that information, the next track command on the device does not work. */ - public async Task SetNextAvTransport(string url, string header, string metaData, CancellationToken cancellationToken = default) + public async Task SetNextAvTransport(string url, string? header, string metaData, CancellationToken cancellationToken = default) { var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); @@ -380,7 +361,7 @@ namespace Emby.Dlna.PlayTo _logger.LogDebug("{PropertyName} - SetNextAvTransport Uri: {Url} DlnaHeaders: {Header}", Properties.Name, url, header); - var command = avCommands.ServiceActions.FirstOrDefault(c => string.Equals(c.Name, "SetNextAVTransportURI", StringComparison.OrdinalIgnoreCase)); + var command = avCommands?.ServiceActions.FirstOrDefault(c => string.Equals(c.Name, "SetNextAVTransportURI", StringComparison.OrdinalIgnoreCase)); if (command is null) { return; @@ -392,14 +373,8 @@ namespace Emby.Dlna.PlayTo { "NextURIMetaData", CreateDidlMeta(metaData) } }; - var service = GetAvTransportService(); - - if (service is null) - { - throw new InvalidOperationException("Unable to find service"); - } - - var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary); + var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service"); + var post = avCommands!.BuildPost(command, service.ServiceType, url, dictionary); // null checked above await new DlnaHttpClient(_logger, _httpClientFactory) .SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header, cancellationToken) .ConfigureAwait(false); @@ -423,12 +398,7 @@ namespace Emby.Dlna.PlayTo return Task.CompletedTask; } - var service = GetAvTransportService(); - if (service is null) - { - throw new InvalidOperationException("Unable to find service"); - } - + var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service"); return new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync( Properties.BaseUrl, service, @@ -460,14 +430,13 @@ namespace Emby.Dlna.PlayTo return; } - var service = GetAvTransportService(); - + var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service"); await new DlnaHttpClient(_logger, _httpClientFactory) .SendCommandAsync( Properties.BaseUrl, service, command.Name, - avCommands.BuildPost(command, service.ServiceType, 1), + avCommands!.BuildPost(command, service.ServiceType, 1), // null checked above cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -484,14 +453,13 @@ namespace Emby.Dlna.PlayTo return; } - var service = GetAvTransportService(); - + var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service"); await new DlnaHttpClient(_logger, _httpClientFactory) .SendCommandAsync( Properties.BaseUrl, service, command.Name, - avCommands.BuildPost(command, service.ServiceType, 1), + avCommands!.BuildPost(command, service.ServiceType, 1), // null checked above cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -500,7 +468,7 @@ namespace Emby.Dlna.PlayTo RestartTimer(true); } - private async void TimerCallback(object sender) + private async void TimerCallback(object? sender) { if (_disposed) { @@ -623,7 +591,7 @@ namespace Emby.Dlna.PlayTo Properties.BaseUrl, service, command.Name, - rendererCommands.BuildPost(command, service.ServiceType), + rendererCommands!.BuildPost(command, service.ServiceType), // null checked above cancellationToken: cancellationToken).ConfigureAwait(false); if (result is null || result.Document is null) @@ -673,7 +641,7 @@ namespace Emby.Dlna.PlayTo Properties.BaseUrl, service, command.Name, - rendererCommands.BuildPost(command, service.ServiceType), + rendererCommands!.BuildPost(command, service.ServiceType), // null checked above cancellationToken: cancellationToken).ConfigureAwait(false); if (result is null || result.Document is null) @@ -728,7 +696,7 @@ namespace Emby.Dlna.PlayTo return null; } - private async Task<UBaseObject> GetMediaInfo(TransportCommands avCommands, CancellationToken cancellationToken) + private async Task<UBaseObject?> GetMediaInfo(TransportCommands avCommands, CancellationToken cancellationToken) { var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMediaInfo"); if (command is null) @@ -798,7 +766,7 @@ namespace Emby.Dlna.PlayTo return null; } - private async Task<(bool Success, UBaseObject Track)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken) + private async Task<(bool Success, UBaseObject? Track)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken) { var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetPositionInfo"); if (command is null) @@ -871,7 +839,7 @@ namespace Emby.Dlna.PlayTo return (true, null); } - XElement uPnpResponse = null; + XElement? uPnpResponse = null; try { @@ -895,7 +863,7 @@ namespace Emby.Dlna.PlayTo return (true, uTrack); } - private XElement ParseResponse(string xml) + private XElement? ParseResponse(string xml) { // Handle different variations sent back by devices. try @@ -929,7 +897,7 @@ namespace Emby.Dlna.PlayTo return null; } - private static UBaseObject CreateUBaseObject(XElement container, string trackUri) + private static UBaseObject CreateUBaseObject(XElement? container, string? trackUri) { ArgumentNullException.ThrowIfNull(container); @@ -972,7 +940,7 @@ namespace Emby.Dlna.PlayTo return new string[4]; } - private async Task<TransportCommands> GetAVProtocolAsync(CancellationToken cancellationToken) + private async Task<TransportCommands?> GetAVProtocolAsync(CancellationToken cancellationToken) { if (AvCommands is not null) { @@ -1004,7 +972,7 @@ namespace Emby.Dlna.PlayTo return AvCommands; } - private async Task<TransportCommands> GetRenderingProtocolAsync(CancellationToken cancellationToken) + private async Task<TransportCommands?> GetRenderingProtocolAsync(CancellationToken cancellationToken) { if (RendererCommands is not null) { @@ -1054,7 +1022,7 @@ namespace Emby.Dlna.PlayTo return baseUrl + url; } - public static async Task<Device> CreateuPnpDeviceAsync(Uri url, IHttpClientFactory httpClientFactory, ILogger logger, CancellationToken cancellationToken) + public static async Task<Device?> CreateuPnpDeviceAsync(Uri url, IHttpClientFactory httpClientFactory, ILogger logger, CancellationToken cancellationToken) { var ssdpHttpClient = new DlnaHttpClient(logger, httpClientFactory); @@ -1287,7 +1255,7 @@ namespace Emby.Dlna.PlayTo } _timer = null; - Properties = null; + Properties = null!; _disposed = true; } diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs index 86db363374..b1ad15cdc9 100644 --- a/Emby.Dlna/PlayTo/PlayToController.cs +++ b/Emby.Dlna/PlayTo/PlayToController.cs @@ -42,7 +42,7 @@ namespace Emby.Dlna.PlayTo private readonly IDeviceDiscovery _deviceDiscovery; private readonly string _serverAddress; - private readonly string _accessToken; + private readonly string? _accessToken; private readonly List<PlaylistItem> _playlist = new List<PlaylistItem>(); private Device _device; @@ -59,7 +59,7 @@ namespace Emby.Dlna.PlayTo IUserManager userManager, IImageProcessor imageProcessor, string serverAddress, - string accessToken, + string? accessToken, IDeviceDiscovery deviceDiscovery, IUserDataManager userDataManager, ILocalizationManager localization, diff --git a/Emby.Dlna/PlayTo/PlayToManager.cs b/Emby.Dlna/PlayTo/PlayToManager.cs index 241dff5ae3..ef617422c4 100644 --- a/Emby.Dlna/PlayTo/PlayToManager.cs +++ b/Emby.Dlna/PlayTo/PlayToManager.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -67,7 +65,7 @@ namespace Emby.Dlna.PlayTo _deviceDiscovery.DeviceDiscovered += OnDeviceDiscoveryDeviceDiscovered; } - private async void OnDeviceDiscoveryDeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e) + private async void OnDeviceDiscoveryDeviceDiscovered(object? sender, GenericEventArgs<UpnpDeviceInfo> e) { if (_disposed) { @@ -76,12 +74,12 @@ namespace Emby.Dlna.PlayTo var info = e.Argument; - if (!info.Headers.TryGetValue("USN", out string usn)) + if (!info.Headers.TryGetValue("USN", out string? usn)) { usn = string.Empty; } - if (!info.Headers.TryGetValue("NT", out string nt)) + if (!info.Headers.TryGetValue("NT", out string? nt)) { nt = string.Empty; } @@ -161,7 +159,7 @@ namespace Emby.Dlna.PlayTo var uri = info.Location; _logger.LogDebug("Attempting to create PlayToController from location {0}", uri); - if (info.Headers.TryGetValue("USN", out string uuid)) + if (info.Headers.TryGetValue("USN", out string? uuid)) { uuid = GetUuid(uuid); } diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs index 48584ae0cb..1303012e1a 100644 --- a/Emby.Server.Implementations/Plugins/PluginManager.cs +++ b/Emby.Server.Implementations/Plugins/PluginManager.cs @@ -677,7 +677,7 @@ namespace Emby.Server.Implementations.Plugins } catch (JsonException ex) { - _logger.LogError(ex, "Error deserializing {Json}.", Encoding.UTF8.GetString(data!)); + _logger.LogError(ex, "Error deserializing {Json}.", Encoding.UTF8.GetString(data)); } if (manifest is not null) diff --git a/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs b/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs index 62b70ce53c..10326363a9 100644 --- a/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs +++ b/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using MediaBrowser.Controller.Entities; @@ -9,12 +7,12 @@ namespace MediaBrowser.Controller.Drawing { public static class ImageProcessorExtensions { - public static string GetImageCacheTag(this IImageProcessor processor, BaseItem item, ImageType imageType) + public static string? GetImageCacheTag(this IImageProcessor processor, BaseItem item, ImageType imageType) { return processor.GetImageCacheTag(item, imageType, 0); } - public static string GetImageCacheTag(this IImageProcessor processor, BaseItem item, ImageType imageType, int imageIndex) + public static string? GetImageCacheTag(this IImageProcessor processor, BaseItem item, ImageType imageType, int imageIndex) { var imageInfo = item.GetImageInfo(imageType, imageIndex); diff --git a/MediaBrowser.Model/Dlna/DeviceProfile.cs b/MediaBrowser.Model/Dlna/DeviceProfile.cs index b7c23669df..07bb002eae 100644 --- a/MediaBrowser.Model/Dlna/DeviceProfile.cs +++ b/MediaBrowser.Model/Dlna/DeviceProfile.cs @@ -314,7 +314,7 @@ namespace MediaBrowser.Model.Dlna /// <param name="audioSampleRate">The audio sample rate.</param> /// <param name="audioBitDepth">The audio bit depth.</param> /// <returns>The <see cref="ResponseProfile"/>.</returns> - public ResponseProfile? GetAudioMediaProfile(string container, string? audioCodec, int? audioChannels, int? audioBitrate, int? audioSampleRate, int? audioBitDepth) + public ResponseProfile? GetAudioMediaProfile(string? container, string? audioCodec, int? audioChannels, int? audioBitrate, int? audioSampleRate, int? audioBitDepth) { foreach (var i in ResponseProfiles) { @@ -438,14 +438,14 @@ namespace MediaBrowser.Model.Dlna /// <param name="isAvc">True if Avc.</param> /// <returns>The <see cref="ResponseProfile"/>.</returns> public ResponseProfile? GetVideoMediaProfile( - string container, + string? container, string? audioCodec, string? videoCodec, int? width, int? height, int? bitDepth, int? videoBitrate, - string videoProfile, + string? videoProfile, VideoRangeType videoRangeType, double? videoLevel, float? videoFramerate, @@ -456,7 +456,7 @@ namespace MediaBrowser.Model.Dlna int? refFrames, int? numVideoStreams, int? numAudioStreams, - string videoCodecTag, + string? videoCodecTag, bool? isAvc) { foreach (var i in ResponseProfiles) diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index f6b882c3e6..7aac4fda50 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -179,15 +179,9 @@ namespace MediaBrowser.Model.Dlna { ValidateMediaOptions(options, true); - var mediaSources = new List<MediaSourceInfo>(); - foreach (var mediaSourceInfo in options.MediaSources) - { - if (string.IsNullOrEmpty(options.MediaSourceId) - || string.Equals(mediaSourceInfo.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase)) - { - mediaSources.Add(mediaSourceInfo); - } - } + var mediaSources = string.IsNullOrEmpty(options.MediaSourceId) + ? options.MediaSources + : options.MediaSources.Where(x => string.Equals(x.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase)); var streams = new List<StreamInfo>(); foreach (var mediaSourceInfo in mediaSources) @@ -216,7 +210,7 @@ namespace MediaBrowser.Model.Dlna return streams.OrderBy(i => { // Nothing beats direct playing a file - if (i.PlayMethod == PlayMethod.DirectPlay && i.MediaSource.Protocol == MediaProtocol.File) + if (i.PlayMethod == PlayMethod.DirectPlay && i.MediaSource?.Protocol == MediaProtocol.File) { return 0; } @@ -235,7 +229,7 @@ namespace MediaBrowser.Model.Dlna } }).ThenBy(i => { - switch (i.MediaSource.Protocol) + switch (i.MediaSource?.Protocol) { case MediaProtocol.File: return 0; @@ -246,7 +240,7 @@ namespace MediaBrowser.Model.Dlna { if (maxBitrate > 0) { - if (i.MediaSource.Bitrate.HasValue) + if (i.MediaSource?.Bitrate is not null) { return Math.Abs(i.MediaSource.Bitrate.Value - maxBitrate); } @@ -585,10 +579,10 @@ namespace MediaBrowser.Model.Dlna MediaSource = item, RunTimeTicks = item.RunTimeTicks, Context = options.Context, - DeviceProfile = options.Profile + DeviceProfile = options.Profile, + SubtitleStreamIndex = options.SubtitleStreamIndex ?? GetDefaultSubtitleStreamIndex(item, options.Profile.SubtitleProfiles) }; - playlistItem.SubtitleStreamIndex = options.SubtitleStreamIndex ?? GetDefaultSubtitleStreamIndex(item, options.Profile.SubtitleProfiles); var subtitleStream = playlistItem.SubtitleStreamIndex.HasValue ? item.GetMediaStream(MediaStreamType.Subtitle, playlistItem.SubtitleStreamIndex.Value) : null; var audioStream = item.GetDefaultAudioStream(options.AudioStreamIndex ?? item.DefaultAudioStreamIndex); @@ -659,7 +653,8 @@ namespace MediaBrowser.Model.Dlna if (audioStreamIndex.HasValue) { playlistItem.AudioStreamIndex = audioStreamIndex; - playlistItem.AudioCodecs = new[] { item.GetMediaStream(MediaStreamType.Audio, audioStreamIndex.Value)?.Codec }; + var audioCodec = item.GetMediaStream(MediaStreamType.Audio, audioStreamIndex.Value)?.Codec; + playlistItem.AudioCodecs = audioCodec is null ? Array.Empty<string>() : new[] { audioCodec }; } } else if (directPlay == PlayMethod.DirectStream) @@ -842,7 +837,7 @@ namespace MediaBrowser.Model.Dlna if (videoStream is not null && videoStream.Level != 0) { - playlistItem.SetOption(qualifier, "level", videoStream.Level.ToString()); + playlistItem.SetOption(qualifier, "level", videoStream.Level.ToString() ?? string.Empty); } // Prefer matching audio codecs, could do better here @@ -871,7 +866,7 @@ namespace MediaBrowser.Model.Dlna // Copy matching audio codec options playlistItem.AudioSampleRate = audioStream.SampleRate; - playlistItem.SetOption(qualifier, "audiochannels", audioStream.Channels.ToString()); + playlistItem.SetOption(qualifier, "audiochannels", audioStream.Channels.ToString() ?? string.Empty); if (!string.IsNullOrEmpty(audioStream.Profile)) { @@ -880,7 +875,7 @@ namespace MediaBrowser.Model.Dlna if (audioStream.Level != 0) { - playlistItem.SetOption(audioStream.Codec, "level", audioStream.Level.ToString()); + playlistItem.SetOption(audioStream.Codec, "level", audioStream.Level.ToString() ?? string.Empty); } } diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index 00543616d1..fc146df306 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -1,9 +1,9 @@ -#nullable disable #pragma warning disable CS1591 using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using Jellyfin.Data.Enums; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; @@ -34,9 +34,9 @@ namespace MediaBrowser.Model.Dlna public DlnaProfileType MediaType { get; set; } - public string Container { get; set; } + public string? Container { get; set; } - public string SubProtocol { get; set; } + public string? SubProtocol { get; set; } public long StartPositionTicks { get; set; } @@ -80,11 +80,11 @@ namespace MediaBrowser.Model.Dlna public float? MaxFramerate { get; set; } - public DeviceProfile DeviceProfile { get; set; } + public required DeviceProfile DeviceProfile { get; set; } - public string DeviceProfileId { get; set; } + public string? DeviceProfileId { get; set; } - public string DeviceId { get; set; } + public string? DeviceId { get; set; } public long? RunTimeTicks { get; set; } @@ -92,21 +92,21 @@ namespace MediaBrowser.Model.Dlna public bool EstimateContentLength { get; set; } - public MediaSourceInfo MediaSource { get; set; } + public MediaSourceInfo? MediaSource { get; set; } public string[] SubtitleCodecs { get; set; } public SubtitleDeliveryMethod SubtitleDeliveryMethod { get; set; } - public string SubtitleFormat { get; set; } + public string? SubtitleFormat { get; set; } - public string PlaySessionId { get; set; } + public string? PlaySessionId { get; set; } public TranscodeReason TranscodeReasons { get; set; } public Dictionary<string, string> StreamOptions { get; private set; } - public string MediaSourceId => MediaSource?.Id; + public string? MediaSourceId => MediaSource?.Id; public bool IsDirectStream => MediaSource?.VideoType is not (VideoType.Dvd or VideoType.BluRay) && PlayMethod is PlayMethod.DirectStream or PlayMethod.DirectPlay; @@ -114,12 +114,12 @@ namespace MediaBrowser.Model.Dlna /// <summary> /// Gets the audio stream that will be used. /// </summary> - public MediaStream TargetAudioStream => MediaSource?.GetDefaultAudioStream(AudioStreamIndex); + public MediaStream? TargetAudioStream => MediaSource?.GetDefaultAudioStream(AudioStreamIndex); /// <summary> /// Gets the video stream that will be used. /// </summary> - public MediaStream TargetVideoStream => MediaSource?.VideoStream; + public MediaStream? TargetVideoStream => MediaSource?.VideoStream; /// <summary> /// Gets the audio sample rate that will be in the output stream. @@ -259,7 +259,7 @@ namespace MediaBrowser.Model.Dlna /// <summary> /// Gets the audio sample rate that will be in the output stream. /// </summary> - public string TargetVideoProfile + public string? TargetVideoProfile { get { @@ -307,7 +307,7 @@ namespace MediaBrowser.Model.Dlna /// Gets the target video codec tag. /// </summary> /// <value>The target video codec tag.</value> - public string TargetVideoCodecTag + public string? TargetVideoCodecTag { get { @@ -364,7 +364,7 @@ namespace MediaBrowser.Model.Dlna { var stream = TargetAudioStream; - string inputCodec = stream?.Codec; + string? inputCodec = stream?.Codec; if (IsDirectStream) { @@ -389,7 +389,7 @@ namespace MediaBrowser.Model.Dlna { var stream = TargetVideoStream; - string inputCodec = stream?.Codec; + string? inputCodec = stream?.Codec; if (IsDirectStream) { @@ -417,7 +417,7 @@ namespace MediaBrowser.Model.Dlna { if (IsDirectStream) { - return MediaSource.Size; + return MediaSource?.Size; } if (RunTimeTicks.HasValue) @@ -580,7 +580,7 @@ namespace MediaBrowser.Model.Dlna } } - public void SetOption(string qualifier, string name, string value) + public void SetOption(string? qualifier, string name, string value) { if (string.IsNullOrEmpty(qualifier)) { @@ -597,7 +597,7 @@ namespace MediaBrowser.Model.Dlna StreamOptions[name] = value; } - public string GetOption(string qualifier, string name) + public string? GetOption(string? qualifier, string name) { var value = GetOption(qualifier + "-" + name); @@ -609,7 +609,7 @@ namespace MediaBrowser.Model.Dlna return value; } - public string GetOption(string name) + public string? GetOption(string name) { if (StreamOptions.TryGetValue(name, out var value)) { @@ -619,7 +619,7 @@ namespace MediaBrowser.Model.Dlna return null; } - public string ToUrl(string baseUrl, string accessToken) + public string ToUrl(string baseUrl, string? accessToken) { ArgumentException.ThrowIfNullOrEmpty(baseUrl); @@ -686,7 +686,7 @@ namespace MediaBrowser.Model.Dlna return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString); } - private static IEnumerable<NameValuePair> BuildParams(StreamInfo item, string accessToken) + private static IEnumerable<NameValuePair> BuildParams(StreamInfo item, string? accessToken) { var list = new List<NameValuePair>(); @@ -730,7 +730,7 @@ namespace MediaBrowser.Model.Dlna list.Add(new NameValuePair("PlaySessionId", item.PlaySessionId ?? string.Empty)); list.Add(new NameValuePair("api_key", accessToken ?? string.Empty)); - string liveStreamId = item.MediaSource?.LiveStreamId; + string? liveStreamId = item.MediaSource?.LiveStreamId; list.Add(new NameValuePair("LiveStreamId", liveStreamId ?? string.Empty)); list.Add(new NameValuePair("SubtitleMethod", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleDeliveryMethod.ToString() : string.Empty)); @@ -772,7 +772,7 @@ namespace MediaBrowser.Model.Dlna list.Add(new NameValuePair("RequireAvc", item.RequireAvc.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); } - list.Add(new NameValuePair("Tag", item.MediaSource.ETag ?? string.Empty)); + list.Add(new NameValuePair("Tag", item.MediaSource?.ETag ?? string.Empty)); string subtitleCodecs = item.SubtitleCodecs.Length == 0 ? string.Empty : @@ -816,13 +816,18 @@ namespace MediaBrowser.Model.Dlna return list; } - public IEnumerable<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, string baseUrl, string accessToken) + public IEnumerable<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, string baseUrl, string? accessToken) { return GetSubtitleProfiles(transcoderSupport, includeSelectedTrackOnly, false, baseUrl, accessToken); } - public IEnumerable<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, bool enableAllProfiles, string baseUrl, string accessToken) + public IEnumerable<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, bool enableAllProfiles, string baseUrl, string? accessToken) { + if (MediaSource is null) + { + return Enumerable.Empty<SubtitleStreamInfo>(); + } + var list = new List<SubtitleStreamInfo>(); // HLS will preserve timestamps so we can just grab the full subtitle stream @@ -856,27 +861,36 @@ namespace MediaBrowser.Model.Dlna return list; } - private void AddSubtitleProfiles(List<SubtitleStreamInfo> list, MediaStream stream, ITranscoderSupport transcoderSupport, bool enableAllProfiles, string baseUrl, string accessToken, long startPositionTicks) + private void AddSubtitleProfiles(List<SubtitleStreamInfo> list, MediaStream stream, ITranscoderSupport transcoderSupport, bool enableAllProfiles, string baseUrl, string? accessToken, long startPositionTicks) { if (enableAllProfiles) { foreach (var profile in DeviceProfile.SubtitleProfiles) { var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, new[] { profile }, transcoderSupport); - - list.Add(info); + if (info is not null) + { + list.Add(info); + } } } else { var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, DeviceProfile.SubtitleProfiles, transcoderSupport); - - list.Add(info); + if (info is not null) + { + list.Add(info); + } } } - private SubtitleStreamInfo GetSubtitleStreamInfo(MediaStream stream, string baseUrl, string accessToken, long startPositionTicks, SubtitleProfile[] subtitleProfiles, ITranscoderSupport transcoderSupport) + private SubtitleStreamInfo? GetSubtitleStreamInfo(MediaStream stream, string baseUrl, string? accessToken, long startPositionTicks, SubtitleProfile[] subtitleProfiles, ITranscoderSupport transcoderSupport) { + if (MediaSource is null) + { + return null; + } + var subtitleProfile = StreamBuilder.GetSubtitleProfile(MediaSource, stream, subtitleProfiles, PlayMethod, transcoderSupport, Container, SubProtocol); var info = new SubtitleStreamInfo { @@ -920,7 +934,7 @@ namespace MediaBrowser.Model.Dlna return info; } - public int? GetTargetVideoBitDepth(string codec) + public int? GetTargetVideoBitDepth(string? codec) { var value = GetOption(codec, "videobitdepth"); @@ -932,7 +946,7 @@ namespace MediaBrowser.Model.Dlna return null; } - public int? GetTargetAudioBitDepth(string codec) + public int? GetTargetAudioBitDepth(string? codec) { var value = GetOption(codec, "audiobitdepth"); @@ -944,7 +958,7 @@ namespace MediaBrowser.Model.Dlna return null; } - public double? GetTargetVideoLevel(string codec) + public double? GetTargetVideoLevel(string? codec) { var value = GetOption(codec, "level"); @@ -956,7 +970,7 @@ namespace MediaBrowser.Model.Dlna return null; } - public int? GetTargetRefFrames(string codec) + public int? GetTargetRefFrames(string? codec) { var value = GetOption(codec, "maxrefframes"); @@ -968,7 +982,7 @@ namespace MediaBrowser.Model.Dlna return null; } - public int? GetTargetAudioChannels(string codec) + public int? GetTargetAudioChannels(string? codec) { var defaultValue = GlobalMaxAudioChannels ?? TranscodingMaxAudioChannels; @@ -988,7 +1002,7 @@ namespace MediaBrowser.Model.Dlna private int? GetMediaStreamCount(MediaStreamType type, int limit) { - var count = MediaSource.GetStreamCount(type); + var count = MediaSource?.GetStreamCount(type); if (count.HasValue) { diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs index c30dad6f90..e4a1e1f887 100644 --- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs +++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs @@ -351,11 +351,11 @@ namespace Jellyfin.Model.Tests // Assert.Contains(uri.Extension, containers); // Check expected video codec (1) - Assert.Contains(targetVideoStream.Codec, streamInfo.TargetVideoCodec); + Assert.Contains(targetVideoStream?.Codec, streamInfo.TargetVideoCodec); Assert.Single(streamInfo.TargetVideoCodec); // Check expected audio codecs (1) - Assert.Contains(targetAudioStream.Codec, streamInfo.TargetAudioCodec); + Assert.Contains(targetAudioStream?.Codec, streamInfo.TargetAudioCodec); Assert.Single(streamInfo.TargetAudioCodec); // Assert.Single(val.AudioCodecs); @@ -410,13 +410,13 @@ namespace Jellyfin.Model.Tests else { // Check expected video codec (1) - Assert.Contains(targetVideoStream.Codec, streamInfo.TargetVideoCodec); + Assert.Contains(targetVideoStream?.Codec, streamInfo.TargetVideoCodec); Assert.Single(streamInfo.TargetVideoCodec); if (transcodeMode.Equals("DirectStream", StringComparison.Ordinal)) { // Check expected audio codecs (1) - if (!targetAudioStream.IsExternal) + if (targetAudioStream?.IsExternal == false) { // Check expected audio codecs (1) if (streamInfo.TranscodeReasons.HasFlag(TranscodeReason.ContainerNotSupported)) @@ -432,7 +432,7 @@ namespace Jellyfin.Model.Tests else if (transcodeMode.Equals("Remux", StringComparison.Ordinal)) { // Check expected audio codecs (1) - Assert.Contains(targetAudioStream.Codec, streamInfo.AudioCodecs); + Assert.Contains(targetAudioStream?.Codec, streamInfo.AudioCodecs); Assert.Single(streamInfo.AudioCodecs); } @@ -440,10 +440,10 @@ namespace Jellyfin.Model.Tests var videoStream = targetVideoStream; Assert.False(streamInfo.EstimateContentLength); Assert.Equal(TranscodeSeekInfo.Auto, streamInfo.TranscodeSeekInfo); - Assert.Contains(videoStream.Profile?.ToLowerInvariant() ?? string.Empty, streamInfo.TargetVideoProfile?.Split(",").Select(s => s.ToLowerInvariant()) ?? Array.Empty<string>()); - Assert.Equal(videoStream.Level, streamInfo.TargetVideoLevel); - Assert.Equal(videoStream.BitDepth, streamInfo.TargetVideoBitDepth); - Assert.InRange(streamInfo.VideoBitrate.GetValueOrDefault(), videoStream.BitRate.GetValueOrDefault(), int.MaxValue); + Assert.Contains(videoStream?.Profile?.ToLowerInvariant() ?? string.Empty, streamInfo.TargetVideoProfile?.Split(",").Select(s => s.ToLowerInvariant()) ?? Array.Empty<string>()); + Assert.Equal(videoStream?.Level, streamInfo.TargetVideoLevel); + Assert.Equal(videoStream?.BitDepth, streamInfo.TargetVideoBitDepth); + Assert.InRange(streamInfo.VideoBitrate.GetValueOrDefault(), videoStream?.BitRate.GetValueOrDefault() ?? 0, int.MaxValue); // Audio codec not supported if ((why & TranscodeReason.AudioCodecNotSupported) != 0) @@ -452,7 +452,7 @@ namespace Jellyfin.Model.Tests if (options.AudioStreamIndex >= 0) { // TODO:fixme - if (!targetAudioStream.IsExternal) + if (targetAudioStream?.IsExternal == false) { Assert.DoesNotContain(targetAudioStream.Codec, streamInfo.AudioCodecs); } From d3c7af0d5c3709e18eac71fdb0279e985b6a9113 Mon Sep 17 00:00:00 2001 From: Bond-009 <bond.009@outlook.com> Date: Sat, 29 Jul 2023 23:52:26 +0200 Subject: [PATCH 475/858] Fix Jellyfin.Networking.Tests (#10055) --- .../Jellyfin.Networking.Tests.csproj | 7 +------ tests/Jellyfin.Networking.Tests/NetworkParseTests.cs | 6 +++--- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj index 4b4bdd2a51..3747db3bbb 100644 --- a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj +++ b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj @@ -18,12 +18,7 @@ </ItemGroup> <ItemGroup> - <ProjectReference Include="../../Emby.Server.Implementations/Emby.Server.Implementations.csproj" /> - <ProjectReference Include="../../MediaBrowser.Common/MediaBrowser.Common.csproj" /> + <ProjectReference Include="../../Jellyfin.Networking/Jellyfin.Networking.csproj" /> </ItemGroup> - <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> - <DefineConstants>DEBUG</DefineConstants> - </PropertyGroup> - </Project> diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs index cb8092ae9a..731cbbafbc 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs @@ -97,7 +97,7 @@ namespace Jellyfin.Networking.Tests /// Checks if IPv4 address is within a defined subnet. /// </summary> /// <param name="netMask">Network mask.</param> - /// <param name="IPAddress">IP Address.</param> + /// <param name="ipAddress">IP Address.</param> [Theory] [InlineData("192.168.5.85/24", "192.168.5.1")] [InlineData("192.168.5.85/24", "192.168.5.254")] @@ -211,7 +211,7 @@ namespace Jellyfin.Networking.Tests if (nm.TryParseInterface(result, out var resultObj)) { - result = resultObj.First().Address.ToString(); + result = resultObj[0].Address.ToString(); var intf = nm.GetBindAddress(source, out _); Assert.Equal(intf, result); @@ -270,7 +270,7 @@ namespace Jellyfin.Networking.Tests if (nm.TryParseInterface(result, out IReadOnlyList<IPData>? resultObj) && resultObj is not null) { // Parse out IPAddresses so we can do a string comparison (ignore subnet masks). - result = resultObj.First().Address.ToString(); + result = resultObj[0].Address.ToString(); } var intf = nm.GetBindAddress(source, out int? _); From dd75f35a1a453541e4ca8cfe9422d3e39e9d668b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20M=C3=BCller?= <github@lonebyte.de> Date: Mon, 31 Jul 2023 21:49:51 +0200 Subject: [PATCH 476/858] Fix the is-local check when resetting the password This fixes the check whether a warning should be logged when resetting the password from outside the local network. Fixes: #10059 --- Jellyfin.Api/Controllers/UserController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 4f61af35e0..1be40111dd 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -494,7 +494,7 @@ public class UserController : BaseJellyfinApiController var isLocal = HttpContext.IsLocal() || _networkManager.IsInLocalNetwork(ip); - if (isLocal) + if (!isLocal) { _logger.LogWarning("Password reset process initiated from outside the local network with IP: {IP}", ip); } From 66ff724acf6bac3e8e65773ee46bb698b2cda334 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20M=C3=BCller?= <github@lonebyte.de> Date: Mon, 31 Jul 2023 22:19:06 +0200 Subject: [PATCH 477/858] Fix the probing of m4a metadata The composer is not set in some of my m4a files. For some reason TagLibSharp returns the composer as an empty string in this case. This causes an exception in PeopleHelper.AddPerson, and thus probing fails. IMHO we can simply ignore empty values. Fixes: #10061 --- .../MediaInfo/AudioFileProber.cs | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index a7e8f774c6..9bcb1c39bd 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -223,30 +223,39 @@ namespace MediaBrowser.Providers.MediaInfo var albumArtists = tags.AlbumArtists; foreach (var albumArtist in albumArtists) { - PeopleHelper.AddPerson(people, new PersonInfo + if (!string.IsNullOrEmpty(albumArtist)) { - Name = albumArtist, - Type = PersonKind.AlbumArtist - }); + PeopleHelper.AddPerson(people, new PersonInfo + { + Name = albumArtist, + Type = PersonKind.AlbumArtist + }); + } } var performers = tags.Performers; foreach (var performer in performers) { - PeopleHelper.AddPerson(people, new PersonInfo + if (!string.IsNullOrEmpty(performer)) { - Name = performer, - Type = PersonKind.Artist - }); + PeopleHelper.AddPerson(people, new PersonInfo + { + Name = performer, + Type = PersonKind.Artist + }); + } } foreach (var composer in tags.Composers) { - PeopleHelper.AddPerson(people, new PersonInfo + if (!string.IsNullOrEmpty(composer)) { - Name = composer, - Type = PersonKind.Composer - }); + PeopleHelper.AddPerson(people, new PersonInfo + { + Name = composer, + Type = PersonKind.Composer + }); + } } _libraryManager.UpdatePeople(audio, people); From e9f23c61c937b230b1d3bd7865a083aeb3d51657 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20M=C3=BCller?= <github@lonebyte.de> Date: Tue, 1 Aug 2023 17:11:32 +0200 Subject: [PATCH 478/858] Fix the fLaC/flac HLS issue also for audio-only I moved the first application of the workaround out of the if block so that it also applies to audio-only streams. The workaround was extended likewise. We should first and foremost adhere to the specifications and apply workarounds afterwards for software that doesn't follow them. So I turned around the workaround to first output the fLaC variant and then the alternative flac variant. Fixes: #10066 --- Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 21 +++++++++++-------- Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs | 6 ++++-- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index 63667e7e69..dfcccddfc2 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -198,15 +198,15 @@ public class DynamicHlsHelper var basicPlaylist = AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup); + // Provide a workaround for the case issue between flac and fLaC. + var flacWaPlaylist = ApplyFlacCaseWorkaround(state, basicPlaylist.ToString()); + if (!string.IsNullOrEmpty(flacWaPlaylist)) + { + builder.Append(flacWaPlaylist); + } + if (state.VideoStream is not null && state.VideoRequest is not null) { - // Provide a workaround for the case issue between flac and fLaC. - var flacWaPlaylist = ApplyFlacCaseWorkaround(state, basicPlaylist.ToString()); - if (!string.IsNullOrEmpty(flacWaPlaylist)) - { - builder.Append(flacWaPlaylist); - } - var encodingOptions = _serverConfigurationManager.GetEncodingOptions(); // Provide SDR HEVC entrance for backward compatibility. @@ -775,8 +775,11 @@ public class DynamicHlsHelper return string.Empty; } - var newPlaylist = srcPlaylist.Replace(",flac\"", ",fLaC\"", StringComparison.Ordinal); + var newPlaylist = srcPlaylist; - return newPlaylist.Contains(",fLaC\"", StringComparison.Ordinal) ? newPlaylist : string.Empty; + newPlaylist = newPlaylist.Replace(",fLaC\"", ",flac\"", StringComparison.Ordinal); + newPlaylist = newPlaylist.Replace("\"fLaC\"", "\"flac\"", StringComparison.Ordinal); + + return string.Equals(srcPlaylist, newPlaylist, StringComparison.Ordinal) ? string.Empty : newPlaylist; } } diff --git a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs index 9a141a16d9..9b1c52045f 100644 --- a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs +++ b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs @@ -5,7 +5,9 @@ using System.Text; namespace Jellyfin.Api.Helpers; /// <summary> -/// Hls Codec string helpers. +/// Helpers to generate HLS codec strings according to +/// <a href="https://datatracker.ietf.org/doc/html/rfc6381#section-3.3">RFC 6381 section 3.3</a> +/// and the <a href="https://mp4ra.org">MP4 Registration Authority</a>. /// </summary> public static class HlsCodecStringHelpers { @@ -27,7 +29,7 @@ public static class HlsCodecStringHelpers /// <summary> /// Codec name for FLAC. /// </summary> - public const string FLAC = "flac"; + public const string FLAC = "fLaC"; /// <summary> /// Codec name for ALAC. From 19fb061381dd107d5e0236cf9d8b59b2e2318130 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20M=C3=BCller?= <github@lonebyte.de> Date: Tue, 1 Aug 2023 17:22:42 +0200 Subject: [PATCH 479/858] Correct the HLS Opus codec string Apple doesn't support Opus via HLS yet, but if they ever do, they will definitely expect "Opus" instead of "opus". See https://mp4ra.org/#/codecs Fixes: #10066 --- Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 28 ++++++++++--------- Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs | 2 +- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index dfcccddfc2..888b667a68 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -198,11 +198,11 @@ public class DynamicHlsHelper var basicPlaylist = AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup); - // Provide a workaround for the case issue between flac and fLaC. - var flacWaPlaylist = ApplyFlacCaseWorkaround(state, basicPlaylist.ToString()); - if (!string.IsNullOrEmpty(flacWaPlaylist)) + // Provide a workaround for alternative codec string capitalization. + var alternativeCodecCapitalizationPlaylist = ApplyCodecCapitalizationWorkaround(state, basicPlaylist.ToString()); + if (!string.IsNullOrEmpty(alternativeCodecCapitalizationPlaylist)) { - builder.Append(flacWaPlaylist); + builder.Append(alternativeCodecCapitalizationPlaylist); } if (state.VideoStream is not null && state.VideoRequest is not null) @@ -238,11 +238,11 @@ public class DynamicHlsHelper var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate; var sdrPlaylist = AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup); - // Provide a workaround for the case issue between flac and fLaC. - flacWaPlaylist = ApplyFlacCaseWorkaround(state, sdrPlaylist.ToString()); - if (!string.IsNullOrEmpty(flacWaPlaylist)) + // Provide a workaround for alternative codec string capitalization. + alternativeCodecCapitalizationPlaylist = ApplyCodecCapitalizationWorkaround(state, sdrPlaylist.ToString()); + if (!string.IsNullOrEmpty(alternativeCodecCapitalizationPlaylist)) { - builder.Append(flacWaPlaylist); + builder.Append(alternativeCodecCapitalizationPlaylist); } // Restore the video codec @@ -275,11 +275,11 @@ public class DynamicHlsHelper var newPlaylist = ReplacePlaylistCodecsField(basicPlaylist, playlistCodecsField, newPlaylistCodecsField); builder.Append(newPlaylist); - // Provide a workaround for the case issue between flac and fLaC. - flacWaPlaylist = ApplyFlacCaseWorkaround(state, newPlaylist); - if (!string.IsNullOrEmpty(flacWaPlaylist)) + // Provide a workaround for alternative codec string capitalization. + alternativeCodecCapitalizationPlaylist = ApplyCodecCapitalizationWorkaround(state, newPlaylist); + if (!string.IsNullOrEmpty(alternativeCodecCapitalizationPlaylist)) { - builder.Append(flacWaPlaylist); + builder.Append(alternativeCodecCapitalizationPlaylist); } } } @@ -768,7 +768,7 @@ public class DynamicHlsHelper StringComparison.Ordinal); } - private string ApplyFlacCaseWorkaround(StreamState state, string srcPlaylist) + private string ApplyCodecCapitalizationWorkaround(StreamState state, string srcPlaylist) { if (!string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)) { @@ -779,6 +779,8 @@ public class DynamicHlsHelper newPlaylist = newPlaylist.Replace(",fLaC\"", ",flac\"", StringComparison.Ordinal); newPlaylist = newPlaylist.Replace("\"fLaC\"", "\"flac\"", StringComparison.Ordinal); + newPlaylist = newPlaylist.Replace(",Opus\"", ",opus\"", StringComparison.Ordinal); + newPlaylist = newPlaylist.Replace("\"Opus\"", "\"opus\"", StringComparison.Ordinal); return string.Equals(srcPlaylist, newPlaylist, StringComparison.Ordinal) ? string.Empty : newPlaylist; } diff --git a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs index 9b1c52045f..5eec1b0ca6 100644 --- a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs +++ b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs @@ -39,7 +39,7 @@ public static class HlsCodecStringHelpers /// <summary> /// Codec name for OPUS. /// </summary> - public const string OPUS = "opus"; + public const string OPUS = "Opus"; /// <summary> /// Gets a MP3 codec string. From 79cff704ff4e00895a8d2b97ecbbea3e2f5f56fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20M=C3=BCller?= <github@lonebyte.de> Date: Tue, 1 Aug 2023 17:28:49 +0200 Subject: [PATCH 480/858] Allow flac inside mp4 for all HLS audio streams The -strict -2 setting was only added if the encoder was set to 'copy'. If 'flac' is explicitly requested, we also need to set it, so that ffmpeg doesn't abort the conversion. Fixes: #10066 --- .../Controllers/DynamicHlsController.cs | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index ce684e457c..94093f1671 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1704,18 +1704,29 @@ public class DynamicHlsController : BaseJellyfinApiController var audioCodec = _encodingHelper.GetAudioEncoder(state); + // dts, flac, opus and truehd are experimental in mp4 muxer + var strictArgs = string.Empty; + var actualOutputAudioCodec = state.ActualOutputAudioCodec; + if (string.Equals(actualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase) + || string.Equals(actualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase) + || string.Equals(actualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase) + || string.Equals(actualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase)) + { + strictArgs = " -strict -2"; + } + if (!state.IsOutputVideo) { if (EncodingHelper.IsCopyCodec(audioCodec)) { var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container); - return "-acodec copy -strict -2" + bitStreamArgs; + return "-acodec copy" + bitStreamArgs + strictArgs; } var audioTranscodeParams = string.Empty; - audioTranscodeParams += "-acodec " + audioCodec; + audioTranscodeParams += "-acodec " + audioCodec + strictArgs; var audioBitrate = state.OutputAudioBitrate; var audioChannels = state.OutputAudioChannels; @@ -1747,17 +1758,6 @@ public class DynamicHlsController : BaseJellyfinApiController return audioTranscodeParams; } - // dts, flac, opus and truehd are experimental in mp4 muxer - var strictArgs = string.Empty; - var actualOutputAudioCodec = state.ActualOutputAudioCodec; - if (string.Equals(actualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase) - || string.Equals(actualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase) - || string.Equals(actualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase) - || string.Equals(actualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase)) - { - strictArgs = " -strict -2"; - } - if (EncodingHelper.IsCopyCodec(audioCodec)) { var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions); From 62be2a2ea95d1ea44989dc82c402aea8b43f404a Mon Sep 17 00:00:00 2001 From: sleepycatcoding <131554884+sleepycatcoding@users.noreply.github.com> Date: Sun, 23 Apr 2023 14:33:01 +0300 Subject: [PATCH 481/858] Fix subtitle encoder if webvtt is requested --- MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs | 2 +- MediaBrowser.Model/MediaInfo/SubtitleFormat.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 794906c3b4..a41e0b7e98 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -293,7 +293,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles return true; } - if (string.Equals(format, SubtitleFormat.VTT, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(format, SubtitleFormat.VTT, StringComparison.OrdinalIgnoreCase) || string.Equals(format, SubtitleFormat.WEBVTT, StringComparison.OrdinalIgnoreCase)) { value = new VttWriter(); return true; diff --git a/MediaBrowser.Model/MediaInfo/SubtitleFormat.cs b/MediaBrowser.Model/MediaInfo/SubtitleFormat.cs index 85de916940..c5a99a9ec0 100644 --- a/MediaBrowser.Model/MediaInfo/SubtitleFormat.cs +++ b/MediaBrowser.Model/MediaInfo/SubtitleFormat.cs @@ -9,6 +9,7 @@ namespace MediaBrowser.Model.MediaInfo public const string SSA = "ssa"; public const string ASS = "ass"; public const string VTT = "vtt"; + public const string WEBVTT = "webvtt"; public const string TTML = "ttml"; } } From f669d4a72e478f05c2b69d14ee6058690a0b9fbd Mon Sep 17 00:00:00 2001 From: sleepycatcoding <131554884+sleepycatcoding@users.noreply.github.com> Date: Sun, 23 Apr 2023 14:33:06 +0300 Subject: [PATCH 482/858] Add contributor --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index d5a87d2692..b7e7778176 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -167,6 +167,7 @@ - [ipitio](https://github.com/ipitio) - [TheTyrius](https://github.com/TheTyrius) - [tallbl0nde](https://github.com/tallbl0nde) + - [sleepycatcoding](https://github.com/sleepycatcoding) # Emby Contributors From 5b71cd8af9a4937ed72bfd50d44a2ad33d9edf76 Mon Sep 17 00:00:00 2001 From: nyanmisaka <nst799610810@gmail.com> Date: Thu, 3 Aug 2023 06:24:02 +0800 Subject: [PATCH 483/858] Expand AMD VA-API Vulkan filtering support to Polaris/gfx8 ROCm OpenCL runtime is not needed anymore when using HDR tone-mapping on Polaris/gfx8. This change requires jellyfin-ffmpeg5 5.1.3-4 or jellyfin-ffmpeg6 6.0-5 or newer versions. Signed-off-by: nyanmisaka <nst799610810@gmail.com> --- .../MediaEncoding/EncodingHelper.cs | 22 +++++++------------ .../MediaEncoding/IMediaEncoder.cs | 4 ++-- .../Encoder/MediaEncoder.cs | 14 +++++------- 3 files changed, 16 insertions(+), 24 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index a702e1003a..a2a20c2adc 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -37,7 +37,7 @@ namespace MediaBrowser.Controller.MediaEncoding private readonly IMediaEncoder _mediaEncoder; private readonly ISubtitleEncoder _subtitleEncoder; private readonly IConfiguration _config; - private readonly Version _minKernelVersionAmdVkFmtModifier = new Version(5, 15); + // i915 hang was fixed by linux 6.2 (3f882f2) private readonly Version _minKerneli915Hang = new Version(5, 18); private readonly Version _maxKerneli915Hang = new Version(6, 1, 3); @@ -892,8 +892,7 @@ namespace MediaBrowser.Controller.MediaEncoding else if (_mediaEncoder.IsVaapiDeviceAmd) { if (IsVulkanFullSupported() - && _mediaEncoder.IsVaapiDeviceSupportVulkanFmtModifier - && Environment.OSVersion.Version >= _minKernelVersionAmdVkFmtModifier) + && _mediaEncoder.IsVaapiDeviceSupportVulkanDrmInterop) { args.Append(GetDrmDeviceArgs(options.VaapiDevice, DrmAlias)); args.Append(GetVaapiDeviceArgs(null, null, null, DrmAlias, VaapiAlias)); @@ -4205,14 +4204,13 @@ namespace MediaBrowser.Controller.MediaEncoding // prefered vaapi + vulkan filters pipeline if (_mediaEncoder.IsVaapiDeviceAmd && isVaapiVkSupported - && _mediaEncoder.IsVaapiDeviceSupportVulkanFmtModifier - && Environment.OSVersion.Version >= _minKernelVersionAmdVkFmtModifier) + && _mediaEncoder.IsVaapiDeviceSupportVulkanDrmInterop) { - // AMD radeonsi path(Vega/gfx9+, kernel>=5.15), with extra vulkan tonemap and overlay support. + // AMD radeonsi path(targeting Polaris/gfx8+), with extra vulkan tonemap and overlay support. return GetAmdVaapiFullVidFiltersPrefered(state, options, vidDecoder, vidEncoder); } - // Intel i965 and Amd radeonsi/r600 path(Polaris/gfx8-), only featuring scale and deinterlace support. + // Intel i965 and Amd legacy driver path, only featuring scale and deinterlace support. return GetVaapiLimitedVidFiltersPrefered(state, options, vidDecoder, vidEncoder); } @@ -4484,7 +4482,7 @@ namespace MediaBrowser.Controller.MediaEncoding // INPUT vaapi surface(vram) if (doVkTonemap || hasSubs) { - // map from vaapi to vulkan/drm via interop (Vega/gfx9+). + // map from vaapi to vulkan/drm via interop (Polaris/gfx8+). mainFilters.Add("hwmap=derive_device=vulkan"); mainFilters.Add("format=vulkan"); } @@ -4513,9 +4511,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (doVkTonemap && !hasSubs) { // OUTPUT vaapi(nv12) surface(vram) - // map from vulkan/drm to vaapi via interop (Vega/gfx9+). - mainFilters.Add("hwmap=derive_device=drm"); - mainFilters.Add("format=drm_prime"); + // map from vulkan/drm to vaapi via interop (Polaris/gfx8+). mainFilters.Add("hwmap=derive_device=vaapi"); mainFilters.Add("format=vaapi"); @@ -4581,9 +4577,7 @@ namespace MediaBrowser.Controller.MediaEncoding else if (isVaapiEncoder) { // OUTPUT vaapi(nv12) surface(vram) - // map from vulkan/drm to vaapi via interop (Vega/gfx9+). - overlayFilters.Add("hwmap=derive_device=drm"); - overlayFilters.Add("format=drm_prime"); + // map from vulkan/drm to vaapi via interop (Polaris/gfx8+). overlayFilters.Add("hwmap=derive_device=vaapi"); overlayFilters.Add("format=vaapi"); diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index f830b9f298..4114dea4fc 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -64,8 +64,8 @@ namespace MediaBrowser.Controller.MediaEncoding /// <summary> /// Gets a value indicating whether the configured Vaapi device supports vulkan drm format modifier. /// </summary> - /// <value><c>true</c> if the Vaapi device supports vulkan drm format modifier, <c>false</c> otherwise.</value> - bool IsVaapiDeviceSupportVulkanFmtModifier { get; } + /// <value><c>true</c> if the Vaapi device supports vulkan drm interop, <c>false</c> otherwise.</value> + bool IsVaapiDeviceSupportVulkanDrmInterop { get; } /// <summary> /// Whether given encoder codec is supported. diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 0885fbe920..9d6cdf7282 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -76,12 +76,10 @@ namespace MediaBrowser.MediaEncoding.Encoder private bool _isVaapiDeviceAmd = false; private bool _isVaapiDeviceInteliHD = false; private bool _isVaapiDeviceInteli965 = false; - private bool _isVaapiDeviceSupportVulkanFmtModifier = false; + private bool _isVaapiDeviceSupportVulkanDrmInterop = false; - private static string[] _vulkanFmtModifierExts = + private static string[] _vulkanExternalMemoryDmaBufExts = { - "VK_KHR_sampler_ycbcr_conversion", - "VK_EXT_image_drm_format_modifier", "VK_KHR_external_memory_fd", "VK_EXT_external_memory_dma_buf", "VK_KHR_external_semaphore_fd", @@ -140,7 +138,7 @@ namespace MediaBrowser.MediaEncoding.Encoder public bool IsVaapiDeviceInteli965 => _isVaapiDeviceInteli965; /// <inheritdoc /> - public bool IsVaapiDeviceSupportVulkanFmtModifier => _isVaapiDeviceSupportVulkanFmtModifier; + public bool IsVaapiDeviceSupportVulkanDrmInterop => _isVaapiDeviceSupportVulkanDrmInterop; [GeneratedRegex(@"[^\/\\]+?(\.[^\/\\\n.]+)?$")] private static partial Regex FfprobePathRegex(); @@ -204,7 +202,7 @@ namespace MediaBrowser.MediaEncoding.Encoder _isVaapiDeviceAmd = validator.CheckVaapiDeviceByDriverName("Mesa Gallium driver", options.VaapiDevice); _isVaapiDeviceInteliHD = validator.CheckVaapiDeviceByDriverName("Intel iHD driver", options.VaapiDevice); _isVaapiDeviceInteli965 = validator.CheckVaapiDeviceByDriverName("Intel i965 driver", options.VaapiDevice); - _isVaapiDeviceSupportVulkanFmtModifier = validator.CheckVulkanDrmDeviceByExtensionName(options.VaapiDevice, _vulkanFmtModifierExts); + _isVaapiDeviceSupportVulkanDrmInterop = validator.CheckVulkanDrmDeviceByExtensionName(options.VaapiDevice, _vulkanExternalMemoryDmaBufExts); if (_isVaapiDeviceAmd) { @@ -219,9 +217,9 @@ namespace MediaBrowser.MediaEncoding.Encoder _logger.LogInformation("VAAPI device {RenderNodePath} is Intel GPU (i965)", options.VaapiDevice); } - if (_isVaapiDeviceSupportVulkanFmtModifier) + if (_isVaapiDeviceSupportVulkanDrmInterop) { - _logger.LogInformation("VAAPI device {RenderNodePath} supports Vulkan DRM format modifier", options.VaapiDevice); + _logger.LogInformation("VAAPI device {RenderNodePath} supports Vulkan DRM interop", options.VaapiDevice); } } } From 44946ded4e167cee8a0bc5f7f1f662e75be6e070 Mon Sep 17 00:00:00 2001 From: nyanmisaka <nst799610810@gmail.com> Date: Thu, 3 Aug 2023 19:16:46 +0800 Subject: [PATCH 484/858] Disable AMD EFC feature since it's still unstable in upstream Signed-off-by: nyanmisaka <nst799610810@gmail.com> --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index a2a20c2adc..d61430b0bc 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -891,6 +891,9 @@ namespace MediaBrowser.Controller.MediaEncoding } else if (_mediaEncoder.IsVaapiDeviceAmd) { + // Disable AMD EFC feature since it's still unstable in upstream Mesa. + Environment.SetEnvironmentVariable("AMD_DEBUG", "noefc"); + if (IsVulkanFullSupported() && _mediaEncoder.IsVaapiDeviceSupportVulkanDrmInterop) { From 28a6694f6eb227eed810dba55cc646b1d5aa9dac Mon Sep 17 00:00:00 2001 From: LJQ <leejunquan7@gmail.com> Date: Fri, 4 Aug 2023 21:19:08 +0800 Subject: [PATCH 485/858] Combined Title and Overview for multi-episodes files for the TMDB provider --- .../Plugins/Tmdb/TV/TmdbEpisodeProvider.cs | 48 +++++++++++++++++-- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs index f18575aa98..b25a15be51 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net.Http; +using System.Text; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Enums; @@ -13,6 +14,8 @@ using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; +using MediaBrowser.Providers.Music; +using TMDbLib.Objects.TvShows; namespace MediaBrowser.Providers.Plugins.Tmdb.TV { @@ -102,9 +105,48 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV return metadataResult; } - var episodeResult = await _tmdbClientManager - .GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken) - .ConfigureAwait(false); + var episodeResult = new TvEpisode(); + if (!info.IndexNumberEnd.HasValue) + { + episodeResult = await _tmdbClientManager + .GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken) + .ConfigureAwait(false); + } + else + { + var startindex = episodeNumber; + var endindex = info.IndexNumberEnd; + List<TvEpisode> result = new List<TvEpisode>(); + for (int? episode = startindex; episode <= endindex; episode++) + { + var episodeInfo = await _tmdbClientManager.GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episode.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken).ConfigureAwait(false); + if (episodeInfo is not null) + { + result.Add(episodeInfo); + } + } + + if (result.Count > 0) + { + episodeResult = result[0]; + } + else + { + return metadataResult; + } + + var name = new StringBuilder(episodeResult.Name); + var overview = new StringBuilder(episodeResult.Overview); + + for (int i = 1; i < result.Count; i++) + { + name.Append(" / " + result[i].Name); + overview.Append(" / " + result[i].Overview); + } + + episodeResult.Name = name.ToString(); + episodeResult.Overview = overview.ToString(); + } if (episodeResult is null) { From 0676b878f3e4709a86e5e397261536745311d303 Mon Sep 17 00:00:00 2001 From: LJQ <leejunquan7@gmail.com> Date: Fri, 4 Aug 2023 21:25:43 +0800 Subject: [PATCH 486/858] Removed unused imports --- MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs index b25a15be51..319364b395 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs @@ -14,7 +14,6 @@ using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; -using MediaBrowser.Providers.Music; using TMDbLib.Objects.TvShows; namespace MediaBrowser.Providers.Plugins.Tmdb.TV From 57c528192a897c3aa2c0a009054a8c118f739815 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 4 Aug 2023 18:03:59 +0000 Subject: [PATCH 487/858] chore(deps): update dependency microsoft.net.test.sdk to v17.7.0 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index de347f3a06..897a762f42 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -46,7 +46,7 @@ <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" /> <PackageVersion Include="Microsoft.Extensions.Logging" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Options" Version="7.0.1" /> - <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.6.3" /> + <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.7.0" /> <PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" /> <PackageVersion Include="MimeTypes" Version="2.4.0" /> <PackageVersion Include="Mono.Nat" Version="3.0.4" /> From 4271f7b5ad90abef51cded9957dbfd9f4d23c2a2 Mon Sep 17 00:00:00 2001 From: LJQ <leejunquan7@gmail.com> Date: Sat, 5 Aug 2023 13:29:31 +0800 Subject: [PATCH 488/858] Added me to CONTRIBUTORS.md --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index d5a87d2692..4009c01cc5 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -167,6 +167,7 @@ - [ipitio](https://github.com/ipitio) - [TheTyrius](https://github.com/TheTyrius) - [tallbl0nde](https://github.com/tallbl0nde) + - [scampower3](https://github.com/scampower3) # Emby Contributors From a0011886b0ddd3882f420cfef6f4406836911e10 Mon Sep 17 00:00:00 2001 From: LJQ <leejunquan7@gmail.com> Date: Sun, 6 Aug 2023 16:14:33 +0800 Subject: [PATCH 489/858] Fixes metadata refresh problems with NFO files --- MediaBrowser.Providers/Manager/MetadataService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index 834ef29f51..75291b3178 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -720,7 +720,7 @@ namespace MediaBrowser.Providers.Manager refreshResult.UpdateType |= ItemUpdateType.ImageUpdate; } - MergeData(localItem, temp, Array.Empty<MetadataField>(), !options.ReplaceAllMetadata, true); + MergeData(localItem, temp, Array.Empty<MetadataField>(), options.ReplaceAllMetadata, true); refreshResult.UpdateType |= ItemUpdateType.MetadataImport; // Only one local provider allowed per item From a09daa11eac1e7f47b88f2e27e8f61b3beb3b4ac Mon Sep 17 00:00:00 2001 From: scatter-dev <christian@christianlegge.dev> Date: Mon, 7 Aug 2023 14:03:36 -0400 Subject: [PATCH 490/858] cleaner regex formatting --- Emby.Naming/Common/NamingOptions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index 5cf0d90832..2bd089ed8f 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -318,7 +318,7 @@ namespace Emby.Naming.Common new EpisodeExpression(@"[\._ -]()[Ee][Pp]_?([0-9]+)([^\\/]*)$"), // <!-- foo.E01., foo.e01. --> new EpisodeExpression(@"[^\\/]*?()\.?[Ee]([0-9]+)\.([^\\/]*)$"), - new EpisodeExpression(@"(?<year>[0-9]{4})[\.\-_ ](?<month>[0-9]{2})[\.\-_ ](?<day>[0-9]{2})", true) + new EpisodeExpression(@"(?<year>[0-9]{4})[._ -](?<month>[0-9]{2})[._ -](?<day>[0-9]{2})", true) { DateTimeFormats = new[] { @@ -328,7 +328,7 @@ namespace Emby.Naming.Common "yyyy MM dd" } }, - new EpisodeExpression(@"(?<day>[0-9]{2})[\.\-_ ](?<month>[0-9]{2})[\.\-_ ](?<year>[0-9]{4})", true) + new EpisodeExpression(@"(?<day>[0-9]{2})[._ -](?<month>[0-9]{2})[._ -](?<year>[0-9]{4})", true) { DateTimeFormats = new[] { From 084e0bf450ac2ac6ffb756aa19ae096b4d7e19d4 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Tue, 8 Aug 2023 14:17:46 +0200 Subject: [PATCH 491/858] Fix error in test preventing Moq update (#10096) --- .../DefaultAuthorizationHandlerTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs index ad8a051fdc..f4f6611477 100644 --- a/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs @@ -66,7 +66,7 @@ namespace Jellyfin.Api.Tests.Auth.DefaultAuthorizationPolicy _userManagerMock .Setup(u => u.GetUserById(It.IsAny<Guid>())) - .Returns<User>(null); + .Returns<User?>(null); var claims = new[] { From 7f97b18d1605a39dfa989c969899b50c0e2f9cce Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 8 Aug 2023 16:38:12 +0000 Subject: [PATCH 492/858] chore(deps): update github/codeql-action action to v2.21.3 --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index fa32c1c0e9..c8882e3182 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@0ba4244466797eb048eb91a6cd43d5c03ca8bd05 # v2.21.2 + uses: github/codeql-action/init@5b6282e01c62d02e720b81eb8a51204f527c3624 # v2.21.3 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@0ba4244466797eb048eb91a6cd43d5c03ca8bd05 # v2.21.2 + uses: github/codeql-action/autobuild@5b6282e01c62d02e720b81eb8a51204f527c3624 # v2.21.3 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@0ba4244466797eb048eb91a6cd43d5c03ca8bd05 # v2.21.2 + uses: github/codeql-action/analyze@5b6282e01c62d02e720b81eb8a51204f527c3624 # v2.21.3 From 956f719724101e34d990738d464d4d2336d41ec3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 14 Aug 2023 15:49:02 +0000 Subject: [PATCH 493/858] chore(deps): update github/codeql-action action to v2.21.4 --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index c8882e3182..1f81a332d5 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@5b6282e01c62d02e720b81eb8a51204f527c3624 # v2.21.3 + uses: github/codeql-action/init@a09933a12a80f87b87005513f0abb1494c27a716 # v2.21.4 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@5b6282e01c62d02e720b81eb8a51204f527c3624 # v2.21.3 + uses: github/codeql-action/autobuild@a09933a12a80f87b87005513f0abb1494c27a716 # v2.21.4 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@5b6282e01c62d02e720b81eb8a51204f527c3624 # v2.21.3 + uses: github/codeql-action/analyze@a09933a12a80f87b87005513f0abb1494c27a716 # v2.21.4 From 769953ad55671b885ee4515e8242fe51c9e8b2ee Mon Sep 17 00:00:00 2001 From: Elu43 <45129108+Elu43@users.noreply.github.com> Date: Wed, 16 Aug 2023 15:33:58 +0200 Subject: [PATCH 494/858] Update fr.csv --- Emby.Server.Implementations/Localization/Ratings/fr.csv | 1 + 1 file changed, 1 insertion(+) diff --git a/Emby.Server.Implementations/Localization/Ratings/fr.csv b/Emby.Server.Implementations/Localization/Ratings/fr.csv index 774a705891..139ea376b7 100644 --- a/Emby.Server.Implementations/Localization/Ratings/fr.csv +++ b/Emby.Server.Implementations/Localization/Ratings/fr.csv @@ -1,5 +1,6 @@ Public Averti,0 Tous Publics,0 +TP,0 U,0 0+,0 6+,6 From a1b06bdcc82c7ec8bddf93b0f7c9303e2a5827fa Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 16 Aug 2023 17:19:44 +0000 Subject: [PATCH 495/858] chore(deps): update dependency microsoft.net.test.sdk to v17.7.1 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 897a762f42..6746bd9742 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -46,7 +46,7 @@ <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" /> <PackageVersion Include="Microsoft.Extensions.Logging" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Options" Version="7.0.1" /> - <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.7.0" /> + <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.7.1" /> <PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" /> <PackageVersion Include="MimeTypes" Version="2.4.0" /> <PackageVersion Include="Mono.Nat" Version="3.0.4" /> From 71e4ea1314b67712e9417293dbd7e90c925f2b0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20Bart=C3=ADk?= <63553146+sambartik@users.noreply.github.com> Date: Thu, 17 Aug 2023 01:07:14 +0200 Subject: [PATCH 496/858] Add Slovak parental ratings --- Emby.Server.Implementations/Localization/Ratings/sk.csv | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 Emby.Server.Implementations/Localization/Ratings/sk.csv diff --git a/Emby.Server.Implementations/Localization/Ratings/sk.csv b/Emby.Server.Implementations/Localization/Ratings/sk.csv new file mode 100644 index 0000000000..dbafd8efa3 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/sk.csv @@ -0,0 +1,6 @@ +NR,0 +U,0 +7,7 +12,12 +15,15 +18,18 From 65b269c151e083a7ed30b010a470e1fc63765904 Mon Sep 17 00:00:00 2001 From: Nick <20588554+nicknsy@users.noreply.github.com> Date: Wed, 16 Aug 2023 21:31:34 -0700 Subject: [PATCH 497/858] Minor code formatting (cvium) --- Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index 45e0d475d7..9b0b65b104 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -307,7 +307,7 @@ public class DynamicHlsHelper AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup); } - if (!isLiveStream && (state.VideoRequest?.EnableTrickplay).GetValueOrDefault(false)) + if (!isLiveStream && (state.VideoRequest?.EnableTrickplay ?? false)) { var sourceId = Guid.Parse(state.Request.MediaSourceId); var trickplayResolutions = await _trickplayManager.GetTrickplayResolutions(sourceId).ConfigureAwait(false); @@ -565,7 +565,7 @@ public class DynamicHlsHelper state.Request.MediaSourceId, user.GetToken()); - var line = string.Format( + builder.AppendFormat( CultureInfo.InvariantCulture, playlistFormat, trickplayInfo.Bandwidth.ToString(CultureInfo.InvariantCulture), @@ -573,7 +573,7 @@ public class DynamicHlsHelper trickplayInfo.Height.ToString(CultureInfo.InvariantCulture), url); - builder.AppendLine(line); + builder.AppendLine(); } } From 4c7fb8f45268c0123d0488dab23dff00c9578257 Mon Sep 17 00:00:00 2001 From: TelepathicWalrus <48514138+TelepathicWalrus@users.noreply.github.com> Date: Fri, 18 Aug 2023 09:25:56 +0100 Subject: [PATCH 498/858] Album gain (#10085) * Add LUFSAlbum DTO * Get loudest track for smallest gain * Move gain search to musicalbum use baseitem LUFS for album * Use .Max for enumerable * Update DTO to be consistent with other DTOs * Remove albumlufs, Move dto for all types --- Emby.Server.Implementations/Dto/DtoService.cs | 3 ++- MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 7a6ed2cb80..6d27703bdc 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -903,10 +903,11 @@ namespace Emby.Server.Implementations.Dto dto.IsPlaceHolder = supportsPlaceHolders.IsPlaceHolder; } + dto.LUFS = item.LUFS; + // Add audio info if (item is Audio audio) { - dto.LUFS = audio.LUFS; dto.Album = audio.Album; if (audio.ExtraType.HasValue) { diff --git a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs index 2dbd513a17..237345206f 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs @@ -183,6 +183,9 @@ namespace MediaBrowser.Controller.Entities.Audio progress.Report(percent * 95); } + // get album LUFS + LUFS = items.OfType<Audio>().Max(item => item.LUFS); + var parentRefreshOptions = refreshOptions; if (childUpdateType > ItemUpdateType.None) { From 2e32e335ac1571d6c118cd1f620f46e274d74c2d Mon Sep 17 00:00:00 2001 From: cvium <clausvium@gmail.com> Date: Fri, 18 Aug 2023 14:00:56 +0200 Subject: [PATCH 499/858] refactor: use ConcurrentDictionary when IMemoryCache isn't needed --- .../Library/LibraryManager.cs | 13 ++++++------- .../LiveTv/TunerHosts/BaseTunerHost.cs | 11 ++++++----- .../LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs | 5 ++--- .../LiveTv/TunerHosts/M3UTunerHost.cs | 5 ++--- 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 8bb2d3c028..8f88113b7f 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -3,6 +3,7 @@ #pragma warning disable CS1591 using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -63,7 +64,7 @@ namespace Emby.Server.Implementations.Library private const string ShortcutFileExtension = ".mblink"; private readonly ILogger<LibraryManager> _logger; - private readonly IMemoryCache _memoryCache; + private readonly ConcurrentDictionary<Guid, BaseItem> _cache; private readonly ITaskManager _taskManager; private readonly IUserManager _userManager; private readonly IUserDataManager _userDataRepository; @@ -111,7 +112,6 @@ namespace Emby.Server.Implementations.Library /// <param name="mediaEncoder">The media encoder.</param> /// <param name="itemRepository">The item repository.</param> /// <param name="imageProcessor">The image processor.</param> - /// <param name="memoryCache">The memory cache.</param> /// <param name="namingOptions">The naming options.</param> /// <param name="directoryService">The directory service.</param> public LibraryManager( @@ -128,7 +128,6 @@ namespace Emby.Server.Implementations.Library IMediaEncoder mediaEncoder, IItemRepository itemRepository, IImageProcessor imageProcessor, - IMemoryCache memoryCache, NamingOptions namingOptions, IDirectoryService directoryService) { @@ -145,7 +144,7 @@ namespace Emby.Server.Implementations.Library _mediaEncoder = mediaEncoder; _itemRepository = itemRepository; _imageProcessor = imageProcessor; - _memoryCache = memoryCache; + _cache = new ConcurrentDictionary<Guid, BaseItem>(); _namingOptions = namingOptions; _extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService); @@ -300,7 +299,7 @@ namespace Emby.Server.Implementations.Library } } - _memoryCache.Set(item.Id, item); + _cache[item.Id] = item; } public void DeleteItem(BaseItem item, DeleteOptions options) @@ -441,7 +440,7 @@ namespace Emby.Server.Implementations.Library _itemRepository.DeleteItem(child.Id); } - _memoryCache.Remove(item.Id); + _cache.TryRemove(item.Id, out _); ReportItemRemoved(item, parent); } @@ -1233,7 +1232,7 @@ namespace Emby.Server.Implementations.Library throw new ArgumentException("Guid can't be empty", nameof(id)); } - if (_memoryCache.TryGetValue(id, out BaseItem item)) + if (_cache.TryGetValue(id, out BaseItem item)) { return item; } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs index 7b6c8b80aa..1721be9e23 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs @@ -3,6 +3,7 @@ #pragma warning disable CS1591 using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; @@ -23,14 +24,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { public abstract class BaseTunerHost { - private readonly IMemoryCache _memoryCache; + private readonly ConcurrentDictionary<string, List<ChannelInfo>> _cache; - protected BaseTunerHost(IServerConfigurationManager config, ILogger<BaseTunerHost> logger, IFileSystem fileSystem, IMemoryCache memoryCache) + protected BaseTunerHost(IServerConfigurationManager config, ILogger<BaseTunerHost> logger, IFileSystem fileSystem) { Config = config; Logger = logger; - _memoryCache = memoryCache; FileSystem = fileSystem; + _cache = new ConcurrentDictionary<string, List<ChannelInfo>>(); } protected IServerConfigurationManager Config { get; } @@ -51,7 +52,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { var key = tuner.Id; - if (enableCache && !string.IsNullOrEmpty(key) && _memoryCache.TryGetValue(key, out List<ChannelInfo> cache)) + if (enableCache && !string.IsNullOrEmpty(key) && _cache.TryGetValue(key, out List<ChannelInfo> cache)) { return cache; } @@ -61,7 +62,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts if (!string.IsNullOrEmpty(key) && list.Count > 0) { - _memoryCache.Set(key, list); + _cache[key] = list; } return list; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index 1795e85a3c..7e588f6812 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -50,9 +50,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun IHttpClientFactory httpClientFactory, IServerApplicationHost appHost, ISocketFactory socketFactory, - IStreamHelper streamHelper, - IMemoryCache memoryCache) - : base(config, logger, fileSystem, memoryCache) + IStreamHelper streamHelper) + : base(config, logger, fileSystem) { _httpClientFactory = httpClientFactory; _appHost = appHost; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index acf3964c8c..613ea117f4 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -54,9 +54,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts IHttpClientFactory httpClientFactory, IServerApplicationHost appHost, INetworkManager networkManager, - IStreamHelper streamHelper, - IMemoryCache memoryCache) - : base(config, logger, fileSystem, memoryCache) + IStreamHelper streamHelper) + : base(config, logger, fileSystem) { _httpClientFactory = httpClientFactory; _appHost = appHost; From 8dc58e8f04ca891aa8e515c369f14ecbb21d2191 Mon Sep 17 00:00:00 2001 From: null <9310d27e@gmail.com> Date: Sun, 20 Aug 2023 21:18:55 +0400 Subject: [PATCH 500/858] Added handling of FFmpeg:probesize variable --- CONTRIBUTORS.md | 1 + .../MediaEncoding/EncodingHelper.cs | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index b7e7778176..e3af12a497 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -238,3 +238,4 @@ - [Jakob Kukla](https://github.com/jakobkukla) - [Utku Özdemir](https://github.com/utkuozdemir) - [JPUC1143](https://github.com/Jpuc1143/) + - [0x25CBFC4F](https://github.com/0x25CBFC4F) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index d61430b0bc..f8d2dd40fd 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -5678,7 +5678,6 @@ namespace MediaBrowser.Controller.MediaEncoding // Apply -analyzeduration as per the environment variable, // otherwise ffmpeg will break on certain files due to default value is 0. - // The default value of -probesize is more than enough, so leave it as is. var ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty; if (state.MediaSource.AnalyzeDurationMs > 0) @@ -5697,6 +5696,22 @@ namespace MediaBrowser.Controller.MediaEncoding inputModifier = inputModifier.Trim(); + // Apply -probesize if configured + var probeSizeArgument = string.Empty; + var ffmpegProbeSize = _config.GetFFmpegProbeSize(); + + if (!string.IsNullOrEmpty(ffmpegProbeSize)) + { + probeSizeArgument = $"-probesize {probeSizeArgument}"; + } + + if (!string.IsNullOrEmpty(probeSizeArgument)) + { + inputModifier += $" {probeSizeArgument}"; + } + + inputModifier = inputModifier.Trim(); + var userAgentParam = GetUserAgentParam(state); if (!string.IsNullOrEmpty(userAgentParam)) From 956e3dab43413798909a85a958231c3a16ac7b7f Mon Sep 17 00:00:00 2001 From: Claus Vium <cvium@users.noreply.github.com> Date: Sun, 20 Aug 2023 20:06:57 +0200 Subject: [PATCH 501/858] fix: accessing Standard* of a Process requires manually disposing them afterwards (#10125) --- Jellyfin.Api/Helpers/TranscodingJobHelper.cs | 2 +- .../MediaEncoding/JobLogger.cs | 4 +- .../Encoder/EncoderValidator.cs | 13 +-- .../Encoder/MediaEncoder.cs | 3 +- .../MediaInfo/AudioFileProber.cs | 3 +- .../FfProbe/FfProbeKeyframeExtractor.cs | 79 ++++++++++--------- 6 files changed, 56 insertions(+), 48 deletions(-) diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index cee8e0f9be..73ebb396d0 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -620,7 +620,7 @@ public class TranscodingJobHelper : IDisposable state.TranscodingJob = transcodingJob; // Important - don't await the log task or we won't be able to kill FFmpeg when the user stops playback - _ = new JobLogger(_logger).StartStreamingLog(state, process.StandardError.BaseStream, logStream); + _ = new JobLogger(_logger).StartStreamingLog(state, process.StandardError, logStream); // Wait for the file to exist before proceeding var ffmpegTargetFile = state.WaitForPath ?? outputPath; diff --git a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs index 3b34af4e96..3d288b9f86 100644 --- a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs +++ b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs @@ -20,12 +20,12 @@ namespace MediaBrowser.Controller.MediaEncoding _logger = logger; } - public async Task StartStreamingLog(EncodingJobInfo state, Stream source, Stream target) + public async Task StartStreamingLog(EncodingJobInfo state, StreamReader reader, Stream target) { try { using (target) - using (var reader = new StreamReader(source)) + using (reader) { while (!reader.EndOfStream && reader.BaseStream.CanRead) { diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index 38118ed0e1..db119ce5c5 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -553,7 +553,8 @@ namespace MediaBrowser.MediaEncoding.Encoder private string GetProcessOutput(string path, string arguments, bool readStdErr, string? testKey) { - using (var process = new Process() + var redirectStandardIn = !string.IsNullOrEmpty(testKey); + using (var process = new Process { StartInfo = new ProcessStartInfo(path, arguments) { @@ -561,7 +562,7 @@ namespace MediaBrowser.MediaEncoding.Encoder UseShellExecute = false, WindowStyle = ProcessWindowStyle.Hidden, ErrorDialog = false, - RedirectStandardInput = !string.IsNullOrEmpty(testKey), + RedirectStandardInput = redirectStandardIn, RedirectStandardOutput = true, RedirectStandardError = true } @@ -571,12 +572,14 @@ namespace MediaBrowser.MediaEncoding.Encoder process.Start(); - if (!string.IsNullOrEmpty(testKey)) + if (redirectStandardIn) { - process.StandardInput.Write(testKey); + using var writer = process.StandardInput; + writer.Write(testKey); } - return readStdErr ? process.StandardError.ReadToEnd() : process.StandardOutput.ReadToEnd(); + using var reader = readStdErr ? process.StandardError : process.StandardOutput; + return reader.ReadToEnd(); } } } diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 9d6cdf7282..346e97ae12 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -511,7 +511,8 @@ namespace MediaBrowser.MediaEncoding.Encoder using (var processWrapper = new ProcessWrapper(process, this)) { StartProcess(processWrapper); - await process.StandardOutput.BaseStream.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false); + using var reader = process.StandardOutput; + await reader.BaseStream.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false); memoryStream.Seek(0, SeekOrigin.Begin); InternalMediaInfoResult result; try diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index 9bcb1c39bd..44f998742f 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -130,7 +130,8 @@ namespace MediaBrowser.Providers.MediaInfo throw; } - output = await process.StandardError.ReadToEndAsync(cancellationToken).ConfigureAwait(false); + using var reader = process.StandardError; + output = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); MatchCollection split = LUFSRegex().Matches(output); diff --git a/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs b/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs index febe9516af..479e6ffdc8 100644 --- a/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs +++ b/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs @@ -68,51 +68,54 @@ public static class FfProbeKeyframeExtractor double streamDuration = 0; double formatDuration = 0; - while (!reader.EndOfStream) + using (reader) { - var line = reader.ReadLine().AsSpan(); - if (line.IsEmpty) + while (!reader.EndOfStream) { - continue; - } - - var firstComma = line.IndexOf(','); - var lineType = line[..firstComma]; - var rest = line[(firstComma + 1)..]; - if (lineType.Equals("packet", StringComparison.OrdinalIgnoreCase)) - { - // Split time and flags from the packet line. Example line: packet,7169.079000,K_ - var secondComma = rest.IndexOf(','); - var ptsTime = rest[..secondComma]; - var flags = rest[(secondComma + 1)..]; - if (flags.StartsWith("K_")) + var line = reader.ReadLine().AsSpan(); + if (line.IsEmpty) { - if (double.TryParse(ptsTime, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var keyframe)) + continue; + } + + var firstComma = line.IndexOf(','); + var lineType = line[..firstComma]; + var rest = line[(firstComma + 1)..]; + if (lineType.Equals("packet", StringComparison.OrdinalIgnoreCase)) + { + // Split time and flags from the packet line. Example line: packet,7169.079000,K_ + var secondComma = rest.IndexOf(','); + var ptsTime = rest[..secondComma]; + var flags = rest[(secondComma + 1)..]; + if (flags.StartsWith("K_")) { - // Have to manually convert to ticks to avoid rounding errors as TimeSpan is only precise down to 1 ms when converting double. - keyframes.Add(Convert.ToInt64(keyframe * TimeSpan.TicksPerSecond)); + if (double.TryParse(ptsTime, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var keyframe)) + { + // Have to manually convert to ticks to avoid rounding errors as TimeSpan is only precise down to 1 ms when converting double. + keyframes.Add(Convert.ToInt64(keyframe * TimeSpan.TicksPerSecond)); + } + } + } + else if (lineType.Equals("stream", StringComparison.OrdinalIgnoreCase)) + { + if (double.TryParse(rest, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var streamDurationResult)) + { + streamDuration = streamDurationResult; + } + } + else if (lineType.Equals("format", StringComparison.OrdinalIgnoreCase)) + { + if (double.TryParse(rest, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var formatDurationResult)) + { + formatDuration = formatDurationResult; } } } - else if (lineType.Equals("stream", StringComparison.OrdinalIgnoreCase)) - { - if (double.TryParse(rest, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var streamDurationResult)) - { - streamDuration = streamDurationResult; - } - } - else if (lineType.Equals("format", StringComparison.OrdinalIgnoreCase)) - { - if (double.TryParse(rest, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var formatDurationResult)) - { - formatDuration = formatDurationResult; - } - } + + // Prefer the stream duration as it should be more accurate + var duration = streamDuration > 0 ? streamDuration : formatDuration; + + return new KeyframeData(TimeSpan.FromSeconds(duration).Ticks, keyframes); } - - // Prefer the stream duration as it should be more accurate - var duration = streamDuration > 0 ? streamDuration : formatDuration; - - return new KeyframeData(TimeSpan.FromSeconds(duration).Ticks, keyframes); } } From 24201ef2e9bd33c1cde1289925910d0b8954a8eb Mon Sep 17 00:00:00 2001 From: 0TTA <OTTTA@protonmail.com> Date: Sat, 19 Aug 2023 15:34:13 +0000 Subject: [PATCH 502/858] Translated using Weblate (Arabic) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ar/ --- Emby.Server.Implementations/Localization/Core/ar.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json index 0e27dafe1b..93d50e6e3b 100644 --- a/Emby.Server.Implementations/Localization/Core/ar.json +++ b/Emby.Server.Implementations/Localization/Core/ar.json @@ -16,7 +16,7 @@ "Folders": "المجلدات", "Genres": "التصنيفات", "HeaderAlbumArtists": "فناني الألبوم", - "HeaderContinueWatching": "أستئناف المشاهدة", + "HeaderContinueWatching": "استئناف المشاهدة", "HeaderFavoriteAlbums": "الألبومات المفضلة", "HeaderFavoriteArtists": "الفنانون المفضلون", "HeaderFavoriteEpisodes": "الحلقات المفضلة", From 388420faa21eee90474e6c6723085f6df5192972 Mon Sep 17 00:00:00 2001 From: Muhammad Wafi Bin Arzu <muhdwafi037@gmail.com> Date: Sat, 19 Aug 2023 19:31:11 +0000 Subject: [PATCH 503/858] Translated using Weblate (Malay) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ms/ --- Emby.Server.Implementations/Localization/Core/ms.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/ms.json b/Emby.Server.Implementations/Localization/Core/ms.json index b2293e4b60..904443bea1 100644 --- a/Emby.Server.Implementations/Localization/Core/ms.json +++ b/Emby.Server.Implementations/Localization/Core/ms.json @@ -1,5 +1,5 @@ { - "Albums": "Album-album", + "Albums": "Albums", "AppDeviceValues": "Apl: {0}, Peranti: {1}", "Application": "Aplikasi", "Artists": "Artis-artis", From d3f8874a3e5ae9d6879a225e04a4ade922fc6c2d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 20 Aug 2023 21:36:58 -0400 Subject: [PATCH 504/858] chore(deps): update dotnet monorepo to v7.0.10 (#10099) * chore(deps): update dotnet monorepo to v7.0.10 * update dotnet sdk --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Bond_009 <bond.009@outlook.com> --- Directory.Packages.props | 16 ++++++++-------- deployment/Dockerfile.centos.amd64 | 2 +- deployment/Dockerfile.fedora.amd64 | 2 +- deployment/Dockerfile.ubuntu.amd64 | 2 +- deployment/Dockerfile.ubuntu.arm64 | 2 +- deployment/Dockerfile.ubuntu.armhf | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 6746bd9742..210d6b814c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -23,14 +23,14 @@ <PackageVersion Include="libse" Version="3.6.13" /> <PackageVersion Include="LrcParser" Version="2023.524.0" /> <PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" /> - <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.9" /> + <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.10" /> <PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" /> - <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.9" /> + <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.10" /> <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.9" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.9" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.9" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.9" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.10" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.10" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.10" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.10" /> <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" /> @@ -39,8 +39,8 @@ <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" /> - <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.9" /> - <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.9" /> + <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.10" /> + <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.10" /> <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Http" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" /> diff --git a/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64 index da986a07ec..28b6310ef1 100644 --- a/deployment/Dockerfile.centos.amd64 +++ b/deployment/Dockerfile.centos.amd64 @@ -13,7 +13,7 @@ RUN yum update -yq \ && yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget # Install DotNET SDK -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/0be7a87e-3a3f-4500-8301-49ccd6f24887/e9e36f35dbaf6625fec3e18f5c2b613f/dotnet-sdk-7.0.306-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/dbfe6cc7-dd82-4cec-b267-31ed988b1652/c60ab4793c3714be878abcb9aa834b63/dotnet-sdk-7.0.400-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.fedora.amd64 b/deployment/Dockerfile.fedora.amd64 index 09f93d41bf..8a52587141 100644 --- a/deployment/Dockerfile.fedora.amd64 +++ b/deployment/Dockerfile.fedora.amd64 @@ -12,7 +12,7 @@ RUN dnf update -yq \ && dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd wget make # Install DotNET SDK -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/0be7a87e-3a3f-4500-8301-49ccd6f24887/e9e36f35dbaf6625fec3e18f5c2b613f/dotnet-sdk-7.0.306-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/dbfe6cc7-dd82-4cec-b267-31ed988b1652/c60ab4793c3714be878abcb9aa834b63/dotnet-sdk-7.0.400-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64 index 9910773da5..e06179ac9b 100644 --- a/deployment/Dockerfile.ubuntu.amd64 +++ b/deployment/Dockerfile.ubuntu.amd64 @@ -17,7 +17,7 @@ RUN apt-get update -yqq \ libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/0be7a87e-3a3f-4500-8301-49ccd6f24887/e9e36f35dbaf6625fec3e18f5c2b613f/dotnet-sdk-7.0.306-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/dbfe6cc7-dd82-4cec-b267-31ed988b1652/c60ab4793c3714be878abcb9aa834b63/dotnet-sdk-7.0.400-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.arm64 b/deployment/Dockerfile.ubuntu.arm64 index aa69b27f49..964c3fca7b 100644 --- a/deployment/Dockerfile.ubuntu.arm64 +++ b/deployment/Dockerfile.ubuntu.arm64 @@ -16,7 +16,7 @@ RUN apt-get update -yqq \ mmv build-essential lsb-release # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/0be7a87e-3a3f-4500-8301-49ccd6f24887/e9e36f35dbaf6625fec3e18f5c2b613f/dotnet-sdk-7.0.306-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/dbfe6cc7-dd82-4cec-b267-31ed988b1652/c60ab4793c3714be878abcb9aa834b63/dotnet-sdk-7.0.400-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.armhf b/deployment/Dockerfile.ubuntu.armhf index bb8597b41f..4eb1343ac5 100644 --- a/deployment/Dockerfile.ubuntu.armhf +++ b/deployment/Dockerfile.ubuntu.armhf @@ -16,7 +16,7 @@ RUN apt-get update -yqq \ mmv build-essential lsb-release # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/0be7a87e-3a3f-4500-8301-49ccd6f24887/e9e36f35dbaf6625fec3e18f5c2b613f/dotnet-sdk-7.0.306-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/dbfe6cc7-dd82-4cec-b267-31ed988b1652/c60ab4793c3714be878abcb9aa834b63/dotnet-sdk-7.0.400-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet From 613f4296e395b0442984e472a996eaadc07915fe Mon Sep 17 00:00:00 2001 From: cvium <clausvium@gmail.com> Date: Mon, 21 Aug 2023 12:13:32 +0200 Subject: [PATCH 505/858] loading works --- Directory.Packages.props | 1 + .../Data/BaseSqliteRepository.cs | 104 +--- .../Data/ConnectionPool.cs | 79 --- .../Data/ManagedConnection.cs | 81 ---- .../Data/SqliteExtensions.cs | 394 ++++++--------- .../Data/SqliteItemRepository.cs | 167 +++---- .../Data/SqliteUserDataRepository.cs | 33 +- .../Emby.Server.Implementations.csproj | 2 +- .../Extensions/SqliteExtensions.cs | 449 ++++++++++++++++++ Jellyfin.Server/Jellyfin.Server.csproj | 2 +- .../Routines/MigrateActivityLogDb.cs | 1 + .../Routines/MigrateAuthenticationDb.cs | 1 + .../Routines/MigrateRatingLevels.cs | 3 +- .../Migrations/Routines/MigrateUserDb.cs | 1 + 14 files changed, 706 insertions(+), 612 deletions(-) delete mode 100644 Emby.Server.Implementations/Data/ConnectionPool.cs delete mode 100644 Emby.Server.Implementations/Data/ManagedConnection.cs create mode 100644 Jellyfin.Server/Extensions/SqliteExtensions.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 210d6b814c..bd2a1d1811 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -27,6 +27,7 @@ <PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" /> <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.10" /> <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" /> + <PackageVersion Include="Microsoft.Data.Sqlite" Version="7.0.10" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.10" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.10" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.10" /> diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs index d05534ee75..2ce87f5b46 100644 --- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs +++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs @@ -5,8 +5,8 @@ using System; using System.Collections.Generic; using Jellyfin.Extensions; +using Microsoft.Data.Sqlite; using Microsoft.Extensions.Logging; -using SQLitePCL.pretty; namespace Emby.Server.Implementations.Data { @@ -45,24 +45,6 @@ namespace Emby.Server.Implementations.Data /// <value>The logger.</value> protected ILogger<BaseSqliteRepository> Logger { get; } - /// <summary> - /// Gets the default connection flags. - /// </summary> - /// <value>The default connection flags.</value> - protected virtual ConnectionFlags DefaultConnectionFlags => ConnectionFlags.NoMutex; - - /// <summary> - /// Gets the transaction mode. - /// </summary> - /// <value>The transaction mode.</value>> - protected TransactionMode TransactionMode => TransactionMode.Deferred; - - /// <summary> - /// Gets the transaction mode for read-only operations. - /// </summary> - /// <value>The transaction mode.</value> - protected TransactionMode ReadTransactionMode => TransactionMode.Deferred; - /// <summary> /// Gets the cache size. /// </summary> @@ -107,23 +89,8 @@ namespace Emby.Server.Implementations.Data /// <see cref="SynchronousMode"/> protected virtual SynchronousMode? Synchronous => SynchronousMode.Normal; - /// <summary> - /// Gets or sets the write lock. - /// </summary> - /// <value>The write lock.</value> - protected ConnectionPool WriteConnections { get; set; } - - /// <summary> - /// Gets or sets the write connection. - /// </summary> - /// <value>The write connection.</value> - protected ConnectionPool ReadConnections { get; set; } - public virtual void Initialize() { - WriteConnections = new ConnectionPool(WriteConnectionsCount, CreateWriteConnection); - ReadConnections = new ConnectionPool(ReadConnectionsCount, CreateReadConnection); - // Configuration and pragmas can affect VACUUM so it needs to be last. using (var connection = GetConnection()) { @@ -131,15 +98,9 @@ namespace Emby.Server.Implementations.Data } } - protected ManagedConnection GetConnection(bool readOnly = false) - => readOnly ? ReadConnections.GetConnection() : WriteConnections.GetConnection(); - - protected SQLiteDatabaseConnection CreateWriteConnection() + protected SqliteConnection GetConnection(bool readOnly = false) { - var writeConnection = SQLite3.Open( - DbFilePath, - DefaultConnectionFlags | ConnectionFlags.Create | ConnectionFlags.ReadWrite, - null); + var writeConnection = new SqliteConnection($"Filename={DbFilePath}"); if (CacheSize.HasValue) { @@ -176,50 +137,14 @@ namespace Emby.Server.Implementations.Data return writeConnection; } - protected SQLiteDatabaseConnection CreateReadConnection() + public SqliteCommand PrepareStatement(SqliteConnection connection, string sql) { - var connection = SQLite3.Open( - DbFilePath, - DefaultConnectionFlags | ConnectionFlags.ReadOnly, - null); - - if (CacheSize.HasValue) - { - connection.Execute("PRAGMA cache_size=" + CacheSize.Value); - } - - if (!string.IsNullOrWhiteSpace(LockingMode)) - { - connection.Execute("PRAGMA locking_mode=" + LockingMode); - } - - if (!string.IsNullOrWhiteSpace(JournalMode)) - { - connection.Execute("PRAGMA journal_mode=" + JournalMode); - } - - if (JournalSizeLimit.HasValue) - { - connection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value); - } - - if (Synchronous.HasValue) - { - connection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value); - } - - connection.Execute("PRAGMA temp_store=" + (int)TempStore); - - return connection; + var command = connection.CreateCommand(); + command.CommandText = sql; + return command; } - public IStatement PrepareStatement(ManagedConnection connection, string sql) - => connection.PrepareStatement(sql); - - public IStatement PrepareStatement(IDatabaseConnection connection, string sql) - => connection.PrepareStatement(sql); - - protected bool TableExists(ManagedConnection connection, string name) + protected bool TableExists(SqliteConnection connection, string name) { return connection.RunInTransaction( db => @@ -236,11 +161,10 @@ namespace Emby.Server.Implementations.Data } return false; - }, - ReadTransactionMode); + }); } - protected List<string> GetColumnNames(IDatabaseConnection connection, string table) + protected List<string> GetColumnNames(SqliteConnection connection, string table) { var columnNames = new List<string>(); @@ -255,7 +179,7 @@ namespace Emby.Server.Implementations.Data return columnNames; } - protected void AddColumn(IDatabaseConnection connection, string table, string columnName, string type, List<string> existingColumnNames) + protected void AddColumn(SqliteConnection connection, string table, string columnName, string type, List<string> existingColumnNames) { if (existingColumnNames.Contains(columnName, StringComparison.OrdinalIgnoreCase)) { @@ -291,12 +215,6 @@ namespace Emby.Server.Implementations.Data return; } - if (dispose) - { - WriteConnections.Dispose(); - ReadConnections.Dispose(); - } - _disposed = true; } } diff --git a/Emby.Server.Implementations/Data/ConnectionPool.cs b/Emby.Server.Implementations/Data/ConnectionPool.cs deleted file mode 100644 index 5ea7e934ff..0000000000 --- a/Emby.Server.Implementations/Data/ConnectionPool.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using System.Collections.Concurrent; -using SQLitePCL.pretty; - -namespace Emby.Server.Implementations.Data; - -/// <summary> -/// A pool of SQLite Database connections. -/// </summary> -public sealed class ConnectionPool : IDisposable -{ - private readonly BlockingCollection<SQLiteDatabaseConnection> _connections = new(); - private bool _disposed; - - /// <summary> - /// Initializes a new instance of the <see cref="ConnectionPool" /> class. - /// </summary> - /// <param name="count">The number of database connection to create.</param> - /// <param name="factory">Factory function to create the database connections.</param> - public ConnectionPool(int count, Func<SQLiteDatabaseConnection> factory) - { - for (int i = 0; i < count; i++) - { - _connections.Add(factory.Invoke()); - } - } - - /// <summary> - /// Gets a database connection from the pool if one is available, otherwise blocks. - /// </summary> - /// <returns>A database connection.</returns> - public ManagedConnection GetConnection() - { - if (_disposed) - { - ThrowObjectDisposedException(); - } - - return new ManagedConnection(_connections.Take(), this); - - static void ThrowObjectDisposedException() - { - throw new ObjectDisposedException(nameof(ConnectionPool)); - } - } - - /// <summary> - /// Return a database connection to the pool. - /// </summary> - /// <param name="connection">The database connection to return.</param> - public void Return(SQLiteDatabaseConnection connection) - { - if (_disposed) - { - connection.Dispose(); - return; - } - - _connections.Add(connection); - } - - /// <inheritdoc /> - public void Dispose() - { - if (_disposed) - { - return; - } - - foreach (var connection in _connections) - { - connection.Dispose(); - } - - _connections.Dispose(); - - _disposed = true; - } -} diff --git a/Emby.Server.Implementations/Data/ManagedConnection.cs b/Emby.Server.Implementations/Data/ManagedConnection.cs deleted file mode 100644 index e84ed8f918..0000000000 --- a/Emby.Server.Implementations/Data/ManagedConnection.cs +++ /dev/null @@ -1,81 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using SQLitePCL.pretty; - -namespace Emby.Server.Implementations.Data -{ - public sealed class ManagedConnection : IDisposable - { - private readonly ConnectionPool _pool; - - private SQLiteDatabaseConnection _db; - - private bool _disposed = false; - - public ManagedConnection(SQLiteDatabaseConnection db, ConnectionPool pool) - { - _db = db; - _pool = pool; - } - - public IStatement PrepareStatement(string sql) - { - return _db.PrepareStatement(sql); - } - - public IEnumerable<IStatement> PrepareAll(string sql) - { - return _db.PrepareAll(sql); - } - - public void ExecuteAll(string sql) - { - _db.ExecuteAll(sql); - } - - public void Execute(string sql, params object[] values) - { - _db.Execute(sql, values); - } - - public void RunQueries(string[] sql) - { - _db.RunQueries(sql); - } - - public void RunInTransaction(Action<IDatabaseConnection> action, TransactionMode mode) - { - _db.RunInTransaction(action, mode); - } - - public T RunInTransaction<T>(Func<IDatabaseConnection, T> action, TransactionMode mode) - { - return _db.RunInTransaction(action, mode); - } - - public IEnumerable<IReadOnlyList<ResultSetValue>> Query(string sql) - { - return _db.Query(sql); - } - - public IEnumerable<IReadOnlyList<ResultSetValue>> Query(string sql, params object[] values) - { - return _db.Query(sql, values); - } - - public void Dispose() - { - if (_disposed) - { - return; - } - - _pool.Return(_db); - - _db = null!; // Don't dispose it - _disposed = true; - } - } -} diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs index 4055b0ba17..9e5cb17027 100644 --- a/Emby.Server.Implementations/Data/SqliteExtensions.cs +++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs @@ -1,11 +1,12 @@ -#nullable disable +#nullable enable #pragma warning disable CS1591 using System; using System.Collections.Generic; +using System.Data; using System.Diagnostics; using System.Globalization; -using SQLitePCL.pretty; +using Microsoft.Data.Sqlite; namespace Emby.Server.Implementations.Data { @@ -52,7 +53,68 @@ namespace Emby.Server.Implementations.Data "yy-MM-dd" }; - public static void RunQueries(this SQLiteDatabaseConnection connection, string[] queries) + private static void EnsureOpen(this SqliteConnection sqliteConnection) + { + if (sqliteConnection.State == ConnectionState.Closed) + { + sqliteConnection.Open(); + } + } + + public static IEnumerable<SqliteDataReader> Query(this SqliteConnection sqliteConnection, string commandText) + { + if (sqliteConnection.State != ConnectionState.Open) + { + sqliteConnection.Open(); + } + + var command = sqliteConnection.CreateCommand(); + command.CommandText = commandText; + using (var reader = command.ExecuteReader()) + { + while (reader.Read()) + { + yield return reader; + } + } + } + + public static void Execute(this SqliteConnection sqliteConnection, string commandText) + { + sqliteConnection.EnsureOpen(); + var command = sqliteConnection.CreateCommand(); + command.CommandText = commandText; + command.ExecuteNonQuery(); + } + + public static void RunInTransaction(this SqliteConnection sqliteConnection, Action<SqliteConnection> action) + { + sqliteConnection.EnsureOpen(); + + using var transaction = sqliteConnection.BeginTransaction(); + action(sqliteConnection); + transaction.Commit(); + } + + public static bool RunInTransaction(this SqliteConnection sqliteConnection, Func<SqliteConnection, bool> action) + { + sqliteConnection.EnsureOpen(); + using var transaction = sqliteConnection.BeginTransaction(); + var result = action(sqliteConnection); + transaction.Commit(); + return result; + } + + public static void ExecuteAll(this SqliteConnection sqliteConnection, string commandText) + { + sqliteConnection.EnsureOpen(); + + var command = sqliteConnection.CreateCommand(); + command.CommandText = commandText; + command.ExecuteNonQuery(); + } + + public static void RunQueries(this SqliteConnection connection, string[] queries) { ArgumentNullException.ThrowIfNull(queries); @@ -62,11 +124,6 @@ namespace Emby.Server.Implementations.Data }); } - public static Guid ReadGuidFromBlob(this ResultSetValue result) - { - return new Guid(result.ToBlob()); - } - public static string ToDateTimeParamValue(this DateTime dateValue) { var kind = DateTimeKind.Utc; @@ -83,27 +140,26 @@ namespace Emby.Server.Implementations.Data private static string GetDateTimeKindFormat(DateTimeKind kind) => (kind == DateTimeKind.Utc) ? DatetimeFormatUtc : DatetimeFormatLocal; - public static DateTime ReadDateTime(this ResultSetValue result) + public static DateTime ReadDateTime(this SqliteDataReader result) { var dateText = result.ToString(); return DateTime.ParseExact( - dateText, + dateText!, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AdjustToUniversal); } - public static bool TryReadDateTime(this IReadOnlyList<ResultSetValue> reader, int index, out DateTime result) + public static bool TryReadDateTime(this SqliteDataReader reader, int index, out DateTime result) { - var item = reader[index]; - if (item.IsDbNull()) + if (reader.IsDBNull(index)) { result = default; return false; } - var dateText = item.ToString(); + var dateText = reader.GetString(index); if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AdjustToUniversal, out var dateTimeResult)) { @@ -115,335 +171,175 @@ namespace Emby.Server.Implementations.Data return false; } - public static bool TryGetGuid(this IReadOnlyList<ResultSetValue> reader, int index, out Guid result) + public static bool TryGetGuid(this SqliteDataReader reader, int index, out Guid result) { - var item = reader[index]; - if (item.IsDbNull()) + if (reader.IsDBNull(index)) { result = default; return false; } - result = item.ReadGuidFromBlob(); + result = reader.GetGuid(index); return true; } - public static bool IsDbNull(this ResultSetValue result) + public static bool TryGetString(this SqliteDataReader reader, int index, out string result) { - return result.SQLiteType == SQLiteType.Null; - } + result = string.Empty; - public static string GetString(this IReadOnlyList<ResultSetValue> result, int index) - { - return result[index].ToString(); - } - - public static bool TryGetString(this IReadOnlyList<ResultSetValue> reader, int index, out string result) - { - result = null; - var item = reader[index]; - if (item.IsDbNull()) + if (reader.IsDBNull(index)) { return false; } - result = item.ToString(); + result = reader.GetString(index); return true; } - public static bool GetBoolean(this IReadOnlyList<ResultSetValue> result, int index) + public static bool TryGetBoolean(this SqliteDataReader reader, int index, out bool result) { - return result[index].ToBool(); - } - - public static bool TryGetBoolean(this IReadOnlyList<ResultSetValue> reader, int index, out bool result) - { - var item = reader[index]; - if (item.IsDbNull()) + if (reader.IsDBNull(index)) { result = default; return false; } - result = item.ToBool(); + result = reader.GetBoolean(index); return true; } - public static bool TryGetInt32(this IReadOnlyList<ResultSetValue> reader, int index, out int result) + public static bool TryGetInt32(this SqliteDataReader reader, int index, out int result) { - var item = reader[index]; - if (item.IsDbNull()) + if (reader.IsDBNull(index)) { result = default; return false; } - result = item.ToInt(); + result = reader.GetInt32(index); return true; } - public static long GetInt64(this IReadOnlyList<ResultSetValue> result, int index) + public static bool TryGetInt64(this SqliteDataReader reader, int index, out long result) { - return result[index].ToInt64(); - } - - public static bool TryGetInt64(this IReadOnlyList<ResultSetValue> reader, int index, out long result) - { - var item = reader[index]; - if (item.IsDbNull()) + if (reader.IsDBNull(index)) { result = default; return false; } - result = item.ToInt64(); + result = reader.GetInt64(index); return true; } - public static bool TryGetSingle(this IReadOnlyList<ResultSetValue> reader, int index, out float result) + public static bool TryGetSingle(this SqliteDataReader reader, int index, out float result) { - var item = reader[index]; - if (item.IsDbNull()) + if (reader.IsDBNull(index)) { result = default; return false; } - result = item.ToFloat(); + result = reader.GetFloat(index); return true; } - public static bool TryGetDouble(this IReadOnlyList<ResultSetValue> reader, int index, out double result) + public static bool TryGetDouble(this SqliteDataReader reader, int index, out double result) { - var item = reader[index]; - if (item.IsDbNull()) + if (reader.IsDBNull(index)) { result = default; return false; } - result = item.ToDouble(); + result = reader.GetDouble(index); return true; } - public static Guid GetGuid(this IReadOnlyList<ResultSetValue> result, int index) - { - return result[index].ReadGuidFromBlob(); - } - [Conditional("DEBUG")] private static void CheckName(string name) { throw new ArgumentException("Invalid param name: " + name, nameof(name)); } - public static void TryBind(this IStatement statement, string name, double value) + public static void TryBind(this SqliteCommand statement, string name, Guid value) { - if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) + if (statement.Parameters.Contains(name)) { - bindParam.Bind(value); + statement.Parameters[name].Value = value; } else { - CheckName(name); + statement.Parameters.Add(new SqliteParameter(name, SqliteType.Blob) { Value = value }); } } - public static void TryBind(this IStatement statement, string name, string value) + public static void TryBind(this SqliteCommand statement, string name, object? value) { - if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) + var preparedValue = value ?? DBNull.Value; + if (statement.Parameters.Contains(name)) { - if (value is null) + statement.Parameters[name].Value = preparedValue; + } + else + { + statement.Parameters.AddWithValue(name, preparedValue); + } + } + + public static void TryBind(this SqliteCommand statement, string name, byte[] value) + { + if (statement.Parameters.Contains(name)) + { + statement.Parameters[name].Value = value; + } + else + { + statement.Parameters.Add(new SqliteParameter(name, SqliteType.Blob, value.Length) { Value = value }); + } + } + + public static void TryBindNull(this SqliteCommand statement, string name) + { + statement.TryBind(name, DBNull.Value); + } + + public static IEnumerable<SqliteDataReader> ExecuteQuery(this SqliteCommand command) + { + using (var reader = command.ExecuteReader()) + { + while (reader.Read()) { - bindParam.BindNull(); - } - else - { - bindParam.Bind(value); + yield return reader; } } - else - { - CheckName(name); - } } - public static void TryBind(this IStatement statement, string name, bool value) + public static int SelectScalarInt(this SqliteCommand command) { - if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) - { - bindParam.Bind(value); - } - else - { - CheckName(name); - } + var result = command.ExecuteScalar(); + return Convert.ToInt32(result!, CultureInfo.InvariantCulture); } - public static void TryBind(this IStatement statement, string name, float value) + public static SqliteCommand PrepareStatement(this SqliteConnection sqliteConnection, string sql) { - if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) - { - bindParam.Bind(value); - } - else - { - CheckName(name); - } + sqliteConnection.EnsureOpen(); + var command = sqliteConnection.CreateCommand(); + command.CommandText = sql; + return command; } - public static void TryBind(this IStatement statement, string name, int value) + // Hacky + public static void MoveNext(this SqliteCommand sqliteCommand) { - if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) - { - bindParam.Bind(value); - } - else - { - CheckName(name); - } + sqliteCommand.Prepare(); + var result = sqliteCommand.ExecuteNonQuery(); } - public static void TryBind(this IStatement statement, string name, Guid value) + public static byte[] GetBlob(this SqliteDataReader reader, int index) { - if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) - { - Span<byte> byteValue = stackalloc byte[16]; - value.TryWriteBytes(byteValue); - bindParam.Bind(byteValue); - } - else - { - CheckName(name); - } - } - - public static void TryBind(this IStatement statement, string name, DateTime value) - { - if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) - { - bindParam.Bind(value.ToDateTimeParamValue()); - } - else - { - CheckName(name); - } - } - - public static void TryBind(this IStatement statement, string name, long value) - { - if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) - { - bindParam.Bind(value); - } - else - { - CheckName(name); - } - } - - public static void TryBind(this IStatement statement, string name, ReadOnlySpan<byte> value) - { - if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) - { - bindParam.Bind(value); - } - else - { - CheckName(name); - } - } - - public static void TryBindNull(this IStatement statement, string name) - { - if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) - { - bindParam.BindNull(); - } - else - { - CheckName(name); - } - } - - public static void TryBind(this IStatement statement, string name, DateTime? value) - { - if (value.HasValue) - { - TryBind(statement, name, value.Value); - } - else - { - TryBindNull(statement, name); - } - } - - public static void TryBind(this IStatement statement, string name, Guid? value) - { - if (value.HasValue) - { - TryBind(statement, name, value.Value); - } - else - { - TryBindNull(statement, name); - } - } - - public static void TryBind(this IStatement statement, string name, double? value) - { - if (value.HasValue) - { - TryBind(statement, name, value.Value); - } - else - { - TryBindNull(statement, name); - } - } - - public static void TryBind(this IStatement statement, string name, int? value) - { - if (value.HasValue) - { - TryBind(statement, name, value.Value); - } - else - { - TryBindNull(statement, name); - } - } - - public static void TryBind(this IStatement statement, string name, float? value) - { - if (value.HasValue) - { - TryBind(statement, name, value.Value); - } - else - { - TryBindNull(statement, name); - } - } - - public static void TryBind(this IStatement statement, string name, bool? value) - { - if (value.HasValue) - { - TryBind(statement, name, value.Value); - } - else - { - TryBindNull(statement, name); - } - } - - public static IEnumerable<IReadOnlyList<ResultSetValue>> ExecuteQuery(this IStatement statement) - { - while (statement.MoveNext()) - { - yield return statement.Current; - } + // Have to reset to casting as there isn't a publicly available GetBlob method + return (byte[])reader.GetValue(index); } } } diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 73ec856fc8..4ceaafa128 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -35,9 +35,9 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Querying; +using Microsoft.Data.Sqlite; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; -using SQLitePCL.pretty; namespace Emby.Server.Implementations.Data { @@ -555,8 +555,7 @@ namespace Emby.Server.Implementations.Data AddColumn(db, "MediaStreams", "DvBlSignalCompatibilityId", "INT", existingColumnNames); AddColumn(db, "MediaStreams", "IsHearingImpaired", "BIT", existingColumnNames); - }, - TransactionMode); + }); connection.RunQueries(postQueries); } @@ -580,8 +579,7 @@ namespace Emby.Server.Implementations.Data saveImagesStatement.MoveNext(); } - }, - TransactionMode); + }); } } @@ -624,12 +622,11 @@ namespace Emby.Server.Implementations.Data db => { SaveItemsInTransaction(db, tuples); - }, - TransactionMode); + }); } } - private void SaveItemsInTransaction(IDatabaseConnection db, IEnumerable<(BaseItem Item, List<Guid> AncestorIds, BaseItem TopParent, string UserDataKey, List<string> InheritedTags)> tuples) + private void SaveItemsInTransaction(SqliteConnection db, IEnumerable<(BaseItem Item, List<Guid> AncestorIds, BaseItem TopParent, string UserDataKey, List<string> InheritedTags)> tuples) { using (var saveItemStatement = PrepareStatement(db, SaveItemCommandText)) using (var deleteAncestorsStatement = PrepareStatement(db, "delete from AncestorIds where ItemId=@ItemId")) @@ -639,7 +636,7 @@ namespace Emby.Server.Implementations.Data { if (requiresReset) { - saveItemStatement.Reset(); + // TODO saveItemStatement.Parameters.Clear(); } var item = tuple.Item; @@ -677,7 +674,7 @@ namespace Emby.Server.Implementations.Data return _appHost.ExpandVirtualPath(path); } - private void SaveItem(BaseItem item, BaseItem topParent, string userDataKey, IStatement saveItemStatement) + private void SaveItem(BaseItem item, BaseItem topParent, string userDataKey, SqliteCommand saveItemStatement) { Type type = item.GetType(); @@ -1389,12 +1386,12 @@ namespace Emby.Server.Implementations.Data return true; } - private BaseItem GetItem(IReadOnlyList<ResultSetValue> reader, InternalItemsQuery query) + private BaseItem GetItem(SqliteDataReader reader, InternalItemsQuery query) { return GetItem(reader, query, HasProgramAttributes(query), HasEpisodeAttributes(query), HasServiceName(query), HasStartDate(query), HasTrailerTypes(query), HasArtistFields(query), HasSeriesFields(query)); } - private BaseItem GetItem(IReadOnlyList<ResultSetValue> reader, InternalItemsQuery query, bool enableProgramAttributes, bool hasEpisodeAttributes, bool hasServiceName, bool queryHasStartDate, bool hasTrailerTypes, bool hasArtistFields, bool hasSeriesFields) + private BaseItem GetItem(SqliteDataReader reader, InternalItemsQuery query, bool enableProgramAttributes, bool hasEpisodeAttributes, bool hasServiceName, bool queryHasStartDate, bool hasTrailerTypes, bool hasArtistFields, bool hasSeriesFields) { var typeString = reader.GetString(0); @@ -1411,7 +1408,7 @@ namespace Emby.Server.Implementations.Data { try { - item = JsonSerializer.Deserialize(reader[1].ToBlob(), type, _jsonOptions) as BaseItem; + item = JsonSerializer.Deserialize(reader.GetStream(1), type, _jsonOptions) as BaseItem; } catch (JsonException ex) { @@ -1452,17 +1449,9 @@ namespace Emby.Server.Implementations.Data item.EndDate = endDate; } - var channelId = reader[index]; - if (!channelId.IsDbNull()) + if (reader.TryGetGuid(index, out var guid)) { - if (!Utf8Parser.TryParse(channelId.ToBlob(), out Guid value, out _, standardFormat: 'N')) - { - var str = reader.GetString(index); - Logger.LogWarning("{ChannelId} isn't in the expected format", str); - value = new Guid(str); - } - - item.ChannelId = value; + item.ChannelId = guid; } index++; @@ -2018,7 +2007,7 @@ namespace Emby.Server.Implementations.Data /// <param name="reader">The reader.</param> /// <param name="item">The item.</param> /// <returns>ChapterInfo.</returns> - private ChapterInfo GetChapter(IReadOnlyList<ResultSetValue> reader, BaseItem item) + private ChapterInfo GetChapter(SqliteDataReader reader, BaseItem item) { var chapter = new ChapterInfo { @@ -2071,23 +2060,22 @@ namespace Emby.Server.Implementations.Data ArgumentNullException.ThrowIfNull(chapters); - var idBlob = id.ToByteArray(); - using (var connection = GetConnection()) { connection.RunInTransaction( db => { // First delete chapters - db.Execute("delete from " + ChaptersTableName + " where ItemId=@ItemId", idBlob); + var command = db.PrepareStatement($"delete from {ChaptersTableName} where ItemId=@ItemId"); + command.TryBind("@ItemId", id); + command.ExecuteNonQuery(); - InsertChapters(idBlob, chapters, db); - }, - TransactionMode); + InsertChapters(id, chapters, db); + }); } } - private void InsertChapters(byte[] idBlob, IReadOnlyList<ChapterInfo> chapters, IDatabaseConnection db) + private void InsertChapters(Guid idBlob, IReadOnlyList<ChapterInfo> chapters, SqliteConnection db) { var startIndex = 0; var limit = 100; @@ -2126,7 +2114,7 @@ namespace Emby.Server.Implementations.Data chapterIndex++; } - statement.Reset(); + // TODO statement.Parameters.Clear(); statement.MoveNext(); } @@ -2463,7 +2451,7 @@ namespace Emby.Server.Implementations.Data } } - private void BindSearchParams(InternalItemsQuery query, IStatement statement) + private void BindSearchParams(InternalItemsQuery query, SqliteCommand statement) { var searchTerm = query.SearchTerm; @@ -2475,7 +2463,7 @@ namespace Emby.Server.Implementations.Data searchTerm = FixUnicodeChars(searchTerm); searchTerm = GetCleanValue(searchTerm); - var commandText = statement.SQL; + var commandText = statement.CommandText; if (commandText.Contains("@SearchTermStartsWith", StringComparison.OrdinalIgnoreCase)) { statement.TryBind("@SearchTermStartsWith", searchTerm + "%"); @@ -2492,7 +2480,7 @@ namespace Emby.Server.Implementations.Data } } - private void BindSimilarParams(InternalItemsQuery query, IStatement statement) + private void BindSimilarParams(InternalItemsQuery query, SqliteCommand statement) { var item = query.SimilarTo; @@ -2501,7 +2489,7 @@ namespace Emby.Server.Implementations.Data return; } - var commandText = statement.SQL; + var commandText = statement.CommandText; if (commandText.Contains("@ItemOfficialRating", StringComparison.OrdinalIgnoreCase)) { @@ -2598,7 +2586,7 @@ namespace Emby.Server.Implementations.Data // Running this again will bind the params GetWhereClauses(query, statement); - return statement.ExecuteQuery().SelectScalarInt().First(); + return statement.SelectScalarInt(); } } @@ -2916,11 +2904,10 @@ namespace Emby.Server.Implementations.Data // Running this again will bind the params GetWhereClauses(query, statement); - result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First(); + result.TotalRecordCount = statement.SelectScalarInt(); } } - }, - ReadTransactionMode); + }); } result.StartIndex = query.StartIndex ?? 0; @@ -3188,7 +3175,7 @@ namespace Emby.Server.Implementations.Data foreach (var row in statement.ExecuteQuery()) { - list.Add(row[0].ReadGuidFromBlob()); + list.Add(row.GetGuid(0)); } } @@ -3224,7 +3211,7 @@ namespace Emby.Server.Implementations.Data } #nullable enable - private List<string> GetWhereClauses(InternalItemsQuery query, IStatement? statement) + private List<string> GetWhereClauses(InternalItemsQuery query, SqliteCommand? statement) { if (query.IsResumable ?? false) { @@ -3647,8 +3634,7 @@ namespace Emby.Server.Implementations.Data if (statement is not null) { - query.PersonIds[i].TryWriteBytes(idBytes); - statement.TryBind(paramName, idBytes); + statement.TryBind(paramName, query.PersonIds[i]); } } @@ -4696,8 +4682,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type db => { connection.ExecuteAll(sql); - }, - TransactionMode); + }); } } @@ -4735,16 +4720,15 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type // Delete the item ExecuteWithSingleParam(db, "delete from TypedBaseItems where guid=@Id", idBlob); - }, - TransactionMode); + }); } } - private void ExecuteWithSingleParam(IDatabaseConnection db, string query, ReadOnlySpan<byte> value) + private void ExecuteWithSingleParam(SqliteConnection db, string query, ReadOnlySpan<byte> value) { using (var statement = PrepareStatement(db, query)) { - statement.TryBind("@Id", value); + statement.TryBind("@Id", value.ToArray()); statement.MoveNext(); } @@ -4826,7 +4810,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type return list; } - private List<string> GetPeopleWhereClauses(InternalPeopleQuery query, IStatement statement) + private List<string> GetPeopleWhereClauses(InternalPeopleQuery query, SqliteCommand statement) { var whereClauses = new List<string>(); @@ -4896,7 +4880,7 @@ AND Type = @InternalPersonType)"); return whereClauses; } - private void UpdateAncestors(Guid itemId, List<Guid> ancestorIds, IDatabaseConnection db, IStatement deleteAncestorsStatement) + private void UpdateAncestors(Guid itemId, List<Guid> ancestorIds, SqliteConnection db, SqliteCommand deleteAncestorsStatement) { if (itemId.Equals(default)) { @@ -4907,12 +4891,14 @@ AND Type = @InternalPersonType)"); CheckDisposed(); - Span<byte> itemIdBlob = stackalloc byte[16]; - itemId.TryWriteBytes(itemIdBlob); + // TODO how to handle span? + Span<byte> itemIdBlob2 = stackalloc byte[16]; + itemId.TryWriteBytes(itemIdBlob2); + var itemIdBlob = Encoding.ASCII.GetBytes(itemId.ToString()); // First delete - deleteAncestorsStatement.Reset(); - deleteAncestorsStatement.TryBind("@ItemId", itemIdBlob); + // TODO deleteAncestorsStatement.Parameters.Clear(); + deleteAncestorsStatement.TryBind("@ItemId", itemId); deleteAncestorsStatement.MoveNext(); if (ancestorIds.Count == 0) @@ -4942,13 +4928,13 @@ AND Type = @InternalPersonType)"); var index = i.ToString(CultureInfo.InvariantCulture); var ancestorId = ancestorIds[i]; - ancestorId.TryWriteBytes(itemIdBlob); + itemIdBlob = Encoding.ASCII.GetBytes(itemId.ToString()); - statement.TryBind("@AncestorId" + index, itemIdBlob); + statement.TryBind("@AncestorId" + index, ancestorId); statement.TryBind("@AncestorIdText" + index, ancestorId.ToString("N", CultureInfo.InvariantCulture)); } - statement.Reset(); + // TODO statement.Parameters.Clear(); statement.MoveNext(); } } @@ -5323,11 +5309,10 @@ AND Type = @InternalPersonType)"); GetWhereClauses(innerQuery, statement); GetWhereClauses(outerQuery, statement); - result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First(); + result.TotalRecordCount = statement.SelectScalarInt(); } } - }, - ReadTransactionMode); + }); } if (result.TotalRecordCount == 0) @@ -5341,7 +5326,7 @@ AND Type = @InternalPersonType)"); return result; } - private static ItemCounts GetItemCounts(IReadOnlyList<ResultSetValue> reader, int countStartColumn, BaseItemKind[] typesToCount) + private static ItemCounts GetItemCounts(SqliteDataReader reader, int countStartColumn, BaseItemKind[] typesToCount) { var counts = new ItemCounts(); @@ -5420,7 +5405,7 @@ AND Type = @InternalPersonType)"); return list; } - private void UpdateItemValues(Guid itemId, List<(int MagicNumber, string Value)> values, IDatabaseConnection db) + private void UpdateItemValues(Guid itemId, List<(int MagicNumber, string Value)> values, SqliteConnection db) { if (itemId.Equals(default)) { @@ -5434,12 +5419,14 @@ AND Type = @InternalPersonType)"); var guidBlob = itemId.ToByteArray(); // First delete - db.Execute("delete from ItemValues where ItemId=@Id", guidBlob); + using var command = db.PrepareStatement("delete from ItemValues where ItemId=@Id"); + command.TryBind("@Id", guidBlob); + command.ExecuteNonQuery(); InsertItemValues(guidBlob, values, db); } - private void InsertItemValues(byte[] idBlob, List<(int MagicNumber, string Value)> values, IDatabaseConnection db) + private void InsertItemValues(byte[] idBlob, List<(int MagicNumber, string Value)> values, SqliteConnection db) { const int Limit = 100; var startIndex = 0; @@ -5484,7 +5471,7 @@ AND Type = @InternalPersonType)"); statement.TryBind("@CleanValue" + index, GetCleanValue(itemValue)); } - statement.Reset(); + // TODO statement.Parameters.Clear(); statement.MoveNext(); } @@ -5512,15 +5499,17 @@ AND Type = @InternalPersonType)"); var itemIdBlob = itemId.ToByteArray(); // First delete chapters - db.Execute("delete from People where ItemId=@ItemId", itemIdBlob); + using var command = db.CreateCommand(); + command.CommandText = "delete from People where ItemId=@ItemId"; + command.TryBind("@ItemId", itemIdBlob); + command.ExecuteNonQuery(); InsertPeople(itemIdBlob, people, db); - }, - TransactionMode); + }); } } - private void InsertPeople(byte[] idBlob, List<PersonInfo> people, IDatabaseConnection db) + private void InsertPeople(byte[] idBlob, List<PersonInfo> people, SqliteConnection db) { const int Limit = 100; var startIndex = 0; @@ -5561,7 +5550,6 @@ AND Type = @InternalPersonType)"); listIndex++; } - statement.Reset(); statement.MoveNext(); } @@ -5570,7 +5558,7 @@ AND Type = @InternalPersonType)"); } } - private PersonInfo GetPerson(IReadOnlyList<ResultSetValue> reader) + private PersonInfo GetPerson(SqliteDataReader reader) { var item = new PersonInfo { @@ -5666,15 +5654,16 @@ AND Type = @InternalPersonType)"); var itemIdBlob = id.ToByteArray(); // Delete existing mediastreams - db.Execute("delete from mediastreams where ItemId=@ItemId", itemIdBlob); + using var command = db.PrepareStatement("delete from mediastreams where ItemId=@ItemId"); + command.TryBind("@ItemId", itemIdBlob); + command.ExecuteNonQuery(); InsertMediaStreams(itemIdBlob, streams, db); - }, - TransactionMode); + }); } } - private void InsertMediaStreams(byte[] idBlob, IReadOnlyList<MediaStream> streams, IDatabaseConnection db) + private void InsertMediaStreams(byte[] idBlob, IReadOnlyList<MediaStream> streams, SqliteConnection db) { const int Limit = 10; var startIndex = 0; @@ -5770,7 +5759,7 @@ AND Type = @InternalPersonType)"); statement.TryBind("@IsHearingImpaired" + index, stream.IsHearingImpaired); } - statement.Reset(); + // TODO statement.Parameters.Clear(); statement.MoveNext(); } @@ -5784,15 +5773,14 @@ AND Type = @InternalPersonType)"); /// </summary> /// <param name="reader">The reader.</param> /// <returns>MediaStream.</returns> - private MediaStream GetMediaStream(IReadOnlyList<ResultSetValue> reader) + private MediaStream GetMediaStream(SqliteDataReader reader) { var item = new MediaStream { - Index = reader[1].ToInt() + Index = reader.GetInt32(1), + Type = Enum.Parse<MediaStreamType>(reader.GetString(2), true) }; - item.Type = Enum.Parse<MediaStreamType>(reader[2].ToString(), true); - if (reader.TryGetString(3, out var codec)) { item.Codec = codec; @@ -6050,18 +6038,19 @@ AND Type = @InternalPersonType)"); { var itemIdBlob = id.ToByteArray(); - db.Execute("delete from mediaattachments where ItemId=@ItemId", itemIdBlob); + using var command = db.PrepareStatement("delete from mediaattachments where ItemId=@ItemId"); + command.TryBind("@ItemId", itemIdBlob); + command.ExecuteNonQuery(); InsertMediaAttachments(itemIdBlob, attachments, db, cancellationToken); - }, - TransactionMode); + }); } } private void InsertMediaAttachments( byte[] idBlob, IReadOnlyList<MediaAttachment> attachments, - IDatabaseConnection db, + SqliteConnection db, CancellationToken cancellationToken) { const int InsertAtOnce = 10; @@ -6111,7 +6100,7 @@ AND Type = @InternalPersonType)"); statement.TryBind("@MIMEType" + index, attachment.MimeType); } - statement.Reset(); + // TODO statement.Parameters.Clear(); statement.MoveNext(); } @@ -6124,11 +6113,11 @@ AND Type = @InternalPersonType)"); /// </summary> /// <param name="reader">The reader.</param> /// <returns>MediaAttachment.</returns> - private MediaAttachment GetMediaAttachment(IReadOnlyList<ResultSetValue> reader) + private MediaAttachment GetMediaAttachment(SqliteDataReader reader) { var item = new MediaAttachment { - Index = reader[1].ToInt() + Index = reader.GetInt32(1) }; if (reader.TryGetString(2, out var codec)) diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs index a1e217ad14..bc3863a65f 100644 --- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs @@ -11,8 +11,8 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; +using Microsoft.Data.Sqlite; using Microsoft.Extensions.Logging; -using SQLitePCL.pretty; namespace Emby.Server.Implementations.Data { @@ -80,12 +80,11 @@ namespace Emby.Server.Implementations.Data db.ExecuteAll("INSERT INTO UserDatas (key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex) SELECT key, InternalUserId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex from userdata where InternalUserId not null"); } } - }, - TransactionMode); + }); } } - private void ImportUserIds(IDatabaseConnection db, IEnumerable<User> users) + private void ImportUserIds(SqliteConnection db, IEnumerable<User> users) { var userIdsWithUserData = GetAllUserIdsWithUserData(db); @@ -100,14 +99,14 @@ namespace Emby.Server.Implementations.Data statement.TryBind("@UserId", user.Id); statement.TryBind("@InternalUserId", user.InternalId); + statement.Prepare(); - statement.MoveNext(); - statement.Reset(); + statement.ExecuteNonQuery(); } } } - private List<Guid> GetAllUserIdsWithUserData(IDatabaseConnection db) + private List<Guid> GetAllUserIdsWithUserData(SqliteConnection db) { var list = new List<Guid>(); @@ -117,7 +116,7 @@ namespace Emby.Server.Implementations.Data { try { - list.Add(row[0].ReadGuidFromBlob()); + list.Add(row.GetGuid(0)); } catch (Exception ex) { @@ -174,12 +173,11 @@ namespace Emby.Server.Implementations.Data db => { SaveUserData(db, internalUserId, key, userData); - }, - TransactionMode); + }); } } - private static void SaveUserData(IDatabaseConnection db, long internalUserId, string key, UserItemData userData) + private static void SaveUserData(SqliteConnection db, long internalUserId, string key, UserItemData userData) { using (var statement = db.PrepareStatement("replace into UserDatas (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)")) { @@ -247,8 +245,7 @@ namespace Emby.Server.Implementations.Data { SaveUserData(db, internalUserId, userItemData.Key, userItemData); } - }, - TransactionMode); + }); } } @@ -336,7 +333,7 @@ namespace Emby.Server.Implementations.Data /// </summary> /// <param name="reader">The list of result set values.</param> /// <returns>The user item data.</returns> - private UserItemData ReadRow(IReadOnlyList<ResultSetValue> reader) + private UserItemData ReadRow(SqliteDataReader reader) { var userData = new UserItemData(); @@ -348,10 +345,10 @@ namespace Emby.Server.Implementations.Data userData.Rating = rating; } - userData.Played = reader[3].ToBool(); - userData.PlayCount = reader[4].ToInt(); - userData.IsFavorite = reader[5].ToBool(); - userData.PlaybackPositionTicks = reader[6].ToInt64(); + userData.Played = reader.GetBoolean(3); + userData.PlayCount = reader.GetInt32(4); + userData.IsFavorite = reader.GetBoolean(5); + userData.PlaybackPositionTicks = reader.GetInt64(6); if (reader.TryReadDateTime(7, out var lastPlayedDate)) { diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index b8655c7600..3aab0a5e9d 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -24,6 +24,7 @@ <ItemGroup> <PackageReference Include="DiscUtils.Udf" /> <PackageReference Include="Jellyfin.XmlTv" /> + <PackageReference Include="Microsoft.Data.Sqlite" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" /> @@ -31,7 +32,6 @@ <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" /> <PackageReference Include="Mono.Nat" /> <PackageReference Include="prometheus-net.DotNetRuntime" /> - <PackageReference Include="SQLitePCL.pretty.netstandard" /> <PackageReference Include="DotNet.Glob" /> </ItemGroup> diff --git a/Jellyfin.Server/Extensions/SqliteExtensions.cs b/Jellyfin.Server/Extensions/SqliteExtensions.cs new file mode 100644 index 0000000000..0e6a1bb13f --- /dev/null +++ b/Jellyfin.Server/Extensions/SqliteExtensions.cs @@ -0,0 +1,449 @@ +#nullable disable +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using SQLitePCL.pretty; + +namespace Jellyfin.Server.Extensions +{ + public static class SqliteExtensions + { + private const string DatetimeFormatUtc = "yyyy-MM-dd HH:mm:ss.FFFFFFFK"; + private const string DatetimeFormatLocal = "yyyy-MM-dd HH:mm:ss.FFFFFFF"; + + /// <summary> + /// An array of ISO-8601 DateTime formats that we support parsing. + /// </summary> + private static readonly string[] _datetimeFormats = new string[] + { + "THHmmssK", + "THHmmK", + "HH:mm:ss.FFFFFFFK", + "HH:mm:ssK", + "HH:mmK", + DatetimeFormatUtc, + "yyyy-MM-dd HH:mm:ssK", + "yyyy-MM-dd HH:mmK", + "yyyy-MM-ddTHH:mm:ss.FFFFFFFK", + "yyyy-MM-ddTHH:mmK", + "yyyy-MM-ddTHH:mm:ssK", + "yyyyMMddHHmmssK", + "yyyyMMddHHmmK", + "yyyyMMddTHHmmssFFFFFFFK", + "THHmmss", + "THHmm", + "HH:mm:ss.FFFFFFF", + "HH:mm:ss", + "HH:mm", + DatetimeFormatLocal, + "yyyy-MM-dd HH:mm:ss", + "yyyy-MM-dd HH:mm", + "yyyy-MM-ddTHH:mm:ss.FFFFFFF", + "yyyy-MM-ddTHH:mm", + "yyyy-MM-ddTHH:mm:ss", + "yyyyMMddHHmmss", + "yyyyMMddHHmm", + "yyyyMMddTHHmmssFFFFFFF", + "yyyy-MM-dd", + "yyyyMMdd", + "yy-MM-dd" + }; + + public static void RunQueries(this SQLiteDatabaseConnection connection, string[] queries) + { + ArgumentNullException.ThrowIfNull(queries); + + connection.RunInTransaction(conn => + { + conn.ExecuteAll(string.Join(';', queries)); + }); + } + + public static Guid ReadGuidFromBlob(this ResultSetValue result) + { + return new Guid(result.ToBlob()); + } + + public static string ToDateTimeParamValue(this DateTime dateValue) + { + var kind = DateTimeKind.Utc; + + return (dateValue.Kind == DateTimeKind.Unspecified) + ? DateTime.SpecifyKind(dateValue, kind).ToString( + GetDateTimeKindFormat(kind), + CultureInfo.InvariantCulture) + : dateValue.ToString( + GetDateTimeKindFormat(dateValue.Kind), + CultureInfo.InvariantCulture); + } + + private static string GetDateTimeKindFormat(DateTimeKind kind) + => (kind == DateTimeKind.Utc) ? DatetimeFormatUtc : DatetimeFormatLocal; + + public static DateTime ReadDateTime(this ResultSetValue result) + { + var dateText = result.ToString(); + + return DateTime.ParseExact( + dateText, + _datetimeFormats, + DateTimeFormatInfo.InvariantInfo, + DateTimeStyles.AdjustToUniversal); + } + + public static bool TryReadDateTime(this IReadOnlyList<ResultSetValue> reader, int index, out DateTime result) + { + var item = reader[index]; + if (item.IsDbNull()) + { + result = default; + return false; + } + + var dateText = item.ToString(); + + if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AdjustToUniversal, out var dateTimeResult)) + { + result = dateTimeResult; + return true; + } + + result = default; + return false; + } + + public static bool TryGetGuid(this IReadOnlyList<ResultSetValue> reader, int index, out Guid result) + { + var item = reader[index]; + if (item.IsDbNull()) + { + result = default; + return false; + } + + result = item.ReadGuidFromBlob(); + return true; + } + + public static bool IsDbNull(this ResultSetValue result) + { + return result.SQLiteType == SQLiteType.Null; + } + + public static string GetString(this IReadOnlyList<ResultSetValue> result, int index) + { + return result[index].ToString(); + } + + public static bool TryGetString(this IReadOnlyList<ResultSetValue> reader, int index, out string result) + { + result = null; + var item = reader[index]; + if (item.IsDbNull()) + { + return false; + } + + result = item.ToString(); + return true; + } + + public static bool GetBoolean(this IReadOnlyList<ResultSetValue> result, int index) + { + return result[index].ToBool(); + } + + public static bool TryGetBoolean(this IReadOnlyList<ResultSetValue> reader, int index, out bool result) + { + var item = reader[index]; + if (item.IsDbNull()) + { + result = default; + return false; + } + + result = item.ToBool(); + return true; + } + + public static bool TryGetInt32(this IReadOnlyList<ResultSetValue> reader, int index, out int result) + { + var item = reader[index]; + if (item.IsDbNull()) + { + result = default; + return false; + } + + result = item.ToInt(); + return true; + } + + public static long GetInt64(this IReadOnlyList<ResultSetValue> result, int index) + { + return result[index].ToInt64(); + } + + public static bool TryGetInt64(this IReadOnlyList<ResultSetValue> reader, int index, out long result) + { + var item = reader[index]; + if (item.IsDbNull()) + { + result = default; + return false; + } + + result = item.ToInt64(); + return true; + } + + public static bool TryGetSingle(this IReadOnlyList<ResultSetValue> reader, int index, out float result) + { + var item = reader[index]; + if (item.IsDbNull()) + { + result = default; + return false; + } + + result = item.ToFloat(); + return true; + } + + public static bool TryGetDouble(this IReadOnlyList<ResultSetValue> reader, int index, out double result) + { + var item = reader[index]; + if (item.IsDbNull()) + { + result = default; + return false; + } + + result = item.ToDouble(); + return true; + } + + public static Guid GetGuid(this IReadOnlyList<ResultSetValue> result, int index) + { + return result[index].ReadGuidFromBlob(); + } + + [Conditional("DEBUG")] + private static void CheckName(string name) + { + throw new ArgumentException("Invalid param name: " + name, nameof(name)); + } + + public static void TryBind(this IStatement statement, string name, double value) + { + if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) + { + bindParam.Bind(value); + } + else + { + CheckName(name); + } + } + + public static void TryBind(this IStatement statement, string name, string value) + { + if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) + { + if (value is null) + { + bindParam.BindNull(); + } + else + { + bindParam.Bind(value); + } + } + else + { + CheckName(name); + } + } + + public static void TryBind(this IStatement statement, string name, bool value) + { + if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) + { + bindParam.Bind(value); + } + else + { + CheckName(name); + } + } + + public static void TryBind(this IStatement statement, string name, float value) + { + if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) + { + bindParam.Bind(value); + } + else + { + CheckName(name); + } + } + + public static void TryBind(this IStatement statement, string name, int value) + { + if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) + { + bindParam.Bind(value); + } + else + { + CheckName(name); + } + } + + public static void TryBind(this IStatement statement, string name, Guid value) + { + if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) + { + Span<byte> byteValue = stackalloc byte[16]; + value.TryWriteBytes(byteValue); + bindParam.Bind(byteValue); + } + else + { + CheckName(name); + } + } + + public static void TryBind(this IStatement statement, string name, DateTime value) + { + if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) + { + bindParam.Bind(value.ToDateTimeParamValue()); + } + else + { + CheckName(name); + } + } + + public static void TryBind(this IStatement statement, string name, long value) + { + if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) + { + bindParam.Bind(value); + } + else + { + CheckName(name); + } + } + + public static void TryBind(this IStatement statement, string name, ReadOnlySpan<byte> value) + { + if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) + { + bindParam.Bind(value); + } + else + { + CheckName(name); + } + } + + public static void TryBindNull(this IStatement statement, string name) + { + if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) + { + bindParam.BindNull(); + } + else + { + CheckName(name); + } + } + + public static void TryBind(this IStatement statement, string name, DateTime? value) + { + if (value.HasValue) + { + TryBind(statement, name, value.Value); + } + else + { + TryBindNull(statement, name); + } + } + + public static void TryBind(this IStatement statement, string name, Guid? value) + { + if (value.HasValue) + { + TryBind(statement, name, value.Value); + } + else + { + TryBindNull(statement, name); + } + } + + public static void TryBind(this IStatement statement, string name, double? value) + { + if (value.HasValue) + { + TryBind(statement, name, value.Value); + } + else + { + TryBindNull(statement, name); + } + } + + public static void TryBind(this IStatement statement, string name, int? value) + { + if (value.HasValue) + { + TryBind(statement, name, value.Value); + } + else + { + TryBindNull(statement, name); + } + } + + public static void TryBind(this IStatement statement, string name, float? value) + { + if (value.HasValue) + { + TryBind(statement, name, value.Value); + } + else + { + TryBindNull(statement, name); + } + } + + public static void TryBind(this IStatement statement, string name, bool? value) + { + if (value.HasValue) + { + TryBind(statement, name, value.Value); + } + else + { + TryBindNull(statement, name); + } + } + + public static IEnumerable<IReadOnlyList<ResultSetValue>> ExecuteQuery(this IStatement statement) + { + while (statement.MoveNext()) + { + yield return statement.Current; + } + } + } +} diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 146de3ae13..a881e7cb4e 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -47,7 +47,7 @@ <PackageReference Include="Serilog.Sinks.Async" /> <PackageReference Include="Serilog.Sinks.Console" /> <PackageReference Include="Serilog.Sinks.File" /> - <PackageReference Include="Serilog.Sinks.Graylog" /> + <PackageReference Include="SQLitePCL.pretty.netstandard" /> <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" /> </ItemGroup> diff --git a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs index e8a0af9f88..5f44ba2caf 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using Emby.Server.Implementations.Data; using Jellyfin.Data.Entities; +using Jellyfin.Server.Extensions; using Jellyfin.Server.Implementations; using MediaBrowser.Controller; using Microsoft.EntityFrameworkCore; diff --git a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs index 09daae0ff9..a1b87fbe0a 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using Emby.Server.Implementations.Data; using Jellyfin.Data.Entities.Security; +using Jellyfin.Server.Extensions; using Jellyfin.Server.Implementations; using MediaBrowser.Controller; using MediaBrowser.Controller.Library; diff --git a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs index 9dee520a50..9f9960a642 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs @@ -3,6 +3,7 @@ using System.Globalization; using System.IO; using Emby.Server.Implementations.Data; +using Jellyfin.Server.Extensions; using MediaBrowser.Controller; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Globalization; @@ -91,7 +92,7 @@ namespace Jellyfin.Server.Migrations.Routines ratingValue = "NULL"; } - var statement = connection.PrepareStatement("UPDATE TypedBaseItems SET InheritedParentalRatingValue = @Value WHERE OfficialRating = @Rating;"); + using var statement = connection.PrepareStatement("UPDATE TypedBaseItems SET InheritedParentalRatingValue = @Value WHERE OfficialRating = @Rating;"); statement.TryBind("@Value", ratingValue); statement.TryBind("@Rating", ratingString); statement.ExecuteQuery(); diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs index 0186500a12..a1de104ade 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs @@ -4,6 +4,7 @@ using Emby.Server.Implementations.Data; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Extensions.Json; +using Jellyfin.Server.Extensions; using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations.Users; using MediaBrowser.Controller; From 493229cc15ea865d151b54d5d6d5c1eea831219a Mon Sep 17 00:00:00 2001 From: cvium <clausvium@gmail.com> Date: Mon, 21 Aug 2023 12:27:07 +0200 Subject: [PATCH 506/858] fix guid blobs --- .../Data/SqliteExtensions.cs | 20 ++++---- .../Data/SqliteItemRepository.cs | 51 +++++++------------ 2 files changed, 29 insertions(+), 42 deletions(-) diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs index 9e5cb17027..541153af2d 100644 --- a/Emby.Server.Implementations/Data/SqliteExtensions.cs +++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs @@ -264,17 +264,10 @@ namespace Emby.Server.Implementations.Data public static void TryBind(this SqliteCommand statement, string name, Guid value) { - if (statement.Parameters.Contains(name)) - { - statement.Parameters[name].Value = value; - } - else - { - statement.Parameters.Add(new SqliteParameter(name, SqliteType.Blob) { Value = value }); - } + statement.TryBind(name, value, true); } - public static void TryBind(this SqliteCommand statement, string name, object? value) + public static void TryBind(this SqliteCommand statement, string name, object? value, bool isBlob = false) { var preparedValue = value ?? DBNull.Value; if (statement.Parameters.Contains(name)) @@ -283,7 +276,14 @@ namespace Emby.Server.Implementations.Data } else { - statement.Parameters.AddWithValue(name, preparedValue); + if (isBlob) + { + statement.Parameters.Add(new SqliteParameter(name, SqliteType.Blob) { Value = value }); + } + else + { + statement.Parameters.AddWithValue(name, preparedValue); + } } } diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 4ceaafa128..f3ad1121a2 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -4891,11 +4891,6 @@ AND Type = @InternalPersonType)"); CheckDisposed(); - // TODO how to handle span? - Span<byte> itemIdBlob2 = stackalloc byte[16]; - itemId.TryWriteBytes(itemIdBlob2); - var itemIdBlob = Encoding.ASCII.GetBytes(itemId.ToString()); - // First delete // TODO deleteAncestorsStatement.Parameters.Clear(); deleteAncestorsStatement.TryBind("@ItemId", itemId); @@ -4921,14 +4916,13 @@ AND Type = @InternalPersonType)"); using (var statement = PrepareStatement(db, insertText.ToString())) { - statement.TryBind("@ItemId", itemIdBlob); + statement.TryBind("@ItemId", itemId); for (var i = 0; i < ancestorIds.Count; i++) { var index = i.ToString(CultureInfo.InvariantCulture); var ancestorId = ancestorIds[i]; - itemIdBlob = Encoding.ASCII.GetBytes(itemId.ToString()); statement.TryBind("@AncestorId" + index, ancestorId); statement.TryBind("@AncestorIdText" + index, ancestorId.ToString("N", CultureInfo.InvariantCulture)); @@ -5416,17 +5410,15 @@ AND Type = @InternalPersonType)"); CheckDisposed(); - var guidBlob = itemId.ToByteArray(); - // First delete using var command = db.PrepareStatement("delete from ItemValues where ItemId=@Id"); - command.TryBind("@Id", guidBlob); + command.TryBind("@Id", itemId); command.ExecuteNonQuery(); - InsertItemValues(guidBlob, values, db); + InsertItemValues(itemId, values, db); } - private void InsertItemValues(byte[] idBlob, List<(int MagicNumber, string Value)> values, SqliteConnection db) + private void InsertItemValues(Guid id, List<(int MagicNumber, string Value)> values, SqliteConnection db) { const int Limit = 100; var startIndex = 0; @@ -5450,7 +5442,7 @@ AND Type = @InternalPersonType)"); using (var statement = PrepareStatement(db, insertText.ToString())) { - statement.TryBind("@ItemId", idBlob); + statement.TryBind("@ItemId", id); for (var i = startIndex; i < endIndex; i++) { @@ -5496,20 +5488,18 @@ AND Type = @InternalPersonType)"); connection.RunInTransaction( db => { - var itemIdBlob = itemId.ToByteArray(); - // First delete chapters using var command = db.CreateCommand(); command.CommandText = "delete from People where ItemId=@ItemId"; - command.TryBind("@ItemId", itemIdBlob); + command.TryBind("@ItemId", itemId); command.ExecuteNonQuery(); - InsertPeople(itemIdBlob, people, db); + InsertPeople(itemId, people, db); }); } } - private void InsertPeople(byte[] idBlob, List<PersonInfo> people, SqliteConnection db) + private void InsertPeople(Guid id, List<PersonInfo> people, SqliteConnection db) { const int Limit = 100; var startIndex = 0; @@ -5533,7 +5523,7 @@ AND Type = @InternalPersonType)"); using (var statement = PrepareStatement(db, insertText.ToString())) { - statement.TryBind("@ItemId", idBlob); + statement.TryBind("@ItemId", id); for (var i = startIndex; i < endIndex; i++) { @@ -5651,19 +5641,17 @@ AND Type = @InternalPersonType)"); connection.RunInTransaction( db => { - var itemIdBlob = id.ToByteArray(); - // Delete existing mediastreams using var command = db.PrepareStatement("delete from mediastreams where ItemId=@ItemId"); - command.TryBind("@ItemId", itemIdBlob); + command.TryBind("@ItemId", id); command.ExecuteNonQuery(); - InsertMediaStreams(itemIdBlob, streams, db); + InsertMediaStreams(id, streams, db); }); } } - private void InsertMediaStreams(byte[] idBlob, IReadOnlyList<MediaStream> streams, SqliteConnection db) + private void InsertMediaStreams(Guid id, IReadOnlyList<MediaStream> streams, SqliteConnection db) { const int Limit = 10; var startIndex = 0; @@ -5695,7 +5683,7 @@ AND Type = @InternalPersonType)"); using (var statement = PrepareStatement(db, insertText.ToString())) { - statement.TryBind("@ItemId", idBlob); + statement.TryBind("@ItemId", id); for (var i = startIndex; i < endIndex; i++) { @@ -5731,6 +5719,7 @@ AND Type = @InternalPersonType)"); statement.TryBind("@PixelFormat" + index, stream.PixelFormat); statement.TryBind("@BitDepth" + index, stream.BitDepth); + statement.TryBind("@IsAnamorphic" + index, stream.IsAnamorphic); statement.TryBind("@IsExternal" + index, stream.IsExternal); statement.TryBind("@RefFrames" + index, stream.RefFrames); @@ -6000,7 +5989,7 @@ AND Type = @InternalPersonType)"); using (var connection = GetConnection(true)) using (var statement = PrepareStatement(connection, cmdText)) { - statement.TryBind("@ItemId", query.ItemId.ToByteArray()); + statement.TryBind("@ItemId", query.ItemId); if (query.Index.HasValue) { @@ -6036,19 +6025,17 @@ AND Type = @InternalPersonType)"); connection.RunInTransaction( db => { - var itemIdBlob = id.ToByteArray(); - using var command = db.PrepareStatement("delete from mediaattachments where ItemId=@ItemId"); - command.TryBind("@ItemId", itemIdBlob); + command.TryBind("@ItemId", id); command.ExecuteNonQuery(); - InsertMediaAttachments(itemIdBlob, attachments, db, cancellationToken); + InsertMediaAttachments(id, attachments, db, cancellationToken); }); } } private void InsertMediaAttachments( - byte[] idBlob, + Guid id, IReadOnlyList<MediaAttachment> attachments, SqliteConnection db, CancellationToken cancellationToken) @@ -6084,7 +6071,7 @@ AND Type = @InternalPersonType)"); using (var statement = PrepareStatement(db, insertText.ToString())) { - statement.TryBind("@ItemId", idBlob); + statement.TryBind("@ItemId", id); for (var i = startIndex; i < endIndex; i++) { From f2d78425638c96485d1effd41bdc7e271ad8029f Mon Sep 17 00:00:00 2001 From: cvium <clausvium@gmail.com> Date: Mon, 21 Aug 2023 12:29:20 +0200 Subject: [PATCH 507/858] add missing using --- Emby.Server.Implementations/Data/SqliteItemRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index f3ad1121a2..0434ed89dc 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -2066,7 +2066,7 @@ namespace Emby.Server.Implementations.Data db => { // First delete chapters - var command = db.PrepareStatement($"delete from {ChaptersTableName} where ItemId=@ItemId"); + using var command = db.PrepareStatement($"delete from {ChaptersTableName} where ItemId=@ItemId"); command.TryBind("@ItemId", id); command.ExecuteNonQuery(); From 0867812c1fabfce52abb1f8fdb17edad822a61af Mon Sep 17 00:00:00 2001 From: cvium <clausvium@gmail.com> Date: Mon, 21 Aug 2023 12:46:30 +0200 Subject: [PATCH 508/858] more using --- Emby.Server.Implementations/Data/SqliteExtensions.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs index 541153af2d..027771a5a8 100644 --- a/Emby.Server.Implementations/Data/SqliteExtensions.cs +++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs @@ -68,7 +68,7 @@ namespace Emby.Server.Implementations.Data sqliteConnection.Open(); } - var command = sqliteConnection.CreateCommand(); + using var command = sqliteConnection.CreateCommand(); command.CommandText = commandText; using (var reader = command.ExecuteReader()) { @@ -82,7 +82,7 @@ namespace Emby.Server.Implementations.Data public static void Execute(this SqliteConnection sqliteConnection, string commandText) { sqliteConnection.EnsureOpen(); - var command = sqliteConnection.CreateCommand(); + using var command = sqliteConnection.CreateCommand(); command.CommandText = commandText; command.ExecuteNonQuery(); } @@ -109,7 +109,7 @@ namespace Emby.Server.Implementations.Data { sqliteConnection.EnsureOpen(); - var command = sqliteConnection.CreateCommand(); + using var command = sqliteConnection.CreateCommand(); command.CommandText = commandText; command.ExecuteNonQuery(); } @@ -333,7 +333,7 @@ namespace Emby.Server.Implementations.Data public static void MoveNext(this SqliteCommand sqliteCommand) { sqliteCommand.Prepare(); - var result = sqliteCommand.ExecuteNonQuery(); + sqliteCommand.ExecuteNonQuery(); } public static byte[] GetBlob(this SqliteDataReader reader, int index) From 061d79c113404359068e94256104f955720bd1eb Mon Sep 17 00:00:00 2001 From: cvium <clausvium@gmail.com> Date: Mon, 21 Aug 2023 14:12:49 +0200 Subject: [PATCH 509/858] remove runintransaction --- .../Data/BaseSqliteRepository.cs | 22 +- .../Data/SqliteExtensions.cs | 56 +- .../Data/SqliteItemRepository.cs | 642 ++++++++---------- .../Data/SqliteUserDataRepository.cs | 90 ++- 4 files changed, 356 insertions(+), 454 deletions(-) diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs index 2ce87f5b46..6ee2d800cd 100644 --- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs +++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs @@ -146,22 +146,16 @@ namespace Emby.Server.Implementations.Data protected bool TableExists(SqliteConnection connection, string name) { - return connection.RunInTransaction( - db => + using var statement = PrepareStatement(connection, "select DISTINCT tbl_name from sqlite_master"); + foreach (var row in statement.ExecuteQuery()) + { + if (string.Equals(name, row.GetString(0), StringComparison.OrdinalIgnoreCase)) { - using (var statement = PrepareStatement(db, "select DISTINCT tbl_name from sqlite_master")) - { - foreach (var row in statement.ExecuteQuery()) - { - if (string.Equals(name, row.GetString(0), StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - } + return true; + } + } - return false; - }); + return false; } protected List<string> GetColumnNames(SqliteConnection connection, string table) diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs index 027771a5a8..40cecbb6bc 100644 --- a/Emby.Server.Implementations/Data/SqliteExtensions.cs +++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs @@ -87,24 +87,6 @@ namespace Emby.Server.Implementations.Data command.ExecuteNonQuery(); } - public static void RunInTransaction(this SqliteConnection sqliteConnection, Action<SqliteConnection> action) - { - sqliteConnection.EnsureOpen(); - - using var transaction = sqliteConnection.BeginTransaction(); - action(sqliteConnection); - transaction.Commit(); - } - - public static bool RunInTransaction(this SqliteConnection sqliteConnection, Func<SqliteConnection, bool> action) - { - sqliteConnection.EnsureOpen(); - using var transaction = sqliteConnection.BeginTransaction(); - var result = action(sqliteConnection); - transaction.Commit(); - return result; - } - public static void ExecuteAll(this SqliteConnection sqliteConnection, string commandText) { sqliteConnection.EnsureOpen(); @@ -117,11 +99,9 @@ namespace Emby.Server.Implementations.Data public static void RunQueries(this SqliteConnection connection, string[] queries) { ArgumentNullException.ThrowIfNull(queries); - - connection.RunInTransaction(conn => - { - conn.ExecuteAll(string.Join(';', queries)); - }); + using var transaction = connection.BeginTransaction(); + connection.ExecuteAll(string.Join(';', queries)); + transaction.Commit(); } public static string ToDateTimeParamValue(this DateTime dateValue) @@ -140,17 +120,6 @@ namespace Emby.Server.Implementations.Data private static string GetDateTimeKindFormat(DateTimeKind kind) => (kind == DateTimeKind.Utc) ? DatetimeFormatUtc : DatetimeFormatLocal; - public static DateTime ReadDateTime(this SqliteDataReader result) - { - var dateText = result.ToString(); - - return DateTime.ParseExact( - dateText!, - _datetimeFormats, - DateTimeFormatInfo.InvariantInfo, - DateTimeStyles.AdjustToUniversal); - } - public static bool TryReadDateTime(this SqliteDataReader reader, int index, out DateTime result) { if (reader.IsDBNull(index)) @@ -256,12 +225,6 @@ namespace Emby.Server.Implementations.Data return true; } - [Conditional("DEBUG")] - private static void CheckName(string name) - { - throw new ArgumentException("Invalid param name: " + name, nameof(name)); - } - public static void TryBind(this SqliteCommand statement, string name, Guid value) { statement.TryBind(name, value, true); @@ -328,18 +291,5 @@ namespace Emby.Server.Implementations.Data command.CommandText = sql; return command; } - - // Hacky - public static void MoveNext(this SqliteCommand sqliteCommand) - { - sqliteCommand.Prepare(); - sqliteCommand.ExecuteNonQuery(); - } - - public static byte[] GetBlob(this SqliteDataReader reader, int index) - { - // Have to reset to casting as there isn't a publicly available GetBlob method - return (byte[])reader.GetValue(index); - } } } diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 0434ed89dc..7e1c3bb4ca 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -440,122 +440,123 @@ namespace Emby.Server.Implementations.Data { connection.RunQueries(queries); - connection.RunInTransaction( - db => - { - var existingColumnNames = GetColumnNames(db, "AncestorIds"); - AddColumn(db, "AncestorIds", "AncestorIdText", "Text", existingColumnNames); + using (var transaction = connection.BeginTransaction()) + { + var existingColumnNames = GetColumnNames(connection, "AncestorIds"); + AddColumn(connection, "AncestorIds", "AncestorIdText", "Text", existingColumnNames); - existingColumnNames = GetColumnNames(db, "TypedBaseItems"); + existingColumnNames = GetColumnNames(connection, "TypedBaseItems"); - AddColumn(db, "TypedBaseItems", "Path", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "StartDate", "DATETIME", existingColumnNames); - AddColumn(db, "TypedBaseItems", "EndDate", "DATETIME", existingColumnNames); - AddColumn(db, "TypedBaseItems", "ChannelId", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "IsMovie", "BIT", existingColumnNames); - AddColumn(db, "TypedBaseItems", "CommunityRating", "Float", existingColumnNames); - AddColumn(db, "TypedBaseItems", "CustomRating", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "IndexNumber", "INT", existingColumnNames); - AddColumn(db, "TypedBaseItems", "IsLocked", "BIT", existingColumnNames); - AddColumn(db, "TypedBaseItems", "Name", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "OfficialRating", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "MediaType", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "Overview", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "ParentIndexNumber", "INT", existingColumnNames); - AddColumn(db, "TypedBaseItems", "PremiereDate", "DATETIME", existingColumnNames); - AddColumn(db, "TypedBaseItems", "ProductionYear", "INT", existingColumnNames); - AddColumn(db, "TypedBaseItems", "ParentId", "GUID", existingColumnNames); - AddColumn(db, "TypedBaseItems", "Genres", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "SortName", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "ForcedSortName", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "RunTimeTicks", "BIGINT", existingColumnNames); - AddColumn(db, "TypedBaseItems", "DateCreated", "DATETIME", existingColumnNames); - AddColumn(db, "TypedBaseItems", "DateModified", "DATETIME", existingColumnNames); - AddColumn(db, "TypedBaseItems", "IsSeries", "BIT", existingColumnNames); - AddColumn(db, "TypedBaseItems", "EpisodeTitle", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "IsRepeat", "BIT", existingColumnNames); - AddColumn(db, "TypedBaseItems", "PreferredMetadataLanguage", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "PreferredMetadataCountryCode", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "DateLastRefreshed", "DATETIME", existingColumnNames); - AddColumn(db, "TypedBaseItems", "DateLastSaved", "DATETIME", existingColumnNames); - AddColumn(db, "TypedBaseItems", "IsInMixedFolder", "BIT", existingColumnNames); - AddColumn(db, "TypedBaseItems", "LockedFields", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "Studios", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "Audio", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "ExternalServiceId", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "Tags", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "IsFolder", "BIT", existingColumnNames); - AddColumn(db, "TypedBaseItems", "InheritedParentalRatingValue", "INT", existingColumnNames); - AddColumn(db, "TypedBaseItems", "UnratedType", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "TopParentId", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "TrailerTypes", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "CriticRating", "Float", existingColumnNames); - AddColumn(db, "TypedBaseItems", "CleanName", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "PresentationUniqueKey", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "OriginalTitle", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "PrimaryVersionId", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "DateLastMediaAdded", "DATETIME", existingColumnNames); - AddColumn(db, "TypedBaseItems", "Album", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "LUFS", "Float", existingColumnNames); - AddColumn(db, "TypedBaseItems", "IsVirtualItem", "BIT", existingColumnNames); - AddColumn(db, "TypedBaseItems", "SeriesName", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "UserDataKey", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "SeasonName", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "SeasonId", "GUID", existingColumnNames); - AddColumn(db, "TypedBaseItems", "SeriesId", "GUID", existingColumnNames); - AddColumn(db, "TypedBaseItems", "ExternalSeriesId", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "Tagline", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "ProviderIds", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "Images", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "ProductionLocations", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "ExtraIds", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "TotalBitrate", "INT", existingColumnNames); - AddColumn(db, "TypedBaseItems", "ExtraType", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "Artists", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "AlbumArtists", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "ExternalId", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "SeriesPresentationUniqueKey", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "ShowId", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "OwnerId", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "Width", "INT", existingColumnNames); - AddColumn(db, "TypedBaseItems", "Height", "INT", existingColumnNames); - AddColumn(db, "TypedBaseItems", "Size", "BIGINT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Path", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "StartDate", "DATETIME", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "EndDate", "DATETIME", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ChannelId", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "IsMovie", "BIT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "CommunityRating", "Float", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "CustomRating", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "IndexNumber", "INT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "IsLocked", "BIT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Name", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "OfficialRating", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "MediaType", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Overview", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ParentIndexNumber", "INT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "PremiereDate", "DATETIME", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ProductionYear", "INT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ParentId", "GUID", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Genres", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "SortName", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ForcedSortName", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "RunTimeTicks", "BIGINT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "DateCreated", "DATETIME", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "DateModified", "DATETIME", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "IsSeries", "BIT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "EpisodeTitle", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "IsRepeat", "BIT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "PreferredMetadataLanguage", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "PreferredMetadataCountryCode", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "DateLastRefreshed", "DATETIME", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "DateLastSaved", "DATETIME", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "IsInMixedFolder", "BIT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "LockedFields", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Studios", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Audio", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ExternalServiceId", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Tags", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "IsFolder", "BIT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "InheritedParentalRatingValue", "INT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "UnratedType", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "TopParentId", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "TrailerTypes", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "CriticRating", "Float", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "CleanName", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "PresentationUniqueKey", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "OriginalTitle", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "PrimaryVersionId", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "DateLastMediaAdded", "DATETIME", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Album", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "LUFS", "Float", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "IsVirtualItem", "BIT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "SeriesName", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "UserDataKey", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "SeasonName", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "SeasonId", "GUID", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "SeriesId", "GUID", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ExternalSeriesId", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Tagline", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ProviderIds", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Images", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ProductionLocations", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ExtraIds", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "TotalBitrate", "INT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ExtraType", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Artists", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "AlbumArtists", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ExternalId", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "SeriesPresentationUniqueKey", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ShowId", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "OwnerId", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Width", "INT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Height", "INT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Size", "BIGINT", existingColumnNames); - existingColumnNames = GetColumnNames(db, "ItemValues"); - AddColumn(db, "ItemValues", "CleanValue", "Text", existingColumnNames); + existingColumnNames = GetColumnNames(connection, "ItemValues"); + AddColumn(connection, "ItemValues", "CleanValue", "Text", existingColumnNames); - existingColumnNames = GetColumnNames(db, ChaptersTableName); - AddColumn(db, ChaptersTableName, "ImageDateModified", "DATETIME", existingColumnNames); + existingColumnNames = GetColumnNames(connection, ChaptersTableName); + AddColumn(connection, ChaptersTableName, "ImageDateModified", "DATETIME", existingColumnNames); - existingColumnNames = GetColumnNames(db, "MediaStreams"); - AddColumn(db, "MediaStreams", "IsAvc", "BIT", existingColumnNames); - AddColumn(db, "MediaStreams", "TimeBase", "TEXT", existingColumnNames); - AddColumn(db, "MediaStreams", "CodecTimeBase", "TEXT", existingColumnNames); - AddColumn(db, "MediaStreams", "Title", "TEXT", existingColumnNames); - AddColumn(db, "MediaStreams", "NalLengthSize", "TEXT", existingColumnNames); - AddColumn(db, "MediaStreams", "Comment", "TEXT", existingColumnNames); - AddColumn(db, "MediaStreams", "CodecTag", "TEXT", existingColumnNames); - AddColumn(db, "MediaStreams", "PixelFormat", "TEXT", existingColumnNames); - AddColumn(db, "MediaStreams", "BitDepth", "INT", existingColumnNames); - AddColumn(db, "MediaStreams", "RefFrames", "INT", existingColumnNames); - AddColumn(db, "MediaStreams", "KeyFrames", "TEXT", existingColumnNames); - AddColumn(db, "MediaStreams", "IsAnamorphic", "BIT", existingColumnNames); + existingColumnNames = GetColumnNames(connection, "MediaStreams"); + AddColumn(connection, "MediaStreams", "IsAvc", "BIT", existingColumnNames); + AddColumn(connection, "MediaStreams", "TimeBase", "TEXT", existingColumnNames); + AddColumn(connection, "MediaStreams", "CodecTimeBase", "TEXT", existingColumnNames); + AddColumn(connection, "MediaStreams", "Title", "TEXT", existingColumnNames); + AddColumn(connection, "MediaStreams", "NalLengthSize", "TEXT", existingColumnNames); + AddColumn(connection, "MediaStreams", "Comment", "TEXT", existingColumnNames); + AddColumn(connection, "MediaStreams", "CodecTag", "TEXT", existingColumnNames); + AddColumn(connection, "MediaStreams", "PixelFormat", "TEXT", existingColumnNames); + AddColumn(connection, "MediaStreams", "BitDepth", "INT", existingColumnNames); + AddColumn(connection, "MediaStreams", "RefFrames", "INT", existingColumnNames); + AddColumn(connection, "MediaStreams", "KeyFrames", "TEXT", existingColumnNames); + AddColumn(connection, "MediaStreams", "IsAnamorphic", "BIT", existingColumnNames); - AddColumn(db, "MediaStreams", "ColorPrimaries", "TEXT", existingColumnNames); - AddColumn(db, "MediaStreams", "ColorSpace", "TEXT", existingColumnNames); - AddColumn(db, "MediaStreams", "ColorTransfer", "TEXT", existingColumnNames); + AddColumn(connection, "MediaStreams", "ColorPrimaries", "TEXT", existingColumnNames); + AddColumn(connection, "MediaStreams", "ColorSpace", "TEXT", existingColumnNames); + AddColumn(connection, "MediaStreams", "ColorTransfer", "TEXT", existingColumnNames); - AddColumn(db, "MediaStreams", "DvVersionMajor", "INT", existingColumnNames); - AddColumn(db, "MediaStreams", "DvVersionMinor", "INT", existingColumnNames); - AddColumn(db, "MediaStreams", "DvProfile", "INT", existingColumnNames); - AddColumn(db, "MediaStreams", "DvLevel", "INT", existingColumnNames); - AddColumn(db, "MediaStreams", "RpuPresentFlag", "INT", existingColumnNames); - AddColumn(db, "MediaStreams", "ElPresentFlag", "INT", existingColumnNames); - AddColumn(db, "MediaStreams", "BlPresentFlag", "INT", existingColumnNames); - AddColumn(db, "MediaStreams", "DvBlSignalCompatibilityId", "INT", existingColumnNames); + AddColumn(connection, "MediaStreams", "DvVersionMajor", "INT", existingColumnNames); + AddColumn(connection, "MediaStreams", "DvVersionMinor", "INT", existingColumnNames); + AddColumn(connection, "MediaStreams", "DvProfile", "INT", existingColumnNames); + AddColumn(connection, "MediaStreams", "DvLevel", "INT", existingColumnNames); + AddColumn(connection, "MediaStreams", "RpuPresentFlag", "INT", existingColumnNames); + AddColumn(connection, "MediaStreams", "ElPresentFlag", "INT", existingColumnNames); + AddColumn(connection, "MediaStreams", "BlPresentFlag", "INT", existingColumnNames); + AddColumn(connection, "MediaStreams", "DvBlSignalCompatibilityId", "INT", existingColumnNames); - AddColumn(db, "MediaStreams", "IsHearingImpaired", "BIT", existingColumnNames); - }); + AddColumn(connection, "MediaStreams", "IsHearingImpaired", "BIT", existingColumnNames); + + transaction.Commit(); + } connection.RunQueries(postQueries); } @@ -567,20 +568,14 @@ namespace Emby.Server.Implementations.Data CheckDisposed(); - using (var connection = GetConnection()) - { - connection.RunInTransaction( - db => - { - using (var saveImagesStatement = PrepareStatement(db, "Update TypedBaseItems set Images=@Images where guid=@Id")) - { - saveImagesStatement.TryBind("@Id", item.Id); - saveImagesStatement.TryBind("@Images", SerializeImages(item.ImageInfos)); + using var connection = GetConnection(); + using var transaction = connection.BeginTransaction(); + using var saveImagesStatement = PrepareStatement(connection, "Update TypedBaseItems set Images=@Images where guid=@Id"); + saveImagesStatement.TryBind("@Id", item.Id); + saveImagesStatement.TryBind("@Images", SerializeImages(item.ImageInfos)); - saveImagesStatement.MoveNext(); - } - }); - } + saveImagesStatement.ExecuteNonQuery(); + transaction.Commit(); } /// <summary> @@ -616,14 +611,10 @@ namespace Emby.Server.Implementations.Data tuples[i] = (item, ancestorIds, topParent, userdataKey, inheritedTags); } - using (var connection = GetConnection()) - { - connection.RunInTransaction( - db => - { - SaveItemsInTransaction(db, tuples); - }); - } + using var connection = GetConnection(); + using var transaction = connection.BeginTransaction(); + SaveItemsInTransaction(connection, tuples); + transaction.Commit(); } private void SaveItemsInTransaction(SqliteConnection db, IEnumerable<(BaseItem Item, List<Guid> AncestorIds, BaseItem TopParent, string UserDataKey, List<string> InheritedTags)> tuples) @@ -1030,7 +1021,7 @@ namespace Emby.Server.Implementations.Data saveItemStatement.TryBind("@OwnerId", ownerId); } - saveItemStatement.MoveNext(); + saveItemStatement.ExecuteNonQuery(); } internal static string SerializeProviderIds(Dictionary<string, string> providerIds) @@ -2060,19 +2051,15 @@ namespace Emby.Server.Implementations.Data ArgumentNullException.ThrowIfNull(chapters); - using (var connection = GetConnection()) - { - connection.RunInTransaction( - db => - { - // First delete chapters - using var command = db.PrepareStatement($"delete from {ChaptersTableName} where ItemId=@ItemId"); - command.TryBind("@ItemId", id); - command.ExecuteNonQuery(); + using var connection = GetConnection(); + using var transaction = connection.BeginTransaction(); + // First delete chapters + using var command = connection.PrepareStatement($"delete from {ChaptersTableName} where ItemId=@ItemId"); + command.TryBind("@ItemId", id); + command.ExecuteNonQuery(); - InsertChapters(id, chapters, db); - }); - } + InsertChapters(id, chapters, connection); + transaction.Commit(); } private void InsertChapters(Guid idBlob, IReadOnlyList<ChapterInfo> chapters, SqliteConnection db) @@ -2115,7 +2102,7 @@ namespace Emby.Server.Implementations.Data } // TODO statement.Parameters.Clear(); - statement.MoveNext(); + statement.ExecuteNonQuery(); } startIndex += limit; @@ -2848,68 +2835,65 @@ namespace Emby.Server.Implementations.Data var list = new List<BaseItem>(); var result = new QueryResult<BaseItem>(); - using (var connection = GetConnection(true)) + using var connection = GetConnection(true); + using var transaction = connection.BeginTransaction(); + if (!isReturningZeroItems) { - connection.RunInTransaction( - db => + using (new QueryTimeLogger(Logger, itemQuery, "GetItems.ItemQuery")) + using (var statement = PrepareStatement(connection, itemQuery)) + { + if (EnableJoinUserData(query)) { - if (!isReturningZeroItems) + statement.TryBind("@UserId", query.User.InternalId); + } + + BindSimilarParams(query, statement); + BindSearchParams(query, statement); + + // Running this again will bind the params + GetWhereClauses(query, statement); + + var hasEpisodeAttributes = HasEpisodeAttributes(query); + var hasServiceName = HasServiceName(query); + var hasProgramAttributes = HasProgramAttributes(query); + var hasStartDate = HasStartDate(query); + var hasTrailerTypes = HasTrailerTypes(query); + var hasArtistFields = HasArtistFields(query); + var hasSeriesFields = HasSeriesFields(query); + + foreach (var row in statement.ExecuteQuery()) + { + var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields); + if (item is not null) { - using (new QueryTimeLogger(Logger, itemQuery, "GetItems.ItemQuery")) - using (var statement = PrepareStatement(db, itemQuery)) - { - if (EnableJoinUserData(query)) - { - statement.TryBind("@UserId", query.User.InternalId); - } - - BindSimilarParams(query, statement); - BindSearchParams(query, statement); - - // Running this again will bind the params - GetWhereClauses(query, statement); - - var hasEpisodeAttributes = HasEpisodeAttributes(query); - var hasServiceName = HasServiceName(query); - var hasProgramAttributes = HasProgramAttributes(query); - var hasStartDate = HasStartDate(query); - var hasTrailerTypes = HasTrailerTypes(query); - var hasArtistFields = HasArtistFields(query); - var hasSeriesFields = HasSeriesFields(query); - - foreach (var row in statement.ExecuteQuery()) - { - var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields); - if (item is not null) - { - list.Add(item); - } - } - } + list.Add(item); } - - if (query.EnableTotalRecordCount) - { - using (new QueryTimeLogger(Logger, totalRecordCountQuery, "GetItems.TotalRecordCount")) - using (var statement = PrepareStatement(db, totalRecordCountQuery)) - { - if (EnableJoinUserData(query)) - { - statement.TryBind("@UserId", query.User.InternalId); - } - - BindSimilarParams(query, statement); - BindSearchParams(query, statement); - - // Running this again will bind the params - GetWhereClauses(query, statement); - - result.TotalRecordCount = statement.SelectScalarInt(); - } - } - }); + } + } } + if (query.EnableTotalRecordCount) + { + using (new QueryTimeLogger(Logger, totalRecordCountQuery, "GetItems.TotalRecordCount")) + using (var statement = PrepareStatement(connection, totalRecordCountQuery)) + { + if (EnableJoinUserData(query)) + { + statement.TryBind("@UserId", query.User.InternalId); + } + + BindSimilarParams(query, statement); + BindSearchParams(query, statement); + + // Running this again will bind the params + GetWhereClauses(query, statement); + + result.TotalRecordCount = statement.SelectScalarInt(); + } + } + + transaction.Commit(); + result.StartIndex = query.StartIndex ?? 0; result.Items = list; return result; @@ -4662,28 +4646,18 @@ namespace Emby.Server.Implementations.Data public void UpdateInheritedValues() { - string sql = string.Join( - ';', - new string[] - { - "delete from ItemValues where type = 6", - - "insert into ItemValues (ItemId, Type, Value, CleanValue) select ItemId, 6, Value, CleanValue from ItemValues where Type=4", - - @"insert into ItemValues (ItemId, Type, Value, CleanValue) select AncestorIds.itemid, 6, ItemValues.Value, ItemValues.CleanValue + const string Statements = """ +delete from ItemValues where type = 6; +insert into ItemValues (ItemId, Type, Value, CleanValue) select ItemId, 6, Value, CleanValue from ItemValues where Type=4; +insert into ItemValues (ItemId, Type, Value, CleanValue) select AncestorIds.itemid, 6, ItemValues.Value, ItemValues.CleanValue FROM AncestorIds LEFT JOIN ItemValues ON (AncestorIds.AncestorId = ItemValues.ItemId) -where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type = 4 " - }); - - using (var connection = GetConnection()) - { - connection.RunInTransaction( - db => - { - connection.ExecuteAll(sql); - }); - } +where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type = 4; +"""; + using var connection = GetConnection(); + using var transaction = connection.BeginTransaction(); + connection.ExecuteAll(Statements); + transaction.Commit(); } public void DeleteItem(Guid id) @@ -4695,42 +4669,36 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type CheckDisposed(); - using (var connection = GetConnection()) - { - connection.RunInTransaction( - db => - { - Span<byte> idBlob = stackalloc byte[16]; - id.TryWriteBytes(idBlob); + using var connection = GetConnection(); + using var transaction = connection.BeginTransaction(); + // Delete people + ExecuteWithSingleParam(connection, "delete from People where ItemId=@Id", id); - // Delete people - ExecuteWithSingleParam(db, "delete from People where ItemId=@Id", idBlob); + // Delete chapters + ExecuteWithSingleParam(connection, "delete from " + ChaptersTableName + " where ItemId=@Id", id); - // Delete chapters - ExecuteWithSingleParam(db, "delete from " + ChaptersTableName + " where ItemId=@Id", idBlob); + // Delete media streams + ExecuteWithSingleParam(connection, "delete from mediastreams where ItemId=@Id", id); - // Delete media streams - ExecuteWithSingleParam(db, "delete from mediastreams where ItemId=@Id", idBlob); + // Delete ancestors + ExecuteWithSingleParam(connection, "delete from AncestorIds where ItemId=@Id", id); - // Delete ancestors - ExecuteWithSingleParam(db, "delete from AncestorIds where ItemId=@Id", idBlob); + // Delete item values + ExecuteWithSingleParam(connection, "delete from ItemValues where ItemId=@Id", id); - // Delete item values - ExecuteWithSingleParam(db, "delete from ItemValues where ItemId=@Id", idBlob); + // Delete the item + ExecuteWithSingleParam(connection, "delete from TypedBaseItems where guid=@Id", id); - // Delete the item - ExecuteWithSingleParam(db, "delete from TypedBaseItems where guid=@Id", idBlob); - }); - } + transaction.Commit(); } - private void ExecuteWithSingleParam(SqliteConnection db, string query, ReadOnlySpan<byte> value) + private void ExecuteWithSingleParam(SqliteConnection db, string query, Guid value) { using (var statement = PrepareStatement(db, query)) { - statement.TryBind("@Id", value.ToArray()); + statement.TryBind("@Id", value); - statement.MoveNext(); + statement.ExecuteNonQuery(); } } @@ -4894,7 +4862,7 @@ AND Type = @InternalPersonType)"); // First delete // TODO deleteAncestorsStatement.Parameters.Clear(); deleteAncestorsStatement.TryBind("@ItemId", itemId); - deleteAncestorsStatement.MoveNext(); + deleteAncestorsStatement.ExecuteNonQuery(); if (ancestorIds.Count == 0) { @@ -4929,7 +4897,7 @@ AND Type = @InternalPersonType)"); } // TODO statement.Parameters.Clear(); - statement.MoveNext(); + statement.ExecuteNonQuery(); } } @@ -5238,75 +5206,74 @@ AND Type = @InternalPersonType)"); var result = new QueryResult<(BaseItem, ItemCounts)>(); using (new QueryTimeLogger(Logger, commandText)) using (var connection = GetConnection(true)) + using (var transaction = connection.BeginTransaction()) { - connection.RunInTransaction( - db => + if (!isReturningZeroItems) + { + using (var statement = PrepareStatement(connection, commandText)) { - if (!isReturningZeroItems) + statement.TryBind("@SelectType", returnType); + if (EnableJoinUserData(query)) { - using (var statement = PrepareStatement(db, commandText)) - { - statement.TryBind("@SelectType", returnType); - if (EnableJoinUserData(query)) - { - statement.TryBind("@UserId", query.User.InternalId); - } - - if (typeSubQuery is not null) - { - GetWhereClauses(typeSubQuery, null); - } - - BindSimilarParams(query, statement); - BindSearchParams(query, statement); - GetWhereClauses(innerQuery, statement); - GetWhereClauses(outerQuery, statement); - - var hasEpisodeAttributes = HasEpisodeAttributes(query); - var hasProgramAttributes = HasProgramAttributes(query); - var hasServiceName = HasServiceName(query); - var hasStartDate = HasStartDate(query); - var hasTrailerTypes = HasTrailerTypes(query); - var hasArtistFields = HasArtistFields(query); - var hasSeriesFields = HasSeriesFields(query); - - foreach (var row in statement.ExecuteQuery()) - { - var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields); - if (item is not null) - { - var countStartColumn = columns.Count - 1; - - list.Add((item, GetItemCounts(row, countStartColumn, typesToCount))); - } - } - } + statement.TryBind("@UserId", query.User.InternalId); } - if (query.EnableTotalRecordCount) + if (typeSubQuery is not null) { - using (var statement = PrepareStatement(db, countText)) + GetWhereClauses(typeSubQuery, null); + } + + BindSimilarParams(query, statement); + BindSearchParams(query, statement); + GetWhereClauses(innerQuery, statement); + GetWhereClauses(outerQuery, statement); + + var hasEpisodeAttributes = HasEpisodeAttributes(query); + var hasProgramAttributes = HasProgramAttributes(query); + var hasServiceName = HasServiceName(query); + var hasStartDate = HasStartDate(query); + var hasTrailerTypes = HasTrailerTypes(query); + var hasArtistFields = HasArtistFields(query); + var hasSeriesFields = HasSeriesFields(query); + + foreach (var row in statement.ExecuteQuery()) + { + var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields); + if (item is not null) { - statement.TryBind("@SelectType", returnType); - if (EnableJoinUserData(query)) - { - statement.TryBind("@UserId", query.User.InternalId); - } + var countStartColumn = columns.Count - 1; - if (typeSubQuery is not null) - { - GetWhereClauses(typeSubQuery, null); - } - - BindSimilarParams(query, statement); - BindSearchParams(query, statement); - GetWhereClauses(innerQuery, statement); - GetWhereClauses(outerQuery, statement); - - result.TotalRecordCount = statement.SelectScalarInt(); + list.Add((item, GetItemCounts(row, countStartColumn, typesToCount))); } } - }); + } + } + + if (query.EnableTotalRecordCount) + { + using (var statement = PrepareStatement(connection, countText)) + { + statement.TryBind("@SelectType", returnType); + if (EnableJoinUserData(query)) + { + statement.TryBind("@UserId", query.User.InternalId); + } + + if (typeSubQuery is not null) + { + GetWhereClauses(typeSubQuery, null); + } + + BindSimilarParams(query, statement); + BindSearchParams(query, statement); + GetWhereClauses(innerQuery, statement); + GetWhereClauses(outerQuery, statement); + + result.TotalRecordCount = statement.SelectScalarInt(); + } + } + + transaction.Commit(); } if (result.TotalRecordCount == 0) @@ -5464,7 +5431,7 @@ AND Type = @InternalPersonType)"); } // TODO statement.Parameters.Clear(); - statement.MoveNext(); + statement.ExecuteNonQuery(); } startIndex += Limit; @@ -5483,20 +5450,17 @@ AND Type = @InternalPersonType)"); CheckDisposed(); - using (var connection = GetConnection()) - { - connection.RunInTransaction( - db => - { - // First delete chapters - using var command = db.CreateCommand(); - command.CommandText = "delete from People where ItemId=@ItemId"; - command.TryBind("@ItemId", itemId); - command.ExecuteNonQuery(); + using var connection = GetConnection(); + using var transaction = connection.BeginTransaction(); + // First delete chapters + using var command = connection.CreateCommand(); + command.CommandText = "delete from People where ItemId=@ItemId"; + command.TryBind("@ItemId", itemId); + command.ExecuteNonQuery(); - InsertPeople(itemId, people, db); - }); - } + InsertPeople(itemId, people, connection); + + transaction.Commit(); } private void InsertPeople(Guid id, List<PersonInfo> people, SqliteConnection db) @@ -5540,7 +5504,7 @@ AND Type = @InternalPersonType)"); listIndex++; } - statement.MoveNext(); + statement.ExecuteNonQuery(); } startIndex += Limit; @@ -5636,19 +5600,16 @@ AND Type = @InternalPersonType)"); cancellationToken.ThrowIfCancellationRequested(); - using (var connection = GetConnection()) - { - connection.RunInTransaction( - db => - { - // Delete existing mediastreams - using var command = db.PrepareStatement("delete from mediastreams where ItemId=@ItemId"); - command.TryBind("@ItemId", id); - command.ExecuteNonQuery(); + using var connection = GetConnection(); + using var transaction = connection.BeginTransaction(); + // Delete existing mediastreams + using var command = connection.PrepareStatement("delete from mediastreams where ItemId=@ItemId"); + command.TryBind("@ItemId", id); + command.ExecuteNonQuery(); - InsertMediaStreams(id, streams, db); - }); - } + InsertMediaStreams(id, streams, connection); + + transaction.Commit(); } private void InsertMediaStreams(Guid id, IReadOnlyList<MediaStream> streams, SqliteConnection db) @@ -5749,7 +5710,7 @@ AND Type = @InternalPersonType)"); } // TODO statement.Parameters.Clear(); - statement.MoveNext(); + statement.ExecuteNonQuery(); } startIndex += Limit; @@ -6021,16 +5982,15 @@ AND Type = @InternalPersonType)"); cancellationToken.ThrowIfCancellationRequested(); using (var connection = GetConnection()) + using (var transaction = connection.BeginTransaction()) + using (var command = connection.PrepareStatement("delete from mediaattachments where ItemId=@ItemId")) { - connection.RunInTransaction( - db => - { - using var command = db.PrepareStatement("delete from mediaattachments where ItemId=@ItemId"); - command.TryBind("@ItemId", id); - command.ExecuteNonQuery(); + command.TryBind("@ItemId", id); + command.ExecuteNonQuery(); - InsertMediaAttachments(id, attachments, db, cancellationToken); - }); + InsertMediaAttachments(id, attachments, connection, cancellationToken); + + transaction.Commit(); } } @@ -6088,7 +6048,7 @@ AND Type = @InternalPersonType)"); } // TODO statement.Parameters.Clear(); - statement.MoveNext(); + statement.ExecuteNonQuery(); } insertText.Length = _mediaAttachmentInsertPrefix.Length; diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs index bc3863a65f..619a644874 100644 --- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs @@ -44,43 +44,45 @@ namespace Emby.Server.Implementations.Data var userDataTableExists = TableExists(connection, "userdata"); var users = userDatasTableExists ? null : _userManager.Users; + using var transaction = connection.BeginTransaction(); + connection.ExecuteAll(string.Join(';', new[] + { + "create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)", - connection.RunInTransaction( - db => - { - db.ExecuteAll(string.Join(';', new[] - { - "create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)", + "drop index if exists idx_userdata", + "drop index if exists idx_userdata1", + "drop index if exists idx_userdata2", + "drop index if exists userdataindex1", + "drop index if exists userdataindex", + "drop index if exists userdataindex3", + "drop index if exists userdataindex4", + "create unique index if not exists UserDatasIndex1 on UserDatas (key, userId)", + "create index if not exists UserDatasIndex2 on UserDatas (key, userId, played)", + "create index if not exists UserDatasIndex3 on UserDatas (key, userId, playbackPositionTicks)", + "create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)" + })); - "drop index if exists idx_userdata", - "drop index if exists idx_userdata1", - "drop index if exists idx_userdata2", - "drop index if exists userdataindex1", - "drop index if exists userdataindex", - "drop index if exists userdataindex3", - "drop index if exists userdataindex4", - "create unique index if not exists UserDatasIndex1 on UserDatas (key, userId)", - "create index if not exists UserDatasIndex2 on UserDatas (key, userId, played)", - "create index if not exists UserDatasIndex3 on UserDatas (key, userId, playbackPositionTicks)", - "create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)" - })); + if (!userDataTableExists) + { + return; + } - if (userDataTableExists) - { - var existingColumnNames = GetColumnNames(db, "userdata"); + var existingColumnNames = GetColumnNames(connection, "userdata"); - AddColumn(db, "userdata", "InternalUserId", "int", existingColumnNames); - AddColumn(db, "userdata", "AudioStreamIndex", "int", existingColumnNames); - AddColumn(db, "userdata", "SubtitleStreamIndex", "int", existingColumnNames); + AddColumn(connection, "userdata", "InternalUserId", "int", existingColumnNames); + AddColumn(connection, "userdata", "AudioStreamIndex", "int", existingColumnNames); + AddColumn(connection, "userdata", "SubtitleStreamIndex", "int", existingColumnNames); - if (!userDatasTableExists) - { - ImportUserIds(db, users); + if (userDatasTableExists) + { + return; + } - db.ExecuteAll("INSERT INTO UserDatas (key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex) SELECT key, InternalUserId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex from userdata where InternalUserId not null"); - } - } - }); + ImportUserIds(connection, users); + + connection.ExecuteAll("INSERT INTO UserDatas (key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex) SELECT key, InternalUserId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex from userdata where InternalUserId not null"); + + transaction.Commit(); } } @@ -99,7 +101,6 @@ namespace Emby.Server.Implementations.Data statement.TryBind("@UserId", user.Id); statement.TryBind("@InternalUserId", user.InternalId); - statement.Prepare(); statement.ExecuteNonQuery(); } @@ -168,12 +169,10 @@ namespace Emby.Server.Implementations.Data cancellationToken.ThrowIfCancellationRequested(); using (var connection = GetConnection()) + using (var transaction = connection.BeginTransaction()) { - connection.RunInTransaction( - db => - { - SaveUserData(db, internalUserId, key, userData); - }); + SaveUserData(connection, internalUserId, key, userData); + transaction.Commit(); } } @@ -225,7 +224,7 @@ namespace Emby.Server.Implementations.Data statement.TryBindNull("@SubtitleStreamIndex"); } - statement.MoveNext(); + statement.ExecuteNonQuery(); } } @@ -237,15 +236,14 @@ namespace Emby.Server.Implementations.Data cancellationToken.ThrowIfCancellationRequested(); using (var connection = GetConnection()) + using (var transaction = connection.BeginTransaction()) { - connection.RunInTransaction( - db => - { - foreach (var userItemData in userDataList) - { - SaveUserData(db, internalUserId, userItemData.Key, userItemData); - } - }); + foreach (var userItemData in userDataList) + { + SaveUserData(connection, internalUserId, userItemData.Key, userItemData); + } + + transaction.Commit(); } } From d223f5b5186f89ff9c6e931ae2341b44b190946d Mon Sep 17 00:00:00 2001 From: cvium <clausvium@gmail.com> Date: Mon, 21 Aug 2023 15:31:02 +0200 Subject: [PATCH 510/858] completely remove sqlitepcl --- Directory.Packages.props | 2 - .../Extensions/SqliteExtensions.cs | 449 ------------------ Jellyfin.Server/Jellyfin.Server.csproj | 2 - .../Routines/MigrateActivityLogDb.cs | 49 +- .../Routines/MigrateAuthenticationDb.cs | 40 +- .../Routines/MigrateDisplayPreferencesDb.cs | 13 +- .../Routines/MigrateRatingLevels.cs | 21 +- .../Migrations/Routines/MigrateUserDb.cs | 11 +- .../Routines/RemoveDuplicateExtras.cs | 13 +- 9 files changed, 66 insertions(+), 534 deletions(-) delete mode 100644 Jellyfin.Server/Extensions/SqliteExtensions.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index bd2a1d1811..06fb1ce14a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -73,8 +73,6 @@ <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.8.2.3" /> <PackageVersion Include="SkiaSharp" Version="2.88.3" /> <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> - <PackageVersion Include="SQLitePCL.pretty.netstandard" Version="3.1.0" /> - <PackageVersion Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.5" /> <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.507" /> <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.4.0" /> <PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" /> diff --git a/Jellyfin.Server/Extensions/SqliteExtensions.cs b/Jellyfin.Server/Extensions/SqliteExtensions.cs deleted file mode 100644 index 0e6a1bb13f..0000000000 --- a/Jellyfin.Server/Extensions/SqliteExtensions.cs +++ /dev/null @@ -1,449 +0,0 @@ -#nullable disable -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using SQLitePCL.pretty; - -namespace Jellyfin.Server.Extensions -{ - public static class SqliteExtensions - { - private const string DatetimeFormatUtc = "yyyy-MM-dd HH:mm:ss.FFFFFFFK"; - private const string DatetimeFormatLocal = "yyyy-MM-dd HH:mm:ss.FFFFFFF"; - - /// <summary> - /// An array of ISO-8601 DateTime formats that we support parsing. - /// </summary> - private static readonly string[] _datetimeFormats = new string[] - { - "THHmmssK", - "THHmmK", - "HH:mm:ss.FFFFFFFK", - "HH:mm:ssK", - "HH:mmK", - DatetimeFormatUtc, - "yyyy-MM-dd HH:mm:ssK", - "yyyy-MM-dd HH:mmK", - "yyyy-MM-ddTHH:mm:ss.FFFFFFFK", - "yyyy-MM-ddTHH:mmK", - "yyyy-MM-ddTHH:mm:ssK", - "yyyyMMddHHmmssK", - "yyyyMMddHHmmK", - "yyyyMMddTHHmmssFFFFFFFK", - "THHmmss", - "THHmm", - "HH:mm:ss.FFFFFFF", - "HH:mm:ss", - "HH:mm", - DatetimeFormatLocal, - "yyyy-MM-dd HH:mm:ss", - "yyyy-MM-dd HH:mm", - "yyyy-MM-ddTHH:mm:ss.FFFFFFF", - "yyyy-MM-ddTHH:mm", - "yyyy-MM-ddTHH:mm:ss", - "yyyyMMddHHmmss", - "yyyyMMddHHmm", - "yyyyMMddTHHmmssFFFFFFF", - "yyyy-MM-dd", - "yyyyMMdd", - "yy-MM-dd" - }; - - public static void RunQueries(this SQLiteDatabaseConnection connection, string[] queries) - { - ArgumentNullException.ThrowIfNull(queries); - - connection.RunInTransaction(conn => - { - conn.ExecuteAll(string.Join(';', queries)); - }); - } - - public static Guid ReadGuidFromBlob(this ResultSetValue result) - { - return new Guid(result.ToBlob()); - } - - public static string ToDateTimeParamValue(this DateTime dateValue) - { - var kind = DateTimeKind.Utc; - - return (dateValue.Kind == DateTimeKind.Unspecified) - ? DateTime.SpecifyKind(dateValue, kind).ToString( - GetDateTimeKindFormat(kind), - CultureInfo.InvariantCulture) - : dateValue.ToString( - GetDateTimeKindFormat(dateValue.Kind), - CultureInfo.InvariantCulture); - } - - private static string GetDateTimeKindFormat(DateTimeKind kind) - => (kind == DateTimeKind.Utc) ? DatetimeFormatUtc : DatetimeFormatLocal; - - public static DateTime ReadDateTime(this ResultSetValue result) - { - var dateText = result.ToString(); - - return DateTime.ParseExact( - dateText, - _datetimeFormats, - DateTimeFormatInfo.InvariantInfo, - DateTimeStyles.AdjustToUniversal); - } - - public static bool TryReadDateTime(this IReadOnlyList<ResultSetValue> reader, int index, out DateTime result) - { - var item = reader[index]; - if (item.IsDbNull()) - { - result = default; - return false; - } - - var dateText = item.ToString(); - - if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AdjustToUniversal, out var dateTimeResult)) - { - result = dateTimeResult; - return true; - } - - result = default; - return false; - } - - public static bool TryGetGuid(this IReadOnlyList<ResultSetValue> reader, int index, out Guid result) - { - var item = reader[index]; - if (item.IsDbNull()) - { - result = default; - return false; - } - - result = item.ReadGuidFromBlob(); - return true; - } - - public static bool IsDbNull(this ResultSetValue result) - { - return result.SQLiteType == SQLiteType.Null; - } - - public static string GetString(this IReadOnlyList<ResultSetValue> result, int index) - { - return result[index].ToString(); - } - - public static bool TryGetString(this IReadOnlyList<ResultSetValue> reader, int index, out string result) - { - result = null; - var item = reader[index]; - if (item.IsDbNull()) - { - return false; - } - - result = item.ToString(); - return true; - } - - public static bool GetBoolean(this IReadOnlyList<ResultSetValue> result, int index) - { - return result[index].ToBool(); - } - - public static bool TryGetBoolean(this IReadOnlyList<ResultSetValue> reader, int index, out bool result) - { - var item = reader[index]; - if (item.IsDbNull()) - { - result = default; - return false; - } - - result = item.ToBool(); - return true; - } - - public static bool TryGetInt32(this IReadOnlyList<ResultSetValue> reader, int index, out int result) - { - var item = reader[index]; - if (item.IsDbNull()) - { - result = default; - return false; - } - - result = item.ToInt(); - return true; - } - - public static long GetInt64(this IReadOnlyList<ResultSetValue> result, int index) - { - return result[index].ToInt64(); - } - - public static bool TryGetInt64(this IReadOnlyList<ResultSetValue> reader, int index, out long result) - { - var item = reader[index]; - if (item.IsDbNull()) - { - result = default; - return false; - } - - result = item.ToInt64(); - return true; - } - - public static bool TryGetSingle(this IReadOnlyList<ResultSetValue> reader, int index, out float result) - { - var item = reader[index]; - if (item.IsDbNull()) - { - result = default; - return false; - } - - result = item.ToFloat(); - return true; - } - - public static bool TryGetDouble(this IReadOnlyList<ResultSetValue> reader, int index, out double result) - { - var item = reader[index]; - if (item.IsDbNull()) - { - result = default; - return false; - } - - result = item.ToDouble(); - return true; - } - - public static Guid GetGuid(this IReadOnlyList<ResultSetValue> result, int index) - { - return result[index].ReadGuidFromBlob(); - } - - [Conditional("DEBUG")] - private static void CheckName(string name) - { - throw new ArgumentException("Invalid param name: " + name, nameof(name)); - } - - public static void TryBind(this IStatement statement, string name, double value) - { - if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) - { - bindParam.Bind(value); - } - else - { - CheckName(name); - } - } - - public static void TryBind(this IStatement statement, string name, string value) - { - if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) - { - if (value is null) - { - bindParam.BindNull(); - } - else - { - bindParam.Bind(value); - } - } - else - { - CheckName(name); - } - } - - public static void TryBind(this IStatement statement, string name, bool value) - { - if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) - { - bindParam.Bind(value); - } - else - { - CheckName(name); - } - } - - public static void TryBind(this IStatement statement, string name, float value) - { - if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) - { - bindParam.Bind(value); - } - else - { - CheckName(name); - } - } - - public static void TryBind(this IStatement statement, string name, int value) - { - if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) - { - bindParam.Bind(value); - } - else - { - CheckName(name); - } - } - - public static void TryBind(this IStatement statement, string name, Guid value) - { - if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) - { - Span<byte> byteValue = stackalloc byte[16]; - value.TryWriteBytes(byteValue); - bindParam.Bind(byteValue); - } - else - { - CheckName(name); - } - } - - public static void TryBind(this IStatement statement, string name, DateTime value) - { - if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) - { - bindParam.Bind(value.ToDateTimeParamValue()); - } - else - { - CheckName(name); - } - } - - public static void TryBind(this IStatement statement, string name, long value) - { - if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) - { - bindParam.Bind(value); - } - else - { - CheckName(name); - } - } - - public static void TryBind(this IStatement statement, string name, ReadOnlySpan<byte> value) - { - if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) - { - bindParam.Bind(value); - } - else - { - CheckName(name); - } - } - - public static void TryBindNull(this IStatement statement, string name) - { - if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) - { - bindParam.BindNull(); - } - else - { - CheckName(name); - } - } - - public static void TryBind(this IStatement statement, string name, DateTime? value) - { - if (value.HasValue) - { - TryBind(statement, name, value.Value); - } - else - { - TryBindNull(statement, name); - } - } - - public static void TryBind(this IStatement statement, string name, Guid? value) - { - if (value.HasValue) - { - TryBind(statement, name, value.Value); - } - else - { - TryBindNull(statement, name); - } - } - - public static void TryBind(this IStatement statement, string name, double? value) - { - if (value.HasValue) - { - TryBind(statement, name, value.Value); - } - else - { - TryBindNull(statement, name); - } - } - - public static void TryBind(this IStatement statement, string name, int? value) - { - if (value.HasValue) - { - TryBind(statement, name, value.Value); - } - else - { - TryBindNull(statement, name); - } - } - - public static void TryBind(this IStatement statement, string name, float? value) - { - if (value.HasValue) - { - TryBind(statement, name, value.Value); - } - else - { - TryBindNull(statement, name); - } - } - - public static void TryBind(this IStatement statement, string name, bool? value) - { - if (value.HasValue) - { - TryBind(statement, name, value.Value); - } - else - { - TryBindNull(statement, name); - } - } - - public static IEnumerable<IReadOnlyList<ResultSetValue>> ExecuteQuery(this IStatement statement) - { - while (statement.MoveNext()) - { - yield return statement.Current; - } - } - } -} diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index a881e7cb4e..60d9849a32 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -47,8 +47,6 @@ <PackageReference Include="Serilog.Sinks.Async" /> <PackageReference Include="Serilog.Sinks.Console" /> <PackageReference Include="Serilog.Sinks.File" /> - <PackageReference Include="SQLitePCL.pretty.netstandard" /> - <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" /> </ItemGroup> <ItemGroup> diff --git a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs index 5f44ba2caf..c7c9c12501 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs @@ -3,12 +3,11 @@ using System.Collections.Generic; using System.IO; using Emby.Server.Implementations.Data; using Jellyfin.Data.Entities; -using Jellyfin.Server.Extensions; using Jellyfin.Server.Implementations; using MediaBrowser.Controller; +using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using SQLitePCL.pretty; namespace Jellyfin.Server.Migrations.Routines { @@ -62,17 +61,12 @@ namespace Jellyfin.Server.Migrations.Routines }; var dataPath = _paths.DataPath; - using (var connection = SQLite3.Open( - Path.Combine(dataPath, DbFilename), - ConnectionFlags.ReadOnly, - null)) + using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}")) { - using var userDbConnection = SQLite3.Open(Path.Combine(dataPath, "users.db"), ConnectionFlags.ReadOnly, null); + using var userDbConnection = new SqliteConnection($"Filename={Path.Combine(dataPath, "users.db")}"); _logger.LogWarning("Migrating the activity database may take a while, do not stop Jellyfin."); using var dbContext = _provider.CreateDbContext(); - var queryResult = connection.Query("SELECT * FROM ActivityLog ORDER BY Id"); - // Make sure that the database is empty in case of failed migration due to power outages, etc. dbContext.ActivityLogs.RemoveRange(dbContext.ActivityLogs); dbContext.SaveChanges(); @@ -82,51 +76,52 @@ namespace Jellyfin.Server.Migrations.Routines var newEntries = new List<ActivityLog>(); + var queryResult = connection.Query("SELECT * FROM ActivityLog ORDER BY Id"); + foreach (var entry in queryResult) { - if (!logLevelDictionary.TryGetValue(entry[8].ToString(), out var severity)) + if (!logLevelDictionary.TryGetValue(entry.GetString(8), out var severity)) { severity = LogLevel.Trace; } var guid = Guid.Empty; - if (entry[6].SQLiteType != SQLiteType.Null && !Guid.TryParse(entry[6].ToString(), out guid)) + if (!entry.IsDBNull(6) && !entry.TryGetGuid(6, out guid)) { + var id = entry.GetString(6); // This is not a valid Guid, see if it is an internal ID from an old Emby schema - _logger.LogWarning("Invalid Guid in UserId column: {Guid}", entry[6].ToString()); + _logger.LogWarning("Invalid Guid in UserId column: {Guid}", id); using var statement = userDbConnection.PrepareStatement("SELECT guid FROM LocalUsersv2 WHERE Id=@Id"); - statement.TryBind("@Id", entry[6].ToString()); + statement.TryBind("@Id", id); - foreach (var row in statement.Query()) + using var reader = statement.ExecuteReader(); + if (reader.HasRows && reader.Read() && reader.TryGetGuid(0, out guid)) { - if (row.Count > 0 && Guid.TryParse(row[0].ToString(), out guid)) - { - // Successfully parsed a Guid from the user table. - break; - } + // Successfully parsed a Guid from the user table. + break; } } - var newEntry = new ActivityLog(entry[1].ToString(), entry[4].ToString(), guid) + var newEntry = new ActivityLog(entry.GetString(1), entry.GetString(4), guid) { - DateCreated = entry[7].ReadDateTime(), + DateCreated = entry.GetDateTime(7), LogSeverity = severity }; - if (entry[2].SQLiteType != SQLiteType.Null) + if (entry.TryGetString(2, out var result)) { - newEntry.Overview = entry[2].ToString(); + newEntry.Overview = result; } - if (entry[3].SQLiteType != SQLiteType.Null) + if (entry.TryGetString(3, out result)) { - newEntry.ShortOverview = entry[3].ToString(); + newEntry.ShortOverview = result; } - if (entry[5].SQLiteType != SQLiteType.Null) + if (entry.TryGetString(5, out result)) { - newEntry.ItemId = entry[5].ToString(); + newEntry.ItemId = result; } newEntries.Add(newEntry); diff --git a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs index a1b87fbe0a..63cbe49503 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs @@ -3,13 +3,12 @@ using System.Collections.Generic; using System.IO; using Emby.Server.Implementations.Data; using Jellyfin.Data.Entities.Security; -using Jellyfin.Server.Extensions; using Jellyfin.Server.Implementations; using MediaBrowser.Controller; using MediaBrowser.Controller.Library; +using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using SQLitePCL.pretty; namespace Jellyfin.Server.Migrations.Routines { @@ -57,10 +56,7 @@ namespace Jellyfin.Server.Migrations.Routines public void Perform() { var dataPath = _appPaths.DataPath; - using (var connection = SQLite3.Open( - Path.Combine(dataPath, DbFilename), - ConnectionFlags.ReadOnly, - null)) + using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}")) { using var dbContext = _dbProvider.CreateDbContext(); @@ -68,23 +64,23 @@ namespace Jellyfin.Server.Migrations.Routines foreach (var row in authenticatedDevices) { - var dateCreatedStr = row[9].ToString(); + var dateCreatedStr = row.GetString(9); _ = DateTime.TryParse(dateCreatedStr, out var dateCreated); - var dateLastActivityStr = row[10].ToString(); + var dateLastActivityStr = row.GetString(10); _ = DateTime.TryParse(dateLastActivityStr, out var dateLastActivity); - if (row[6].IsDbNull()) + if (row.IsDBNull(6)) { - dbContext.ApiKeys.Add(new ApiKey(row[3].ToString()) + dbContext.ApiKeys.Add(new ApiKey(row.GetString(3)) { - AccessToken = row[1].ToString(), + AccessToken = row.GetString(1), DateCreated = dateCreated, DateLastActivity = dateLastActivity }); } else { - var userId = new Guid(row[6].ToString()); + var userId = row.GetGuid(6); var user = _userManager.GetUserById(userId); if (user is null) { @@ -93,14 +89,14 @@ namespace Jellyfin.Server.Migrations.Routines } dbContext.Devices.Add(new Device( - new Guid(row[6].ToString()), - row[3].ToString(), - row[4].ToString(), - row[5].ToString(), - row[2].ToString()) + userId, + row.GetString(3), + row.GetString(4), + row.GetString(5), + row.GetString(2)) { - AccessToken = row[1].ToString(), - IsActive = row[8].ToBool(), + AccessToken = row.GetString(1), + IsActive = row.GetBoolean(8), DateCreated = dateCreated, DateLastActivity = dateLastActivity }); @@ -111,12 +107,12 @@ namespace Jellyfin.Server.Migrations.Routines var deviceIds = new HashSet<string>(); foreach (var row in deviceOptions) { - if (row[2].IsDbNull()) + if (row.IsDBNull(2)) { continue; } - var deviceId = row[2].ToString(); + var deviceId = row.GetString(2); if (deviceIds.Contains(deviceId)) { continue; @@ -126,7 +122,7 @@ namespace Jellyfin.Server.Migrations.Routines dbContext.DeviceOptions.Add(new DeviceOptions(deviceId) { - CustomName = row[1].IsDbNull() ? null : row[1].ToString() + CustomName = row.IsDBNull(1) ? null : row.GetString(1) }); } diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs index 8fe2b087d9..a036fe9bb5 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs @@ -4,15 +4,16 @@ using System.IO; using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; +using Emby.Server.Implementations.Data; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Server.Implementations; using MediaBrowser.Controller; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Dto; +using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using SQLitePCL.pretty; namespace Jellyfin.Server.Migrations.Routines { @@ -83,22 +84,22 @@ namespace Jellyfin.Server.Migrations.Routines var displayPrefs = new HashSet<string>(StringComparer.OrdinalIgnoreCase); var customDisplayPrefs = new HashSet<string>(StringComparer.OrdinalIgnoreCase); var dbFilePath = Path.Combine(_paths.DataPath, DbFilename); - using (var connection = SQLite3.Open(dbFilePath, ConnectionFlags.ReadOnly, null)) + using (var connection = new SqliteConnection($"Filename={dbFilePath}")) { using var dbContext = _provider.CreateDbContext(); var results = connection.Query("SELECT * FROM userdisplaypreferences"); foreach (var result in results) { - var dto = JsonSerializer.Deserialize<DisplayPreferencesDto>(result[3].ToBlob(), _jsonOptions); + var dto = JsonSerializer.Deserialize<DisplayPreferencesDto>(result.GetStream(3), _jsonOptions); if (dto is null) { continue; } - var itemId = new Guid(result[1].ToBlob()); - var dtoUserId = new Guid(result[1].ToBlob()); - var client = result[2].ToString(); + var itemId = result.GetGuid(1); + var dtoUserId = itemId; + var client = result.GetString(2); var displayPreferencesKey = $"{dtoUserId}|{itemId}|{client}"; if (displayPrefs.Contains(displayPreferencesKey)) { diff --git a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs index 9f9960a642..06eda329cb 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs @@ -1,14 +1,12 @@ using System; using System.Globalization; using System.IO; - using Emby.Server.Implementations.Data; -using Jellyfin.Server.Extensions; using MediaBrowser.Controller; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Globalization; +using Microsoft.Data.Sqlite; using Microsoft.Extensions.Logging; -using SQLitePCL.pretty; namespace Jellyfin.Server.Migrations.Routines { @@ -21,17 +19,14 @@ namespace Jellyfin.Server.Migrations.Routines private readonly ILogger<MigrateRatingLevels> _logger; private readonly IServerApplicationPaths _applicationPaths; private readonly ILocalizationManager _localizationManager; - private readonly IItemRepository _repository; public MigrateRatingLevels( IServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory, - ILocalizationManager localizationManager, - IItemRepository repository) + ILocalizationManager localizationManager) { _applicationPaths = applicationPaths; _localizationManager = localizationManager; - _repository = repository; _logger = loggerFactory.CreateLogger<MigrateRatingLevels>(); } @@ -71,15 +66,13 @@ namespace Jellyfin.Server.Migrations.Routines // Migrate parental rating strings to new levels _logger.LogInformation("Recalculating parental rating levels based on rating string."); - using (var connection = SQLite3.Open( - dbPath, - ConnectionFlags.ReadWrite, - null)) + using (var connection = new SqliteConnection($"Filename={dbPath}")) + using (var transaction = connection.BeginTransaction()) { var queryResult = connection.Query("SELECT DISTINCT OfficialRating FROM TypedBaseItems"); foreach (var entry in queryResult) { - var ratingString = entry[0].ToString(); + var ratingString = entry.GetString(0); if (string.IsNullOrEmpty(ratingString)) { connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = NULL WHERE OfficialRating IS NULL OR OfficialRating='';"); @@ -95,9 +88,11 @@ namespace Jellyfin.Server.Migrations.Routines using var statement = connection.PrepareStatement("UPDATE TypedBaseItems SET InheritedParentalRatingValue = @Value WHERE OfficialRating = @Rating;"); statement.TryBind("@Value", ratingValue); statement.TryBind("@Rating", ratingString); - statement.ExecuteQuery(); + statement.ExecuteNonQuery(); } } + + transaction.Commit(); } } } diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs index a1de104ade..9cf888d62b 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs @@ -4,7 +4,6 @@ using Emby.Server.Implementations.Data; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Extensions.Json; -using Jellyfin.Server.Extensions; using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations.Users; using MediaBrowser.Controller; @@ -12,9 +11,9 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Users; +using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using SQLitePCL.pretty; using JsonSerializer = System.Text.Json.JsonSerializer; namespace Jellyfin.Server.Migrations.Routines @@ -65,7 +64,7 @@ namespace Jellyfin.Server.Migrations.Routines var dataPath = _paths.DataPath; _logger.LogInformation("Migrating the user database may take a while, do not stop Jellyfin."); - using (var connection = SQLite3.Open(Path.Combine(dataPath, DbFilename), ConnectionFlags.ReadOnly, null)) + using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}")) { var dbContext = _provider.CreateDbContext(); @@ -76,7 +75,7 @@ namespace Jellyfin.Server.Migrations.Routines foreach (var entry in queryResult) { - UserMockup? mockup = JsonSerializer.Deserialize<UserMockup>(entry[2].ToBlob(), JsonDefaults.Options); + UserMockup? mockup = JsonSerializer.Deserialize<UserMockup>(entry.GetStream(2), JsonDefaults.Options); if (mockup is null) { continue; @@ -109,8 +108,8 @@ namespace Jellyfin.Server.Migrations.Routines var user = new User(mockup.Name, policy.AuthenticationProviderId!, policy.PasswordResetProviderId!) { - Id = entry[1].ReadGuidFromBlob(), - InternalId = entry[0].ToInt64(), + Id = entry.GetGuid(1), + InternalId = entry.GetInt64(0), MaxParentalAgeRating = policy.MaxParentalRating, EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess, RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit, diff --git a/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs b/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs index 6c26e47e15..98017e3ef4 100644 --- a/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs +++ b/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs @@ -1,10 +1,11 @@ using System; using System.Globalization; using System.IO; - +using System.Linq; +using Emby.Server.Implementations.Data; using MediaBrowser.Controller; +using Microsoft.Data.Sqlite; using Microsoft.Extensions.Logging; -using SQLitePCL.pretty; namespace Jellyfin.Server.Migrations.Routines { @@ -37,14 +38,12 @@ namespace Jellyfin.Server.Migrations.Routines { var dataPath = _paths.DataPath; var dbPath = Path.Combine(dataPath, DbFilename); - using (var connection = SQLite3.Open( - dbPath, - ConnectionFlags.ReadWrite, - null)) + using (var connection = new SqliteConnection($"Filename={dbPath}")) { // Query the database for the ids of duplicate extras var queryResult = connection.Query("SELECT t1.Path FROM TypedBaseItems AS t1, TypedBaseItems AS t2 WHERE t1.Path=t2.Path AND t1.Type!=t2.Type AND t1.Type='MediaBrowser.Controller.Entities.Video'"); - var bads = string.Join(", ", queryResult.SelectScalarString()); + // TODO does this LINQ execute before the reader is disposed? + var bads = string.Join(", ", queryResult.Select(x => x.GetString(0)).ToList()); // Do nothing if no duplicate extras were detected if (bads.Length == 0) From 84643e328df6b194eb4de893b8b5e232af5e2a0c Mon Sep 17 00:00:00 2001 From: Bond-009 <bond.009@outlook.com> Date: Mon, 21 Aug 2023 18:38:32 +0200 Subject: [PATCH 511/858] Reduce the amount of allocations in GetWhereClauses (#10114) --- .../Data/SqliteItemRepository.cs | 396 +++++++----------- jellyfin.ruleset | 2 + 2 files changed, 163 insertions(+), 235 deletions(-) diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 73ec856fc8..94b4f48455 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -26,7 +26,6 @@ using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Extensions; -using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Playlists; @@ -1305,88 +1304,27 @@ namespace Emby.Server.Implementations.Data { if (_config.Configuration.SkipDeserializationForBasicTypes) { - if (type == typeof(Channel)) - { - return false; - } - - if (type == typeof(UserRootFolder)) + if (type == typeof(Channel) + || type == typeof(UserRootFolder)) { return false; } } - if (type == typeof(Season)) - { - return false; - } - - if (type == typeof(MusicArtist)) - { - return false; - } - - if (type == typeof(Person)) - { - return false; - } - - if (type == typeof(MusicGenre)) - { - return false; - } - - if (type == typeof(Genre)) - { - return false; - } - - if (type == typeof(Studio)) - { - return false; - } - - if (type == typeof(PlaylistsFolder)) - { - return false; - } - - if (type == typeof(PhotoAlbum)) - { - return false; - } - - if (type == typeof(Year)) - { - return false; - } - - if (type == typeof(Book)) - { - return false; - } - - if (type == typeof(LiveTvProgram)) - { - return false; - } - - if (type == typeof(AudioBook)) - { - return false; - } - - if (type == typeof(Audio)) - { - return false; - } - - if (type == typeof(MusicAlbum)) - { - return false; - } - - return true; + return type != typeof(Season) + && type != typeof(MusicArtist) + && type != typeof(Person) + && type != typeof(MusicGenre) + && type != typeof(Genre) + && type != typeof(Studio) + && type != typeof(PlaylistsFolder) + && type != typeof(PhotoAlbum) + && type != typeof(Year) + && type != typeof(Book) + && type != typeof(LiveTvProgram) + && type != typeof(AudioBook) + && type != typeof(Audio) + && type != typeof(MusicAlbum); } private BaseItem GetItem(IReadOnlyList<ResultSetValue> reader, InternalItemsQuery query) @@ -2105,7 +2043,7 @@ namespace Emby.Server.Implementations.Data insertText.AppendFormat(CultureInfo.InvariantCulture, "(@ItemId, @ChapterIndex{0}, @StartPositionTicks{0}, @Name{0}, @ImagePath{0}, @ImageDateModified{0}),", i.ToString(CultureInfo.InvariantCulture)); } - insertText.Length -= 1; // Remove last , + insertText.Length -= 1; // Remove trailing comma using (var statement = PrepareStatement(db, insertText.ToString())) { @@ -3604,7 +3542,6 @@ namespace Emby.Server.Implementations.Data statement?.TryBind(paramName, "%" + trailerTypes[i] + "%"); } - // Remove last " OR " clauseBuilder.Length -= Or.Length; clauseBuilder.Append(')'); @@ -3652,7 +3589,6 @@ namespace Emby.Server.Implementations.Data } } - // Remove last " OR " clauseBuilder.Length -= Or.Length; clauseBuilder.Append(')'); @@ -3819,215 +3755,219 @@ namespace Emby.Server.Implementations.Data if (query.ArtistIds.Length > 0) { - var clauses = new List<string>(); - var index = 0; - foreach (var artistId in query.ArtistIds) + clauseBuilder.Append('('); + for (var i = 0; i < query.ArtistIds.Length; i++) { - var paramName = "@ArtistIds" + index; - clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))"); - statement?.TryBind(paramName, artistId); - index++; + clauseBuilder.Append("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@ArtistIds") + .Append(i) + .Append(") and Type<=1)) OR "); + statement?.TryBind("@ArtistIds" + i, query.ArtistIds[i]); } - var clause = "(" + string.Join(" OR ", clauses) + ")"; - whereClauses.Add(clause); + clauseBuilder.Length -= Or.Length; + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; } if (query.AlbumArtistIds.Length > 0) { - var clauses = new List<string>(); - var index = 0; - foreach (var artistId in query.AlbumArtistIds) + clauseBuilder.Append('('); + for (var i = 0; i < query.AlbumArtistIds.Length; i++) { - var paramName = "@ArtistIds" + index; - clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=1))"); - statement?.TryBind(paramName, artistId); - index++; + clauseBuilder.Append("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@ArtistIds") + .Append(i) + .Append(") and Type=1)) OR "); + statement?.TryBind("@ArtistIds" + i, query.AlbumArtistIds[i]); } - var clause = "(" + string.Join(" OR ", clauses) + ")"; - whereClauses.Add(clause); + clauseBuilder.Length -= Or.Length; + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; } if (query.ContributingArtistIds.Length > 0) { - var clauses = new List<string>(); - var index = 0; - foreach (var artistId in query.ContributingArtistIds) + clauseBuilder.Append('('); + for (var i = 0; i < query.ContributingArtistIds.Length; i++) { - var paramName = "@ArtistIds" + index; - clauses.Add("((select CleanName from TypedBaseItems where guid=" + paramName + ") in (select CleanValue from ItemValues where ItemId=Guid and Type=0) AND (select CleanName from TypedBaseItems where guid=" + paramName + ") not in (select CleanValue from ItemValues where ItemId=Guid and Type=1))"); - statement?.TryBind(paramName, artistId); - index++; + clauseBuilder.Append("((select CleanName from TypedBaseItems where guid=@ArtistIds") + .Append(i) + .Append(") in (select CleanValue from ItemValues where ItemId=Guid and Type=0) AND (select CleanName from TypedBaseItems where guid=@ArtistIds") + .Append(i) + .Append(") not in (select CleanValue from ItemValues where ItemId=Guid and Type=1)) OR "); + statement?.TryBind("@ArtistIds" + i, query.ContributingArtistIds[i]); } - var clause = "(" + string.Join(" OR ", clauses) + ")"; - whereClauses.Add(clause); + clauseBuilder.Length -= Or.Length; + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; } if (query.AlbumIds.Length > 0) { - var clauses = new List<string>(); - var index = 0; - foreach (var albumId in query.AlbumIds) + clauseBuilder.Append('('); + for (var i = 0; i < query.AlbumIds.Length; i++) { - var paramName = "@AlbumIds" + index; - clauses.Add("Album in (select Name from typedbaseitems where guid=" + paramName + ")"); - statement?.TryBind(paramName, albumId); - index++; + clauseBuilder.Append("Album in (select Name from typedbaseitems where guid=@AlbumIds") + .Append(i) + .Append(") OR "); + statement?.TryBind("@AlbumIds" + i, query.AlbumIds[i]); } - var clause = "(" + string.Join(" OR ", clauses) + ")"; - whereClauses.Add(clause); + clauseBuilder.Length -= Or.Length; + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; } if (query.ExcludeArtistIds.Length > 0) { - var clauses = new List<string>(); - var index = 0; - foreach (var artistId in query.ExcludeArtistIds) + clauseBuilder.Append('('); + for (var i = 0; i < query.ExcludeArtistIds.Length; i++) { - var paramName = "@ExcludeArtistId" + index; - clauses.Add("(guid not in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))"); - statement?.TryBind(paramName, artistId); - index++; + clauseBuilder.Append("(guid not in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@ExcludeArtistId") + .Append(i) + .Append(") and Type<=1)) OR "); + statement?.TryBind("@ExcludeArtistId" + i, query.ExcludeArtistIds[i]); } - var clause = "(" + string.Join(" OR ", clauses) + ")"; - whereClauses.Add(clause); + clauseBuilder.Length -= Or.Length; + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; } if (query.GenreIds.Count > 0) { - var clauses = new List<string>(); - var index = 0; - foreach (var genreId in query.GenreIds) + clauseBuilder.Append('('); + for (var i = 0; i < query.GenreIds.Count; i++) { - var paramName = "@GenreId" + index; - clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=2))"); - statement?.TryBind(paramName, genreId); - index++; + clauseBuilder.Append("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@GenreId") + .Append(i) + .Append(") and Type=2)) OR "); + statement?.TryBind("@GenreId" + i, query.GenreIds[i]); } - var clause = "(" + string.Join(" OR ", clauses) + ")"; - whereClauses.Add(clause); + clauseBuilder.Length -= Or.Length; + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; } if (query.Genres.Count > 0) { - var clauses = new List<string>(); - var index = 0; - foreach (var item in query.Genres) + clauseBuilder.Append('('); + for (var i = 0; i < query.Genres.Count; i++) { - clauses.Add("@Genre" + index + " in (select CleanValue from ItemValues where ItemId=Guid and Type=2)"); - statement?.TryBind("@Genre" + index, GetCleanValue(item)); - index++; + clauseBuilder.Append("@Genre") + .Append(i) + .Append(" in (select CleanValue from ItemValues where ItemId=Guid and Type=2) OR "); + statement?.TryBind("@Genre" + i, GetCleanValue(query.Genres[i])); } - var clause = "(" + string.Join(" OR ", clauses) + ")"; - whereClauses.Add(clause); + clauseBuilder.Length -= Or.Length; + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; } if (tags.Count > 0) { - var clauses = new List<string>(); - var index = 0; - foreach (var item in tags) + clauseBuilder.Append('('); + for (var i = 0; i < tags.Count; i++) { - clauses.Add("@Tag" + index + " in (select CleanValue from ItemValues where ItemId=Guid and Type=4)"); - statement?.TryBind("@Tag" + index, GetCleanValue(item)); - index++; + clauseBuilder.Append("@Tag") + .Append(i) + .Append(" in (select CleanValue from ItemValues where ItemId=Guid and Type=4) OR "); + statement?.TryBind("@Tag" + i, GetCleanValue(tags[i])); } - var clause = "(" + string.Join(" OR ", clauses) + ")"; - whereClauses.Add(clause); + clauseBuilder.Length -= Or.Length; + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; } if (excludeTags.Count > 0) { - var clauses = new List<string>(); - var index = 0; - foreach (var item in excludeTags) + clauseBuilder.Append('('); + for (var i = 0; i < excludeTags.Count; i++) { - clauses.Add("@ExcludeTag" + index + " not in (select CleanValue from ItemValues where ItemId=Guid and Type=4)"); - statement?.TryBind("@ExcludeTag" + index, GetCleanValue(item)); - index++; + clauseBuilder.Append("@ExcludeTag") + .Append(i) + .Append(" not in (select CleanValue from ItemValues where ItemId=Guid and Type=4) OR "); + statement?.TryBind("@ExcludeTag" + i, GetCleanValue(excludeTags[i])); } - var clause = "(" + string.Join(" OR ", clauses) + ")"; - whereClauses.Add(clause); + clauseBuilder.Length -= Or.Length; + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; } if (query.StudioIds.Length > 0) { - var clauses = new List<string>(); - var index = 0; - foreach (var studioId in query.StudioIds) + clauseBuilder.Append('('); + for (var i = 0; i < query.StudioIds.Length; i++) { - var paramName = "@StudioId" + index; - clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=3))"); - statement?.TryBind(paramName, studioId); - index++; + clauseBuilder.Append("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@StudioId") + .Append(i) + .Append(") and Type=3)) OR "); + statement?.TryBind("@StudioId" + i, query.StudioIds[i]); } - var clause = "(" + string.Join(" OR ", clauses) + ")"; - whereClauses.Add(clause); + clauseBuilder.Length -= Or.Length; + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; } if (query.OfficialRatings.Length > 0) { - var clauses = new List<string>(); - var index = 0; - foreach (var item in query.OfficialRatings) + clauseBuilder.Append('('); + for (var i = 0; i < query.OfficialRatings.Length; i++) { - clauses.Add("OfficialRating=@OfficialRating" + index); - statement?.TryBind("@OfficialRating" + index, item); - index++; + clauseBuilder.Append("OfficialRating=@OfficialRating").Append(i).Append(Or); + statement?.TryBind("@OfficialRating" + i, query.OfficialRatings[i]); } - var clause = "(" + string.Join(" OR ", clauses) + ")"; - whereClauses.Add(clause); + clauseBuilder.Length -= Or.Length; + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; } - var ratingClauseBuilder = new StringBuilder("("); + clauseBuilder.Append('('); if (query.HasParentalRating ?? false) { - ratingClauseBuilder.Append("InheritedParentalRatingValue not null"); + clauseBuilder.Append("InheritedParentalRatingValue not null"); if (query.MinParentalRating.HasValue) { - ratingClauseBuilder.Append(" AND InheritedParentalRatingValue >= @MinParentalRating"); + clauseBuilder.Append(" AND InheritedParentalRatingValue >= @MinParentalRating"); statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value); } if (query.MaxParentalRating.HasValue) { - ratingClauseBuilder.Append(" AND InheritedParentalRatingValue <= @MaxParentalRating"); + clauseBuilder.Append(" AND InheritedParentalRatingValue <= @MaxParentalRating"); statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); } } else if (query.BlockUnratedItems.Length > 0) { - var paramName = "@UnratedType"; - var index = 0; - string blockedUnratedItems = string.Join(',', query.BlockUnratedItems.Select(_ => paramName + index++)); - ratingClauseBuilder.Append("(InheritedParentalRatingValue is null AND UnratedType not in (" + blockedUnratedItems + "))"); + const string ParamName = "@UnratedType"; + clauseBuilder.Append("(InheritedParentalRatingValue is null AND UnratedType not in ("); - if (statement is not null) + for (int i = 0; i < query.BlockUnratedItems.Length; i++) { - for (var ind = 0; ind < query.BlockUnratedItems.Length; ind++) - { - statement.TryBind(paramName + ind, query.BlockUnratedItems[ind].ToString()); - } + clauseBuilder.Append(ParamName).Append(i).Append(','); + statement?.TryBind(ParamName + i, query.BlockUnratedItems[i].ToString()); } + // Remove trailing comma + clauseBuilder.Length--; + clauseBuilder.Append("))"); + if (query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue) { - ratingClauseBuilder.Append(" OR ("); + clauseBuilder.Append(" OR ("); } if (query.MinParentalRating.HasValue) { - ratingClauseBuilder.Append("InheritedParentalRatingValue >= @MinParentalRating"); + clauseBuilder.Append("InheritedParentalRatingValue >= @MinParentalRating"); statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value); } @@ -4035,50 +3975,50 @@ namespace Emby.Server.Implementations.Data { if (query.MinParentalRating.HasValue) { - ratingClauseBuilder.Append(" AND "); + clauseBuilder.Append(" AND "); } - ratingClauseBuilder.Append("InheritedParentalRatingValue <= @MaxParentalRating"); + clauseBuilder.Append("InheritedParentalRatingValue <= @MaxParentalRating"); statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); } if (query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue) { - ratingClauseBuilder.Append(")"); + clauseBuilder.Append(')'); } if (!(query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue)) { - ratingClauseBuilder.Append(" OR InheritedParentalRatingValue not null"); + clauseBuilder.Append(" OR InheritedParentalRatingValue not null"); } } else if (query.MinParentalRating.HasValue) { - ratingClauseBuilder.Append("InheritedParentalRatingValue is null OR (InheritedParentalRatingValue >= @MinParentalRating"); + clauseBuilder.Append("InheritedParentalRatingValue is null OR (InheritedParentalRatingValue >= @MinParentalRating"); statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value); if (query.MaxParentalRating.HasValue) { - ratingClauseBuilder.Append(" AND InheritedParentalRatingValue <= @MaxParentalRating"); + clauseBuilder.Append(" AND InheritedParentalRatingValue <= @MaxParentalRating"); statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); } - ratingClauseBuilder.Append(")"); + clauseBuilder.Append(')'); } else if (query.MaxParentalRating.HasValue) { - ratingClauseBuilder.Append("InheritedParentalRatingValue is null OR InheritedParentalRatingValue <= @MaxParentalRating"); + clauseBuilder.Append("InheritedParentalRatingValue is null OR InheritedParentalRatingValue <= @MaxParentalRating"); statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); } else if (!query.HasParentalRating ?? false) { - ratingClauseBuilder.Append("InheritedParentalRatingValue is null"); + clauseBuilder.Append("InheritedParentalRatingValue is null"); } - var ratingClauseString = ratingClauseBuilder.ToString(); - if (!string.Equals(ratingClauseString, "(", StringComparison.OrdinalIgnoreCase)) + if (clauseBuilder.Length > 1) { - whereClauses.Add(ratingClauseString + ")"); + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; } if (query.HasOfficialRating.HasValue) @@ -4565,7 +4505,6 @@ namespace Emby.Server.Implementations.Data return whereClauses; } -#nullable disable /// <summary> /// Formats a where clause for the specified provider. @@ -4582,6 +4521,7 @@ namespace Emby.Server.Implementations.Data provider); } +#nullable disable private List<string> GetItemByNameTypesInQuery(InternalItemsQuery query) { var list = new List<string>(); @@ -4661,24 +4601,17 @@ namespace Emby.Server.Implementations.Data return true; } - if (query.IncludeItemTypes.Contains(BaseItemKind.Episode) + return query.IncludeItemTypes.Contains(BaseItemKind.Episode) || query.IncludeItemTypes.Contains(BaseItemKind.Video) || query.IncludeItemTypes.Contains(BaseItemKind.Movie) || query.IncludeItemTypes.Contains(BaseItemKind.MusicVideo) || query.IncludeItemTypes.Contains(BaseItemKind.Series) - || query.IncludeItemTypes.Contains(BaseItemKind.Season)) - { - return true; - } - - return false; + || query.IncludeItemTypes.Contains(BaseItemKind.Season); } public void UpdateInheritedValues() { - string sql = string.Join( - ';', - new string[] + var queries = new string[] { "delete from ItemValues where type = 6", @@ -4688,16 +4621,11 @@ namespace Emby.Server.Implementations.Data FROM AncestorIds LEFT JOIN ItemValues ON (AncestorIds.AncestorId = ItemValues.ItemId) where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type = 4 " - }); + }; using (var connection = GetConnection()) { - connection.RunInTransaction( - db => - { - connection.ExecuteAll(sql); - }, - TransactionMode); + connection.RunQueries(queries); } } @@ -4794,25 +4722,25 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type CheckDisposed(); - var commandText = "select ItemId, Name, Role, PersonType, SortOrder from People p"; + StringBuilder commandText = new StringBuilder("select ItemId, Name, Role, PersonType, SortOrder from People p"); var whereClauses = GetPeopleWhereClauses(query, null); if (whereClauses.Count != 0) { - commandText += " where " + string.Join(" AND ", whereClauses); + commandText.Append(" where ").AppendJoin(" AND ", whereClauses); } - commandText += " order by ListOrder"; + commandText.Append(" order by ListOrder"); if (query.Limit > 0) { - commandText += " LIMIT " + query.Limit; + commandText.Append(" LIMIT ").Append(query.Limit); } var list = new List<PersonInfo>(); using (var connection = GetConnection(true)) - using (var statement = PrepareStatement(connection, commandText)) + using (var statement = PrepareStatement(connection, commandText.ToString())) { // Run this again to bind the params GetPeopleWhereClauses(query, statement); @@ -4930,7 +4858,7 @@ AND Type = @InternalPersonType)"); i.ToString(CultureInfo.InvariantCulture)); } - // Remove last , + // Remove trailing comma insertText.Length--; using (var statement = PrepareStatement(db, insertText.ToString())) @@ -5458,7 +5386,7 @@ AND Type = @InternalPersonType)"); i); } - // Remove last comma + // Remove trailing comma insertText.Length--; using (var statement = PrepareStatement(db, insertText.ToString())) @@ -5539,7 +5467,7 @@ AND Type = @InternalPersonType)"); i.ToString(CultureInfo.InvariantCulture)); } - // Remove last comma + // Remove trailing comma insertText.Length--; using (var statement = PrepareStatement(db, insertText.ToString())) @@ -5788,11 +5716,10 @@ AND Type = @InternalPersonType)"); { var item = new MediaStream { - Index = reader[1].ToInt() + Index = reader[1].ToInt(), + Type = Enum.Parse<MediaStreamType>(reader[2].ToString(), true) }; - item.Type = Enum.Parse<MediaStreamType>(reader[2].ToString(), true); - if (reader.TryGetString(3, out var codec)) { item.Codec = codec; @@ -6012,7 +5939,7 @@ AND Type = @InternalPersonType)"); using (var connection = GetConnection(true)) using (var statement = PrepareStatement(connection, cmdText)) { - statement.TryBind("@ItemId", query.ItemId.ToByteArray()); + statement.TryBind("@ItemId", query.ItemId); if (query.Index.HasValue) { @@ -6073,14 +6000,13 @@ AND Type = @InternalPersonType)"); for (var i = startIndex; i < endIndex; i++) { - var index = i.ToString(CultureInfo.InvariantCulture); insertText.Append("(@ItemId, "); foreach (var column in _mediaAttachmentSaveColumns.Skip(1)) { insertText.Append('@') .Append(column) - .Append(index) + .Append(i) .Append(','); } diff --git a/jellyfin.ruleset b/jellyfin.ruleset index c846e2cd41..505d35481f 100644 --- a/jellyfin.ruleset +++ b/jellyfin.ruleset @@ -89,6 +89,8 @@ <Rule Id="CA1727" Action="Error" /> <!-- error on CA1813: Avoid unsealed attributes --> <Rule Id="CA1813" Action="Error" /> + <!-- error on CA1834: Use 'StringBuilder.Append(char)' instead of 'StringBuilder.Append(string)' when the input is a constant unit string --> + <Rule Id="CA1834" Action="Error" /> <!-- error on CA1843: Do not use 'WaitAll' with a single task --> <Rule Id="CA1843" Action="Error" /> <!-- error on CA1845: Use span-based 'string.Concat' --> From a963bce9beb16f0d66ec0cef8d92f0d4f6536730 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Mon, 21 Aug 2023 19:09:32 +0200 Subject: [PATCH 512/858] Reduce log spam on failed logins Failed logins already get logged higher up the call chain --- .../Users/DefaultAuthenticationProvider.cs | 21 +++++++++++-------- .../Users/UserManager.cs | 2 +- .../Authentication/IAuthenticationProvider.cs | 8 +++---- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs index 72f3d6e8ec..cb2d09a670 100644 --- a/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs +++ b/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Jellyfin.Data.Entities; using MediaBrowser.Controller.Authentication; @@ -39,14 +40,18 @@ namespace Jellyfin.Server.Implementations.Users /// <inheritdoc /> // This is the version that we need to use for local users. Because reasons. - public Task<ProviderAuthenticationResult> Authenticate(string username, string password, User resolvedUser) + public Task<ProviderAuthenticationResult> Authenticate(string username, string password, User? resolvedUser) { - if (resolvedUser is null) + [DoesNotReturn] + static void ThrowAuthenticationException() { - throw new AuthenticationException("Specified user does not exist."); + throw new AuthenticationException("Invalid username or password"); } - bool success = false; + if (resolvedUser is null) + { + ThrowAuthenticationException(); + } // As long as jellyfin supports password-less users, we need this little block here to accommodate if (!HasPassword(resolvedUser) && string.IsNullOrEmpty(password)) @@ -60,15 +65,13 @@ namespace Jellyfin.Server.Implementations.Users // Handle the case when the stored password is null, but the user tried to login with a password if (resolvedUser.Password is null) { - throw new AuthenticationException("Invalid username or password"); + ThrowAuthenticationException(); } PasswordHash readyHash = PasswordHash.Parse(resolvedUser.Password); - success = _cryptographyProvider.Verify(readyHash, password); - - if (!success) + if (!_cryptographyProvider.Verify(readyHash, password)) { - throw new AuthenticationException("Invalid username or password"); + ThrowAuthenticationException(); } // Migrate old hashes to the new default diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index ec0c64cd72..5010751ddb 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -833,7 +833,7 @@ namespace Jellyfin.Server.Implementations.Users } catch (AuthenticationException ex) { - _logger.LogError(ex, "Error authenticating with provider {Provider}", provider.Name); + _logger.LogDebug(ex, "Error authenticating with provider {Provider}", provider.Name); return (username, false); } diff --git a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs index a56d3c8223..81b532fda8 100644 --- a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs +++ b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System.Threading.Tasks; @@ -23,7 +21,7 @@ namespace MediaBrowser.Controller.Authentication public interface IRequiresResolvedUser { - Task<ProviderAuthenticationResult> Authenticate(string username, string password, User resolvedUser); + Task<ProviderAuthenticationResult> Authenticate(string username, string password, User? resolvedUser); } public interface IHasNewUserPolicy @@ -33,8 +31,8 @@ namespace MediaBrowser.Controller.Authentication public class ProviderAuthenticationResult { - public string Username { get; set; } + public required string Username { get; set; } - public string DisplayName { get; set; } + public string? DisplayName { get; set; } } } From fb511dbae2ccca240e02818f7cba8d6d933fcadd Mon Sep 17 00:00:00 2001 From: cvium <clausvium@gmail.com> Date: Mon, 21 Aug 2023 20:34:50 +0200 Subject: [PATCH 513/858] rename variable and fix crash --- .../Data/BaseSqliteRepository.cs | 18 +++++++++--------- .../Data/SqliteItemRepository.cs | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs index 6ee2d800cd..2152edaf49 100644 --- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs +++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs @@ -100,41 +100,41 @@ namespace Emby.Server.Implementations.Data protected SqliteConnection GetConnection(bool readOnly = false) { - var writeConnection = new SqliteConnection($"Filename={DbFilePath}"); + var connection = new SqliteConnection($"Filename={DbFilePath}"); if (CacheSize.HasValue) { - writeConnection.Execute("PRAGMA cache_size=" + CacheSize.Value); + connection.Execute("PRAGMA cache_size=" + CacheSize.Value); } if (!string.IsNullOrWhiteSpace(LockingMode)) { - writeConnection.Execute("PRAGMA locking_mode=" + LockingMode); + connection.Execute("PRAGMA locking_mode=" + LockingMode); } if (!string.IsNullOrWhiteSpace(JournalMode)) { - writeConnection.Execute("PRAGMA journal_mode=" + JournalMode); + connection.Execute("PRAGMA journal_mode=" + JournalMode); } if (JournalSizeLimit.HasValue) { - writeConnection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value); + connection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value); } if (Synchronous.HasValue) { - writeConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value); + connection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value); } if (PageSize.HasValue) { - writeConnection.Execute("PRAGMA page_size=" + PageSize.Value); + connection.Execute("PRAGMA page_size=" + PageSize.Value); } - writeConnection.Execute("PRAGMA temp_store=" + (int)TempStore); + connection.Execute("PRAGMA temp_store=" + (int)TempStore); - return writeConnection; + return connection; } public SqliteCommand PrepareStatement(SqliteConnection connection, string sql) diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 7e1c3bb4ca..ad7ade54e3 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -5917,7 +5917,7 @@ AND Type = @InternalPersonType)"); item.DvBlSignalCompatibilityId = dvBlSignalCompatibilityId; } - item.IsHearingImpaired = reader.GetBoolean(43); + item.IsHearingImpaired = reader.TryGetBoolean(43, out var result) && result; if (item.Type == MediaStreamType.Subtitle) { From cf04b43fa4396a67705a2c276cc76d633a075c46 Mon Sep 17 00:00:00 2001 From: cvium <clausvium@gmail.com> Date: Mon, 21 Aug 2023 21:37:18 +0200 Subject: [PATCH 514/858] simplify extension methods --- .../Data/BaseSqliteRepository.cs | 1 + .../Data/SqliteExtensions.cs | 40 +--- .../Data/SqliteItemRepository.cs | 222 +++++++++--------- .../Data/SqliteUserDataRepository.cs | 4 +- 4 files changed, 114 insertions(+), 153 deletions(-) diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs index 2152edaf49..e257c9edc7 100644 --- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs +++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs @@ -134,6 +134,7 @@ namespace Emby.Server.Implementations.Data connection.Execute("PRAGMA temp_store=" + (int)TempStore); + connection.Open(); return connection; } diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs index 40cecbb6bc..14f0f58307 100644 --- a/Emby.Server.Implementations/Data/SqliteExtensions.cs +++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs @@ -53,14 +53,6 @@ namespace Emby.Server.Implementations.Data "yy-MM-dd" }; - private static void EnsureOpen(this SqliteConnection sqliteConnection) - { - if (sqliteConnection.State == ConnectionState.Closed) - { - sqliteConnection.Open(); - } - } - public static IEnumerable<SqliteDataReader> Query(this SqliteConnection sqliteConnection, string commandText) { if (sqliteConnection.State != ConnectionState.Open) @@ -81,29 +73,11 @@ namespace Emby.Server.Implementations.Data public static void Execute(this SqliteConnection sqliteConnection, string commandText) { - sqliteConnection.EnsureOpen(); using var command = sqliteConnection.CreateCommand(); command.CommandText = commandText; command.ExecuteNonQuery(); } - public static void ExecuteAll(this SqliteConnection sqliteConnection, string commandText) - { - sqliteConnection.EnsureOpen(); - - using var command = sqliteConnection.CreateCommand(); - command.CommandText = commandText; - command.ExecuteNonQuery(); - } - - public static void RunQueries(this SqliteConnection connection, string[] queries) - { - ArgumentNullException.ThrowIfNull(queries); - using var transaction = connection.BeginTransaction(); - connection.ExecuteAll(string.Join(';', queries)); - transaction.Commit(); - } - public static string ToDateTimeParamValue(this DateTime dateValue) { var kind = DateTimeKind.Utc; @@ -239,6 +213,7 @@ namespace Emby.Server.Implementations.Data } else { + // Blobs aren't always detected automatically if (isBlob) { statement.Parameters.Add(new SqliteParameter(name, SqliteType.Blob) { Value = value }); @@ -250,18 +225,6 @@ namespace Emby.Server.Implementations.Data } } - public static void TryBind(this SqliteCommand statement, string name, byte[] value) - { - if (statement.Parameters.Contains(name)) - { - statement.Parameters[name].Value = value; - } - else - { - statement.Parameters.Add(new SqliteParameter(name, SqliteType.Blob, value.Length) { Value = value }); - } - } - public static void TryBindNull(this SqliteCommand statement, string name) { statement.TryBind(name, DBNull.Value); @@ -286,7 +249,6 @@ namespace Emby.Server.Implementations.Data public static SqliteCommand PrepareStatement(this SqliteConnection sqliteConnection, string sql) { - sqliteConnection.EnsureOpen(); var command = sqliteConnection.CreateCommand(); command.CommandText = sql; return command; diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index ad7ade54e3..d30d35b256 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -437,128 +437,126 @@ namespace Emby.Server.Implementations.Data }; using (var connection = GetConnection()) + using (var transaction = connection.BeginTransaction()) { - connection.RunQueries(queries); + connection.Execute(string.Join(';', queries)); - using (var transaction = connection.BeginTransaction()) - { - var existingColumnNames = GetColumnNames(connection, "AncestorIds"); - AddColumn(connection, "AncestorIds", "AncestorIdText", "Text", existingColumnNames); + var existingColumnNames = GetColumnNames(connection, "AncestorIds"); + AddColumn(connection, "AncestorIds", "AncestorIdText", "Text", existingColumnNames); - existingColumnNames = GetColumnNames(connection, "TypedBaseItems"); + existingColumnNames = GetColumnNames(connection, "TypedBaseItems"); - AddColumn(connection, "TypedBaseItems", "Path", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "StartDate", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "EndDate", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ChannelId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IsMovie", "BIT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "CommunityRating", "Float", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "CustomRating", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IndexNumber", "INT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IsLocked", "BIT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Name", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "OfficialRating", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "MediaType", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Overview", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ParentIndexNumber", "INT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "PremiereDate", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ProductionYear", "INT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ParentId", "GUID", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Genres", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "SortName", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ForcedSortName", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "RunTimeTicks", "BIGINT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "DateCreated", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "DateModified", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IsSeries", "BIT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "EpisodeTitle", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IsRepeat", "BIT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "PreferredMetadataLanguage", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "PreferredMetadataCountryCode", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "DateLastRefreshed", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "DateLastSaved", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IsInMixedFolder", "BIT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "LockedFields", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Studios", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Audio", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ExternalServiceId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Tags", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IsFolder", "BIT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "InheritedParentalRatingValue", "INT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "UnratedType", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "TopParentId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "TrailerTypes", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "CriticRating", "Float", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "CleanName", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "PresentationUniqueKey", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "OriginalTitle", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "PrimaryVersionId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "DateLastMediaAdded", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Album", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "LUFS", "Float", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IsVirtualItem", "BIT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "SeriesName", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "UserDataKey", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "SeasonName", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "SeasonId", "GUID", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "SeriesId", "GUID", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ExternalSeriesId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Tagline", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ProviderIds", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Images", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ProductionLocations", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ExtraIds", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "TotalBitrate", "INT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ExtraType", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Artists", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "AlbumArtists", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ExternalId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "SeriesPresentationUniqueKey", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ShowId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "OwnerId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Width", "INT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Height", "INT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Size", "BIGINT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Path", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "StartDate", "DATETIME", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "EndDate", "DATETIME", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ChannelId", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "IsMovie", "BIT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "CommunityRating", "Float", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "CustomRating", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "IndexNumber", "INT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "IsLocked", "BIT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Name", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "OfficialRating", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "MediaType", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Overview", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ParentIndexNumber", "INT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "PremiereDate", "DATETIME", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ProductionYear", "INT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ParentId", "GUID", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Genres", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "SortName", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ForcedSortName", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "RunTimeTicks", "BIGINT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "DateCreated", "DATETIME", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "DateModified", "DATETIME", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "IsSeries", "BIT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "EpisodeTitle", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "IsRepeat", "BIT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "PreferredMetadataLanguage", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "PreferredMetadataCountryCode", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "DateLastRefreshed", "DATETIME", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "DateLastSaved", "DATETIME", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "IsInMixedFolder", "BIT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "LockedFields", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Studios", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Audio", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ExternalServiceId", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Tags", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "IsFolder", "BIT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "InheritedParentalRatingValue", "INT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "UnratedType", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "TopParentId", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "TrailerTypes", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "CriticRating", "Float", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "CleanName", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "PresentationUniqueKey", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "OriginalTitle", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "PrimaryVersionId", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "DateLastMediaAdded", "DATETIME", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Album", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "LUFS", "Float", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "IsVirtualItem", "BIT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "SeriesName", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "UserDataKey", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "SeasonName", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "SeasonId", "GUID", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "SeriesId", "GUID", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ExternalSeriesId", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Tagline", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ProviderIds", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Images", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ProductionLocations", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ExtraIds", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "TotalBitrate", "INT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ExtraType", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Artists", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "AlbumArtists", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ExternalId", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "SeriesPresentationUniqueKey", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ShowId", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "OwnerId", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Width", "INT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Height", "INT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Size", "BIGINT", existingColumnNames); - existingColumnNames = GetColumnNames(connection, "ItemValues"); - AddColumn(connection, "ItemValues", "CleanValue", "Text", existingColumnNames); + existingColumnNames = GetColumnNames(connection, "ItemValues"); + AddColumn(connection, "ItemValues", "CleanValue", "Text", existingColumnNames); - existingColumnNames = GetColumnNames(connection, ChaptersTableName); - AddColumn(connection, ChaptersTableName, "ImageDateModified", "DATETIME", existingColumnNames); + existingColumnNames = GetColumnNames(connection, ChaptersTableName); + AddColumn(connection, ChaptersTableName, "ImageDateModified", "DATETIME", existingColumnNames); - existingColumnNames = GetColumnNames(connection, "MediaStreams"); - AddColumn(connection, "MediaStreams", "IsAvc", "BIT", existingColumnNames); - AddColumn(connection, "MediaStreams", "TimeBase", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "CodecTimeBase", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "Title", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "NalLengthSize", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "Comment", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "CodecTag", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "PixelFormat", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "BitDepth", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "RefFrames", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "KeyFrames", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "IsAnamorphic", "BIT", existingColumnNames); + existingColumnNames = GetColumnNames(connection, "MediaStreams"); + AddColumn(connection, "MediaStreams", "IsAvc", "BIT", existingColumnNames); + AddColumn(connection, "MediaStreams", "TimeBase", "TEXT", existingColumnNames); + AddColumn(connection, "MediaStreams", "CodecTimeBase", "TEXT", existingColumnNames); + AddColumn(connection, "MediaStreams", "Title", "TEXT", existingColumnNames); + AddColumn(connection, "MediaStreams", "NalLengthSize", "TEXT", existingColumnNames); + AddColumn(connection, "MediaStreams", "Comment", "TEXT", existingColumnNames); + AddColumn(connection, "MediaStreams", "CodecTag", "TEXT", existingColumnNames); + AddColumn(connection, "MediaStreams", "PixelFormat", "TEXT", existingColumnNames); + AddColumn(connection, "MediaStreams", "BitDepth", "INT", existingColumnNames); + AddColumn(connection, "MediaStreams", "RefFrames", "INT", existingColumnNames); + AddColumn(connection, "MediaStreams", "KeyFrames", "TEXT", existingColumnNames); + AddColumn(connection, "MediaStreams", "IsAnamorphic", "BIT", existingColumnNames); - AddColumn(connection, "MediaStreams", "ColorPrimaries", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "ColorSpace", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "ColorTransfer", "TEXT", existingColumnNames); + AddColumn(connection, "MediaStreams", "ColorPrimaries", "TEXT", existingColumnNames); + AddColumn(connection, "MediaStreams", "ColorSpace", "TEXT", existingColumnNames); + AddColumn(connection, "MediaStreams", "ColorTransfer", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "DvVersionMajor", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "DvVersionMinor", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "DvProfile", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "DvLevel", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "RpuPresentFlag", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "ElPresentFlag", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "BlPresentFlag", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "DvBlSignalCompatibilityId", "INT", existingColumnNames); + AddColumn(connection, "MediaStreams", "DvVersionMajor", "INT", existingColumnNames); + AddColumn(connection, "MediaStreams", "DvVersionMinor", "INT", existingColumnNames); + AddColumn(connection, "MediaStreams", "DvProfile", "INT", existingColumnNames); + AddColumn(connection, "MediaStreams", "DvLevel", "INT", existingColumnNames); + AddColumn(connection, "MediaStreams", "RpuPresentFlag", "INT", existingColumnNames); + AddColumn(connection, "MediaStreams", "ElPresentFlag", "INT", existingColumnNames); + AddColumn(connection, "MediaStreams", "BlPresentFlag", "INT", existingColumnNames); + AddColumn(connection, "MediaStreams", "DvBlSignalCompatibilityId", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "IsHearingImpaired", "BIT", existingColumnNames); + AddColumn(connection, "MediaStreams", "IsHearingImpaired", "BIT", existingColumnNames); - transaction.Commit(); - } + connection.Execute(string.Join(';', postQueries)); - connection.RunQueries(postQueries); + transaction.Commit(); } } @@ -674,7 +672,7 @@ namespace Emby.Server.Implementations.Data if (TypeRequiresDeserialization(type)) { - saveItemStatement.TryBind("@data", JsonSerializer.SerializeToUtf8Bytes(item, type, _jsonOptions)); + saveItemStatement.TryBind("@data", JsonSerializer.SerializeToUtf8Bytes(item, type, _jsonOptions), true); } else { @@ -4656,7 +4654,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type """; using var connection = GetConnection(); using var transaction = connection.BeginTransaction(); - connection.ExecuteAll(Statements); + connection.Execute(Statements); transaction.Commit(); } diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs index 619a644874..45da8fd3f3 100644 --- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs @@ -45,7 +45,7 @@ namespace Emby.Server.Implementations.Data var users = userDatasTableExists ? null : _userManager.Users; using var transaction = connection.BeginTransaction(); - connection.ExecuteAll(string.Join(';', new[] + connection.Execute(string.Join(';', new[] { "create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)", @@ -80,7 +80,7 @@ namespace Emby.Server.Implementations.Data ImportUserIds(connection, users); - connection.ExecuteAll("INSERT INTO UserDatas (key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex) SELECT key, InternalUserId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex from userdata where InternalUserId not null"); + connection.Execute("INSERT INTO UserDatas (key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex) SELECT key, InternalUserId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex from userdata where InternalUserId not null"); transaction.Commit(); } From 791413a50f3ad8afc0a554f16828e70c3c8752b8 Mon Sep 17 00:00:00 2001 From: cvium <clausvium@gmail.com> Date: Mon, 21 Aug 2023 21:38:16 +0200 Subject: [PATCH 515/858] open before changing pragmas --- Emby.Server.Implementations/Data/BaseSqliteRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs index e257c9edc7..7b3c7b0bb4 100644 --- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs +++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs @@ -101,6 +101,7 @@ namespace Emby.Server.Implementations.Data protected SqliteConnection GetConnection(bool readOnly = false) { var connection = new SqliteConnection($"Filename={DbFilePath}"); + connection.Open(); if (CacheSize.HasValue) { @@ -134,7 +135,6 @@ namespace Emby.Server.Implementations.Data connection.Execute("PRAGMA temp_store=" + (int)TempStore); - connection.Open(); return connection; } From e7016e38b87afa5655f216304464341f774fcc8a Mon Sep 17 00:00:00 2001 From: cvium <clausvium@gmail.com> Date: Mon, 21 Aug 2023 21:49:39 +0200 Subject: [PATCH 516/858] remove readonly --- .../Data/BaseSqliteRepository.cs | 2 +- .../Data/SqliteItemRepository.cs | 28 +++++++++---------- .../Data/SqliteUserDataRepository.cs | 2 +- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs index 7b3c7b0bb4..bf079d90ca 100644 --- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs +++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs @@ -98,7 +98,7 @@ namespace Emby.Server.Implementations.Data } } - protected SqliteConnection GetConnection(bool readOnly = false) + protected SqliteConnection GetConnection() { var connection = new SqliteConnection($"Filename={DbFilePath}"); connection.Open(); diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index d30d35b256..d18dcf1279 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -1273,7 +1273,7 @@ namespace Emby.Server.Implementations.Data CheckDisposed(); - using (var connection = GetConnection(true)) + using (var connection = GetConnection()) using (var statement = PrepareStatement(connection, _retrieveItemColumnsSelectQuery)) { statement.TryBind("@guid", id); @@ -1956,7 +1956,7 @@ namespace Emby.Server.Implementations.Data CheckDisposed(); var chapters = new List<ChapterInfo>(); - using (var connection = GetConnection(true)) + using (var connection = GetConnection()) using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId order by ChapterIndex asc")) { statement.TryBind("@ItemId", item.Id); @@ -1975,7 +1975,7 @@ namespace Emby.Server.Implementations.Data { CheckDisposed(); - using (var connection = GetConnection(true)) + using (var connection = GetConnection()) using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId and ChapterIndex=@ChapterIndex")) { statement.TryBind("@ItemId", item.Id); @@ -2557,7 +2557,7 @@ namespace Emby.Server.Implementations.Data var commandText = commandTextBuilder.ToString(); using (new QueryTimeLogger(Logger, commandText)) - using (var connection = GetConnection(true)) + using (var connection = GetConnection()) using (var statement = PrepareStatement(connection, commandText)) { if (EnableJoinUserData(query)) @@ -2625,7 +2625,7 @@ namespace Emby.Server.Implementations.Data var commandText = commandTextBuilder.ToString(); var items = new List<BaseItem>(); using (new QueryTimeLogger(Logger, commandText)) - using (var connection = GetConnection(true)) + using (var connection = GetConnection()) using (var statement = PrepareStatement(connection, commandText)) { if (EnableJoinUserData(query)) @@ -2833,7 +2833,7 @@ namespace Emby.Server.Implementations.Data var list = new List<BaseItem>(); var result = new QueryResult<BaseItem>(); - using var connection = GetConnection(true); + using var connection = GetConnection(); using var transaction = connection.BeginTransaction(); if (!isReturningZeroItems) { @@ -3141,7 +3141,7 @@ namespace Emby.Server.Implementations.Data var commandText = commandTextBuilder.ToString(); var list = new List<Guid>(); using (new QueryTimeLogger(Logger, commandText)) - using (var connection = GetConnection(true)) + using (var connection = GetConnection()) using (var statement = PrepareStatement(connection, commandText)) { if (EnableJoinUserData(query)) @@ -4723,7 +4723,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type } var list = new List<string>(); - using (var connection = GetConnection(true)) + using (var connection = GetConnection()) using (var statement = PrepareStatement(connection, commandText.ToString())) { // Run this again to bind the params @@ -4761,7 +4761,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type } var list = new List<PersonInfo>(); - using (var connection = GetConnection(true)) + using (var connection = GetConnection()) using (var statement = PrepareStatement(connection, commandText)) { // Run this again to bind the params @@ -5003,7 +5003,7 @@ AND Type = @InternalPersonType)"); var list = new List<string>(); using (new QueryTimeLogger(Logger, commandText)) - using (var connection = GetConnection(true)) + using (var connection = GetConnection()) using (var statement = PrepareStatement(connection, commandText)) { foreach (var row in statement.ExecuteQuery()) @@ -5203,8 +5203,8 @@ AND Type = @InternalPersonType)"); var list = new List<(BaseItem, ItemCounts)>(); var result = new QueryResult<(BaseItem, ItemCounts)>(); using (new QueryTimeLogger(Logger, commandText)) - using (var connection = GetConnection(true)) - using (var transaction = connection.BeginTransaction()) + using (var connection = GetConnection()) + using (var transaction = connection.BeginTransaction(deferred: true)) { if (!isReturningZeroItems) { @@ -5557,7 +5557,7 @@ AND Type = @InternalPersonType)"); cmdText += " order by StreamIndex ASC"; - using (var connection = GetConnection(true)) + using (var connection = GetConnection()) { var list = new List<MediaStream>(); @@ -5945,7 +5945,7 @@ AND Type = @InternalPersonType)"); cmdText += " order by AttachmentIndex ASC"; var list = new List<MediaAttachment>(); - using (var connection = GetConnection(true)) + using (var connection = GetConnection()) using (var statement = PrepareStatement(connection, cmdText)) { statement.TryBind("@ItemId", query.ItemId); diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs index 45da8fd3f3..f7c4be3980 100644 --- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs @@ -267,7 +267,7 @@ namespace Emby.Server.Implementations.Data ArgumentException.ThrowIfNullOrEmpty(key); - using (var connection = GetConnection(true)) + using (var connection = GetConnection()) { using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId")) { From a061e8f8e49c271e63a15563bc7d1490b64b1458 Mon Sep 17 00:00:00 2001 From: cvium <clausvium@gmail.com> Date: Mon, 21 Aug 2023 21:54:56 +0200 Subject: [PATCH 517/858] fix bad merge --- Emby.Server.Implementations/Data/SqliteItemRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index e95f55f5f2..96870a719d 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -4697,7 +4697,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type var list = new List<PersonInfo>(); using (var connection = GetConnection()) - using (var statement = PrepareStatement(connection, commandText)) + using (var statement = PrepareStatement(connection, commandText.ToString())) { // Run this again to bind the params GetPeopleWhereClauses(query, statement); From d1190c52155bd1b487b9f2d39c4e94d0bf8857b2 Mon Sep 17 00:00:00 2001 From: cvium <clausvium@gmail.com> Date: Mon, 21 Aug 2023 22:12:08 +0200 Subject: [PATCH 518/858] fix userdata table not being committed --- .../Data/SqliteUserDataRepository.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs index f7c4be3980..a5edcc58c0 100644 --- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs @@ -45,10 +45,9 @@ namespace Emby.Server.Implementations.Data var users = userDatasTableExists ? null : _userManager.Users; using var transaction = connection.BeginTransaction(); - connection.Execute(string.Join(';', new[] - { + connection.Execute(string.Join( + ';', "create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)", - "drop index if exists idx_userdata", "drop index if exists idx_userdata1", "drop index if exists idx_userdata2", @@ -59,11 +58,11 @@ namespace Emby.Server.Implementations.Data "create unique index if not exists UserDatasIndex1 on UserDatas (key, userId)", "create index if not exists UserDatasIndex2 on UserDatas (key, userId, played)", "create index if not exists UserDatasIndex3 on UserDatas (key, userId, playbackPositionTicks)", - "create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)" - })); + "create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)")); if (!userDataTableExists) { + transaction.Commit(); return; } From 7e46d6bcc75bbca9462d31694e814b52491308e5 Mon Sep 17 00:00:00 2001 From: FantasyGmm <16450052+FantasyGmm@users.noreply.github.com> Date: Tue, 22 Aug 2023 11:44:43 +0800 Subject: [PATCH 519/858] fix debian/ubuntu arm64 build error,runtime argument missing --- debian/rules | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/debian/rules b/debian/rules index f55b1807ea..069d48aad1 100755 --- a/debian/rules +++ b/debian/rules @@ -25,6 +25,10 @@ ifeq ($(HOST_ARCH),arm64) # Building ARM DOTNETRUNTIME := debian-arm64 endif +ifeq ($(HOST_ARCH),aarch64) + # Building ARM + DOTNETRUNTIME := debian-arm64 +endif export DH_VERBOSE=1 export DOTNET_CLI_TELEMETRY_OPTOUT=1 From 0d3d9490e5997824cfd97c6776ca000be127deef Mon Sep 17 00:00:00 2001 From: cvium <clausvium@gmail.com> Date: Tue, 22 Aug 2023 07:27:21 +0200 Subject: [PATCH 520/858] remove unused deps --- Emby.Server.Implementations/Data/SqliteExtensions.cs | 1 - Emby.Server.Implementations/Data/SqliteItemRepository.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs index 14f0f58307..f3b3da64f4 100644 --- a/Emby.Server.Implementations/Data/SqliteExtensions.cs +++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Data; -using System.Diagnostics; using System.Globalization; using Microsoft.Data.Sqlite; diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 96870a719d..ca121f8a20 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -3,7 +3,6 @@ #pragma warning disable CS1591 using System; -using System.Buffers.Text; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; From 05e40ecb933f1b8734e026dd3cefe9e05122b712 Mon Sep 17 00:00:00 2001 From: cvium <clausvium@gmail.com> Date: Tue, 22 Aug 2023 08:31:34 +0200 Subject: [PATCH 521/858] review comments --- Emby.Server.Implementations/Data/SqliteExtensions.cs | 1 + Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs index f3b3da64f4..30e334393c 100644 --- a/Emby.Server.Implementations/Data/SqliteExtensions.cs +++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs @@ -243,6 +243,7 @@ namespace Emby.Server.Implementations.Data public static int SelectScalarInt(this SqliteCommand command) { var result = command.ExecuteScalar(); + // Can't be null since the method is used to retrieve Count return Convert.ToInt32(result!, CultureInfo.InvariantCulture); } diff --git a/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs b/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs index 98017e3ef4..6c34e1f5bb 100644 --- a/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs +++ b/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs @@ -39,11 +39,11 @@ namespace Jellyfin.Server.Migrations.Routines var dataPath = _paths.DataPath; var dbPath = Path.Combine(dataPath, DbFilename); using (var connection = new SqliteConnection($"Filename={dbPath}")) + using (var transaction = connection.BeginTransaction()) { // Query the database for the ids of duplicate extras var queryResult = connection.Query("SELECT t1.Path FROM TypedBaseItems AS t1, TypedBaseItems AS t2 WHERE t1.Path=t2.Path AND t1.Type!=t2.Type AND t1.Type='MediaBrowser.Controller.Entities.Video'"); - // TODO does this LINQ execute before the reader is disposed? - var bads = string.Join(", ", queryResult.Select(x => x.GetString(0)).ToList()); + var bads = string.Join(", ", queryResult.Select(x => x.GetString(0))); // Do nothing if no duplicate extras were detected if (bads.Length == 0) @@ -75,6 +75,7 @@ namespace Jellyfin.Server.Migrations.Routines // Delete all duplicate extras _logger.LogInformation("Removing found duplicated extras for the following items: {DuplicateExtras}", bads); connection.Execute("DELETE FROM TypedBaseItems WHERE rowid IN (SELECT t1.rowid FROM TypedBaseItems AS t1, TypedBaseItems AS t2 WHERE t1.Path=t2.Path AND t1.Type!=t2.Type AND t1.Type='MediaBrowser.Controller.Entities.Video')"); + transaction.Commit(); } } } From d92e9ae85e41fef981729f544bfd6df2c052a712 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Tue, 22 Aug 2023 18:09:31 +0200 Subject: [PATCH 522/858] Enable nullable for more files and add tests Adds basic tests for FFProbeVideoInfo.CreateDummyChapters Fixed error message CreateDummyChapters instead of reporting the total minutes it only reported the minute component --- .../Data/SqliteItemRepository.cs | 13 +- .../Drawing/IImageProcessor.cs | 2 +- .../Entities/ItemImageInfo.cs | 8 +- .../Security/IAuthenticationManager.cs | 4 +- MediaBrowser.Model/Entities/ChapterInfo.cs | 7 +- .../MediaInfo/FFProbeVideoInfo.cs | 140 +++++++----------- src/Jellyfin.Drawing/ImageProcessor.cs | 8 +- .../Jellyfin.Providers.Tests.csproj | 3 + .../MediaInfo/FFProbeVideoInfoTests.cs | 61 ++++++++ 9 files changed, 136 insertions(+), 110 deletions(-) create mode 100644 tests/Jellyfin.Providers.Tests/MediaInfo/FFProbeVideoInfoTests.cs diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 94b4f48455..e32a2ac04a 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -1971,18 +1971,7 @@ namespace Emby.Server.Implementations.Data if (reader.TryGetString(2, out var imagePath)) { chapter.ImagePath = imagePath; - - if (!string.IsNullOrEmpty(chapter.ImagePath)) - { - try - { - chapter.ImageTag = _imageProcessor.GetImageCacheTag(item, chapter); - } - catch (Exception ex) - { - Logger.LogError(ex, "Failed to create image cache tag."); - } - } + chapter.ImageTag = _imageProcessor.GetImageCacheTag(item, chapter); } if (reader.TryReadDateTime(3, out var imageDateModified)) diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs index e5ce0aa210..cdc3d52b9b 100644 --- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs +++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs @@ -66,7 +66,7 @@ namespace MediaBrowser.Controller.Drawing /// <returns>Guid.</returns> string GetImageCacheTag(BaseItem item, ItemImageInfo image); - string GetImageCacheTag(BaseItem item, ChapterInfo chapter); + string? GetImageCacheTag(BaseItem item, ChapterInfo chapter); string? GetImageCacheTag(User user); diff --git a/MediaBrowser.Controller/Entities/ItemImageInfo.cs b/MediaBrowser.Controller/Entities/ItemImageInfo.cs index 0171af27cf..1d45d4da0b 100644 --- a/MediaBrowser.Controller/Entities/ItemImageInfo.cs +++ b/MediaBrowser.Controller/Entities/ItemImageInfo.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -14,7 +12,7 @@ namespace MediaBrowser.Controller.Entities /// Gets or sets the path. /// </summary> /// <value>The path.</value> - public string Path { get; set; } + public required string Path { get; set; } /// <summary> /// Gets or sets the type. @@ -36,9 +34,9 @@ namespace MediaBrowser.Controller.Entities /// Gets or sets the blurhash. /// </summary> /// <value>The blurhash.</value> - public string BlurHash { get; set; } + public string? BlurHash { get; set; } [JsonIgnore] - public bool IsLocalFile => Path is null || !Path.StartsWith("http", StringComparison.OrdinalIgnoreCase); + public bool IsLocalFile => !Path.StartsWith("http", StringComparison.OrdinalIgnoreCase); } } diff --git a/MediaBrowser.Controller/Security/IAuthenticationManager.cs b/MediaBrowser.Controller/Security/IAuthenticationManager.cs index e3d18c8c09..070ab7a856 100644 --- a/MediaBrowser.Controller/Security/IAuthenticationManager.cs +++ b/MediaBrowser.Controller/Security/IAuthenticationManager.cs @@ -1,6 +1,4 @@ -#nullable enable - -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading.Tasks; namespace MediaBrowser.Controller.Security diff --git a/MediaBrowser.Model/Entities/ChapterInfo.cs b/MediaBrowser.Model/Entities/ChapterInfo.cs index 45554c3dc0..d6b9056512 100644 --- a/MediaBrowser.Model/Entities/ChapterInfo.cs +++ b/MediaBrowser.Model/Entities/ChapterInfo.cs @@ -1,4 +1,3 @@ -#nullable disable #pragma warning disable CS1591 using System; @@ -20,16 +19,16 @@ namespace MediaBrowser.Model.Entities /// Gets or sets the name. /// </summary> /// <value>The name.</value> - public string Name { get; set; } + public string? Name { get; set; } /// <summary> /// Gets or sets the image path. /// </summary> /// <value>The image path.</value> - public string ImagePath { get; set; } + public string? ImagePath { get; set; } public DateTime ImageDateModified { get; set; } - public string ImageTag { get; set; } + public string? ImageTag { get; set; } } } diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index 213639371a..2ff3c75803 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -1,11 +1,8 @@ -#nullable disable - #pragma warning disable CA1068, CS1591 using System; using System.Collections.Generic; using System.Globalization; -using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -83,9 +80,9 @@ namespace MediaBrowser.Providers.MediaInfo CancellationToken cancellationToken) where T : Video { - BlurayDiscInfo blurayDiscInfo = null; + BlurayDiscInfo? blurayDiscInfo = null; - Model.MediaInfo.MediaInfo mediaInfoResult = null; + Model.MediaInfo.MediaInfo? mediaInfoResult = null; if (!item.IsShortcut || options.EnableRemoteContentProbe) { @@ -131,7 +128,7 @@ namespace MediaBrowser.Providers.MediaInfo var m2ts = _mediaEncoder.GetPrimaryPlaylistM2tsFiles(item.Path); // Return if no playable .m2ts files are found - if (blurayDiscInfo.Files.Length == 0 || m2ts.Count == 0) + if (blurayDiscInfo == null || blurayDiscInfo.Files.Length == 0 || m2ts.Count == 0) { _logger.LogError("No playable .m2ts files found in Blu-ray structure, skipping FFprobe."); return ItemUpdateType.MetadataImport; @@ -192,16 +189,14 @@ namespace MediaBrowser.Providers.MediaInfo protected async Task Fetch( Video video, CancellationToken cancellationToken, - Model.MediaInfo.MediaInfo mediaInfo, - BlurayDiscInfo blurayInfo, + Model.MediaInfo.MediaInfo? mediaInfo, + BlurayDiscInfo? blurayInfo, MetadataRefreshOptions options) { - List<MediaStream> mediaStreams; + List<MediaStream> mediaStreams = new List<MediaStream>(); IReadOnlyList<MediaAttachment> mediaAttachments; ChapterInfo[] chapters; - mediaStreams = new List<MediaStream>(); - // Add external streams before adding the streams from the file to preserve stream IDs on remote videos await AddExternalSubtitlesAsync(video, mediaStreams, options, cancellationToken).ConfigureAwait(false); @@ -221,18 +216,6 @@ namespace MediaBrowser.Providers.MediaInfo video.TotalBitrate = mediaInfo.Bitrate; video.RunTimeTicks = mediaInfo.RunTimeTicks; video.Size = mediaInfo.Size; - - if (video.VideoType == VideoType.VideoFile) - { - var extension = (Path.GetExtension(video.Path) ?? string.Empty).TrimStart('.'); - - video.Container = extension; - } - else - { - video.Container = null; - } - video.Container = mediaInfo.Container; chapters = mediaInfo.Chapters ?? Array.Empty<ChapterInfo>(); @@ -243,8 +226,7 @@ namespace MediaBrowser.Providers.MediaInfo } else { - var currentMediaStreams = video.GetMediaStreams(); - foreach (var mediaStream in currentMediaStreams) + foreach (var mediaStream in video.GetMediaStreams()) { if (!mediaStream.IsExternal) { @@ -295,8 +277,8 @@ namespace MediaBrowser.Providers.MediaInfo _itemRepo.SaveMediaAttachments(video.Id, mediaAttachments, cancellationToken); } - if (options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh || - options.MetadataRefreshMode == MetadataRefreshMode.Default) + if (options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh + || options.MetadataRefreshMode == MetadataRefreshMode.Default) { if (_config.Configuration.DummyChapterDuration > 0 && chapters.Length == 0 && mediaStreams.Any(i => i.Type == MediaStreamType.Video)) { @@ -321,11 +303,11 @@ namespace MediaBrowser.Providers.MediaInfo { for (int i = 0; i < chapters.Length; i++) { - string name = chapters[i].Name; + string? name = chapters[i].Name; // Check if the name is empty and/or if the name is a time // Some ripping programs do that. - if (string.IsNullOrWhiteSpace(name) || - TimeSpan.TryParse(name, out _)) + if (string.IsNullOrWhiteSpace(name) + || TimeSpan.TryParse(name, out _)) { chapters[i].Name = string.Format( CultureInfo.InvariantCulture, @@ -384,23 +366,18 @@ namespace MediaBrowser.Providers.MediaInfo // Use the ffprobe values if these are empty if (videoStream is not null) { - videoStream.BitRate = IsEmpty(videoStream.BitRate) ? currentBitRate : videoStream.BitRate; - videoStream.Width = IsEmpty(videoStream.Width) ? currentWidth : videoStream.Width; - videoStream.Height = IsEmpty(videoStream.Height) ? currentHeight : videoStream.Height; + videoStream.BitRate = videoStream.BitRate.GetValueOrDefault() == 0 ? currentBitRate : videoStream.BitRate; + videoStream.Width = videoStream.Width.GetValueOrDefault() == 0 ? currentWidth : videoStream.Width; + videoStream.Height = videoStream.Height.GetValueOrDefault() == 0 ? currentHeight : videoStream.Height; } } - private bool IsEmpty(int? num) - { - return !num.HasValue || num.Value == 0; - } - /// <summary> /// Gets information about the longest playlist on a bdrom. /// </summary> /// <param name="path">The path.</param> /// <returns>VideoStream.</returns> - private BlurayDiscInfo GetBDInfo(string path) + private BlurayDiscInfo? GetBDInfo(string path) { ArgumentException.ThrowIfNullOrEmpty(path); @@ -527,32 +504,29 @@ namespace MediaBrowser.Providers.MediaInfo private void FetchPeople(Video video, Model.MediaInfo.MediaInfo data, MetadataRefreshOptions options) { - var replaceData = options.ReplaceAllMetadata; - - if (!video.IsLocked && !video.LockedFields.Contains(MetadataField.Cast)) + if (video.IsLocked + || video.LockedFields.Contains(MetadataField.Cast) + || data.People.Length == 0) { - if (replaceData || _libraryManager.GetPeople(video).Count == 0) - { - var people = new List<PersonInfo>(); - - foreach (var person in data.People) - { - PeopleHelper.AddPerson(people, new PersonInfo - { - Name = person.Name, - Type = person.Type, - Role = person.Role - }); - } - - _libraryManager.UpdatePeople(video, people); - } + return; } - } - private SubtitleOptions GetOptions() - { - return _config.GetConfiguration<SubtitleOptions>("subtitles"); + if (options.ReplaceAllMetadata || _libraryManager.GetPeople(video).Count == 0) + { + var people = new List<PersonInfo>(); + + foreach (var person in data.People) + { + PeopleHelper.AddPerson(people, new PersonInfo + { + Name = person.Name, + Type = person.Type, + Role = person.Role + }); + } + + _libraryManager.UpdatePeople(video, people); + } } /// <summary> @@ -575,7 +549,7 @@ namespace MediaBrowser.Providers.MediaInfo var enableSubtitleDownloading = options.MetadataRefreshMode == MetadataRefreshMode.Default || options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh; - var subtitleOptions = GetOptions(); + var subtitleOptions = _config.GetConfiguration<SubtitleOptions>("subtitles"); var libraryOptions = _libraryManager.GetLibraryOptions(video); @@ -659,9 +633,9 @@ namespace MediaBrowser.Providers.MediaInfo /// </summary> /// <param name="video">The video.</param> /// <returns>An array of dummy chapters.</returns> - private ChapterInfo[] CreateDummyChapters(Video video) + internal ChapterInfo[] CreateDummyChapters(Video video) { - var runtime = video.RunTimeTicks ?? 0; + var runtime = video.RunTimeTicks.GetValueOrDefault(); // Only process files with a runtime higher than 0 and lower than 12h. The latter are likely corrupted. if (runtime < 0 || runtime > TimeSpan.FromHours(12).Ticks) @@ -671,30 +645,30 @@ namespace MediaBrowser.Providers.MediaInfo CultureInfo.InvariantCulture, "{0} has an invalid runtime of {1} minutes", video.Name, - TimeSpan.FromTicks(runtime).Minutes)); + TimeSpan.FromTicks(runtime).TotalMinutes)); } long dummyChapterDuration = TimeSpan.FromSeconds(_config.Configuration.DummyChapterDuration).Ticks; - if (runtime > dummyChapterDuration) + if (runtime <= dummyChapterDuration) { - int chapterCount = (int)(runtime / dummyChapterDuration); - var chapters = new ChapterInfo[chapterCount]; - - long currentChapterTicks = 0; - for (int i = 0; i < chapterCount; i++) - { - chapters[i] = new ChapterInfo - { - StartPositionTicks = currentChapterTicks - }; - - currentChapterTicks += dummyChapterDuration; - } - - return chapters; + return Array.Empty<ChapterInfo>(); } - return Array.Empty<ChapterInfo>(); + int chapterCount = (int)(runtime / dummyChapterDuration); + var chapters = new ChapterInfo[chapterCount]; + + long currentChapterTicks = 0; + for (int i = 0; i < chapterCount; i++) + { + chapters[i] = new ChapterInfo + { + StartPositionTicks = currentChapterTicks + }; + + currentChapterTicks += dummyChapterDuration; + } + + return chapters; } } } diff --git a/src/Jellyfin.Drawing/ImageProcessor.cs b/src/Jellyfin.Drawing/ImageProcessor.cs index 4e5d3b4d55..44e06bb521 100644 --- a/src/Jellyfin.Drawing/ImageProcessor.cs +++ b/src/Jellyfin.Drawing/ImageProcessor.cs @@ -13,7 +13,6 @@ using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; @@ -437,8 +436,13 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable => (item.Path + image.DateModified.Ticks).GetMD5().ToString("N", CultureInfo.InvariantCulture); /// <inheritdoc /> - public string GetImageCacheTag(BaseItem item, ChapterInfo chapter) + public string? GetImageCacheTag(BaseItem item, ChapterInfo chapter) { + if (chapter.ImagePath is null) + { + return null; + } + return GetImageCacheTag(item, new ItemImageInfo { Path = chapter.ImagePath, diff --git a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj index c12f0cd685..1263043a51 100644 --- a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj +++ b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj @@ -7,6 +7,9 @@ </ItemGroup> <ItemGroup> + <PackageReference Include="AutoFixture" /> + <PackageReference Include="AutoFixture.AutoMoq" /> + <PackageReference Include="AutoFixture.Xunit2" /> <PackageReference Include="Microsoft.NET.Test.Sdk" /> <PackageReference Include="Moq" /> <PackageReference Include="xunit" /> diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/FFProbeVideoInfoTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/FFProbeVideoInfoTests.cs new file mode 100644 index 0000000000..76922af8d5 --- /dev/null +++ b/tests/Jellyfin.Providers.Tests/MediaInfo/FFProbeVideoInfoTests.cs @@ -0,0 +1,61 @@ +using System; +using AutoFixture; +using AutoFixture.AutoMoq; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Providers.MediaInfo; +using Moq; +using Xunit; + +namespace Jellyfin.Providers.Tests.MediaInfo; + +public class FFProbeVideoInfoTests +{ + private readonly FFProbeVideoInfo _fFProbeVideoInfo; + + public FFProbeVideoInfoTests() + { + var serverConfiguration = new ServerConfiguration() + { + DummyChapterDuration = (int)TimeSpan.FromMinutes(5).TotalSeconds + }; + var serverConfig = new Mock<IServerConfigurationManager>(); + serverConfig.Setup(c => c.Configuration) + .Returns(serverConfiguration); + + IFixture fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true }); + fixture.Inject(serverConfig); + _fFProbeVideoInfo = fixture.Create<FFProbeVideoInfo>(); + } + + [Theory] + [InlineData(-1L)] + [InlineData(long.MinValue)] + [InlineData(long.MaxValue)] + public void CreateDummyChapters_InvalidRuntime_ThrowsArgumentException(long? runtime) + { + Assert.Throws<ArgumentException>( + () => _fFProbeVideoInfo.CreateDummyChapters(new Video() + { + RunTimeTicks = runtime + })); + } + + [Theory] + [InlineData(null, 0)] + [InlineData(0L, 0)] + [InlineData(1L, 0)] + [InlineData(TimeSpan.TicksPerMinute * 5, 0)] + [InlineData((TimeSpan.TicksPerMinute * 5) + 1, 1)] + [InlineData(TimeSpan.TicksPerMinute * 50, 10)] + public void CreateDummyChapters_ValidRuntime_CorrectChaptersCount(long? runtime, int chaptersCount) + { + var chapters = _fFProbeVideoInfo.CreateDummyChapters(new Video() + { + RunTimeTicks = runtime + }); + + Assert.Equal(chaptersCount, chapters.Length); + } +} From cb48fe02c2cef7270bb071b90d565cf63744c32d Mon Sep 17 00:00:00 2001 From: cvium <clausvium@gmail.com> Date: Tue, 22 Aug 2023 20:12:16 +0200 Subject: [PATCH 523/858] remove nullable enable --- Emby.Server.Implementations/Data/SqliteExtensions.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs index 30e334393c..01b5fdaeea 100644 --- a/Emby.Server.Implementations/Data/SqliteExtensions.cs +++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs @@ -1,4 +1,3 @@ -#nullable enable #pragma warning disable CS1591 using System; From 18a311d32fd6e3cdfed466017bda46566a013106 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Tue, 22 Aug 2023 21:14:54 +0200 Subject: [PATCH 524/858] == null -> is null --- Emby.Server.Implementations/ApplicationHost.cs | 4 ++-- Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 2 +- Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs | 2 +- Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs | 2 +- Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs | 2 +- MediaBrowser.Controller/Entities/BaseItem.cs | 2 +- MediaBrowser.Controller/Entities/TV/Episode.cs | 2 +- MediaBrowser.Controller/Entities/Video.cs | 2 +- MediaBrowser.Controller/Library/ItemResolveArgs.cs | 2 +- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 2 +- MediaBrowser.Model/Dlna/StreamBuilder.cs | 4 ++-- MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs | 2 +- .../Json/Converters/JsonDelimitedArrayConverter.cs | 2 +- src/Jellyfin.Extensions/StreamExtensions.cs | 4 ++-- 14 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index dd90a89505..8b13ccadab 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1006,7 +1006,7 @@ namespace Emby.Server.Implementations if (ConfigurationManager.GetNetworkConfiguration().EnablePublishedServerUriByRequest) { int? requestPort = request.Host.Port; - if (requestPort == null + if (requestPort is null || (requestPort == 80 && string.Equals(request.Scheme, "http", StringComparison.OrdinalIgnoreCase)) || (requestPort == 443 && string.Equals(request.Scheme, "https", StringComparison.OrdinalIgnoreCase))) { @@ -1190,7 +1190,7 @@ namespace Emby.Server.Implementations } } - if (_sessionManager != null) + if (_sessionManager is not null) { // used for closing websockets foreach (var session in _sessionManager.Sessions) diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index 63667e7e69..fe602fba39 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -693,7 +693,7 @@ public class DynamicHlsHelper // Currently we only transcode to 8 bits AV1 int bitDepth = 8; if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec) - && state.VideoStream != null + && state.VideoStream is not null && state.VideoStream.BitDepth.HasValue) { bitDepth = state.VideoStream.BitDepth.Value; diff --git a/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs b/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs index a34fd01d5e..3e3604b2ad 100644 --- a/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs +++ b/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs @@ -77,7 +77,7 @@ public class CommaDelimitedArrayModelBinder : IModelBinder var typedValueIndex = 0; for (var i = 0; i < parsedValues.Length; i++) { - if (parsedValues[i] != null) + if (parsedValues[i] is not null) { typedValues.SetValue(parsedValues[i], typedValueIndex); typedValueIndex++; diff --git a/Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs b/Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs index cb9a829557..ae9f0a8cdb 100644 --- a/Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs +++ b/Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs @@ -77,7 +77,7 @@ public class PipeDelimitedArrayModelBinder : IModelBinder var typedValueIndex = 0; for (var i = 0; i < parsedValues.Length; i++) { - if (parsedValues[i] != null) + if (parsedValues[i] is not null) { typedValues.SetValue(parsedValues[i], typedValueIndex); typedValueIndex++; diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index e1dfa1d315..3271e08e48 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -276,7 +276,7 @@ namespace Jellyfin.Server.Extensions } else if (NetworkExtensions.TryParseToSubnet(allowedProxies[i], out var subnet)) { - if (subnet != null) + if (subnet is not null) { AddIPAddress(config, options, subnet.Prefix, subnet.PrefixLength); } diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 5018110035..9f3e8eec96 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1864,7 +1864,7 @@ namespace MediaBrowser.Controller.Entities /// <exception cref="ArgumentException">Backdrops should be accessed using Item.Backdrops.</exception> public bool HasImage(ImageType type, int imageIndex) { - return GetImageInfo(type, imageIndex) != null; + return GetImageInfo(type, imageIndex) is not null; } public void SetImage(ItemImageInfo image, int index) diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs index 597b4cecbc..bf31508c1d 100644 --- a/MediaBrowser.Controller/Entities/TV/Episode.cs +++ b/MediaBrowser.Controller/Entities/TV/Episode.cs @@ -99,7 +99,7 @@ namespace MediaBrowser.Controller.Entities.TV } [JsonIgnore] - public bool IsInSeasonFolder => FindParent<Season>() != null; + public bool IsInSeasonFolder => FindParent<Season>() is not null; [JsonIgnore] public string SeriesPresentationUniqueKey { get; set; } diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs index 5b7abea10a..9f685b7e2e 100644 --- a/MediaBrowser.Controller/Entities/Video.cs +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -333,7 +333,7 @@ namespace MediaBrowser.Controller.Entities protected override bool IsActiveRecording() { - return LiveTvManager.GetActiveRecordingInfo(Path) != null; + return LiveTvManager.GetActiveRecordingInfo(Path) is not null; } public override bool CanDelete() diff --git a/MediaBrowser.Controller/Library/ItemResolveArgs.cs b/MediaBrowser.Controller/Library/ItemResolveArgs.cs index c701021679..dcd0110fb5 100644 --- a/MediaBrowser.Controller/Library/ItemResolveArgs.cs +++ b/MediaBrowser.Controller/Library/ItemResolveArgs.cs @@ -217,7 +217,7 @@ namespace MediaBrowser.Controller.Library /// <returns><c>true</c> if [contains file system entry by name] [the specified name]; otherwise, <c>false</c>.</returns> public bool ContainsFileSystemEntryByName(string name) { - return GetFileSystemEntryByName(name) != null; + return GetFileSystemEntryByName(name) is not null; } public string GetCollectionType() diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index d61430b0bc..fd2b495919 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -2692,7 +2692,7 @@ namespace MediaBrowser.Controller.MediaEncoding string args = string.Empty; // http://ffmpeg.org/ffmpeg-all.html#toc-Complex-filtergraphs-1 - if (state.VideoStream != null && videoProcessFilters.Contains("-filter_complex", StringComparison.Ordinal)) + if (state.VideoStream is not null && videoProcessFilters.Contains("-filter_complex", StringComparison.Ordinal)) { int videoStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.VideoStream); diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index f6b882c3e6..adfe0da281 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -135,7 +135,7 @@ namespace MediaBrowser.Model.Dlna } } - if (transcodingProfile != null) + if (transcodingProfile is not null) { if (!item.SupportsTranscoding) { @@ -759,7 +759,7 @@ namespace MediaBrowser.Model.Dlna { // prefer direct copy profile float videoFramerate = videoStream?.AverageFrameRate ?? videoStream?.RealFrameRate ?? 0; - TransportStreamTimestamp? timestamp = videoStream == null ? TransportStreamTimestamp.None : item.Timestamp; + TransportStreamTimestamp? timestamp = videoStream is null ? TransportStreamTimestamp.None : item.Timestamp; int? numAudioStreams = item.GetStreamCount(MediaStreamType.Audio); int? numVideoStreams = item.GetStreamCount(MediaStreamType.Video); diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index 2ff3c75803..35ea04d218 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -128,7 +128,7 @@ namespace MediaBrowser.Providers.MediaInfo var m2ts = _mediaEncoder.GetPrimaryPlaylistM2tsFiles(item.Path); // Return if no playable .m2ts files are found - if (blurayDiscInfo == null || blurayDiscInfo.Files.Length == 0 || m2ts.Count == 0) + if (blurayDiscInfo is null || blurayDiscInfo.Files.Length == 0 || m2ts.Count == 0) { _logger.LogError("No playable .m2ts files found in Blu-ray structure, skipping FFprobe."); return ItemUpdateType.MetadataImport; diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs index 321cfa502a..17096c0170 100644 --- a/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs +++ b/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs @@ -59,7 +59,7 @@ namespace Jellyfin.Extensions.Json.Converters var typedValueIndex = 0; for (var i = 0; i < stringEntries.Length; i++) { - if (parsedValues[i] != null) + if (parsedValues[i] is not null) { typedValues.SetValue(parsedValues[i], typedValueIndex); typedValueIndex++; diff --git a/src/Jellyfin.Extensions/StreamExtensions.cs b/src/Jellyfin.Extensions/StreamExtensions.cs index 9751d9d42a..d76558ded4 100644 --- a/src/Jellyfin.Extensions/StreamExtensions.cs +++ b/src/Jellyfin.Extensions/StreamExtensions.cs @@ -40,7 +40,7 @@ namespace Jellyfin.Extensions public static IEnumerable<string> ReadAllLines(this TextReader reader) { string? line; - while ((line = reader.ReadLine()) != null) + while ((line = reader.ReadLine()) is not null) { yield return line; } @@ -54,7 +54,7 @@ namespace Jellyfin.Extensions public static async IAsyncEnumerable<string> ReadAllLinesAsync(this TextReader reader) { string? line; - while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) != null) + while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) is not null) { yield return line; } From a1a155168070f049f64ef33da4b8b29c18db4769 Mon Sep 17 00:00:00 2001 From: cvium <clausvium@gmail.com> Date: Wed, 23 Aug 2023 09:08:22 +0200 Subject: [PATCH 525/858] remove batteries init --- Jellyfin.Server/Helpers/StartupHelpers.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Jellyfin.Server/Helpers/StartupHelpers.cs b/Jellyfin.Server/Helpers/StartupHelpers.cs index fda6e54656..95b2504063 100644 --- a/Jellyfin.Server/Helpers/StartupHelpers.cs +++ b/Jellyfin.Server/Helpers/StartupHelpers.cs @@ -297,7 +297,5 @@ public static class StartupHelpers // Disable the "Expect: 100-Continue" header by default // http://stackoverflow.com/questions/566437/http-post-returns-the-error-417-expectation-failed-c ServicePointManager.Expect100Continue = false; - - Batteries_V2.Init(); } } From 7e4e715a90dd451985df036b7ac9724c24d616b3 Mon Sep 17 00:00:00 2001 From: cvium <clausvium@gmail.com> Date: Wed, 23 Aug 2023 09:10:48 +0200 Subject: [PATCH 526/858] remove unused using --- Jellyfin.Server/Helpers/StartupHelpers.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Jellyfin.Server/Helpers/StartupHelpers.cs b/Jellyfin.Server/Helpers/StartupHelpers.cs index 95b2504063..66d393decb 100644 --- a/Jellyfin.Server/Helpers/StartupHelpers.cs +++ b/Jellyfin.Server/Helpers/StartupHelpers.cs @@ -15,7 +15,6 @@ using MediaBrowser.Model.IO; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Serilog; -using SQLitePCL; using ILogger = Microsoft.Extensions.Logging.ILogger; namespace Jellyfin.Server.Helpers; From 56ea7c651ea245659af86b9684b5be540e8e3447 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 23 Aug 2023 11:23:44 +0200 Subject: [PATCH 527/858] chore(deps): update skiasharp monorepo (#10142) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 210d6b814c..c7c3561978 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -66,11 +66,11 @@ <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.0.2" /> <PackageVersion Include="SerilogAnalyzer" Version="0.15.0" /> <PackageVersion Include="SharpFuzz" Version="2.1.1" /> - <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.3" /> + <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.4" /> <PackageVersion Include="SkiaSharp.Svg" Version="1.60.0" /> - <PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.3" /> - <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.8.2.3" /> - <PackageVersion Include="SkiaSharp" Version="2.88.3" /> + <PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.4" /> + <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.8.2.4" /> + <PackageVersion Include="SkiaSharp" Version="2.88.4" /> <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> <PackageVersion Include="SQLitePCL.pretty.netstandard" Version="3.1.0" /> <PackageVersion Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.5" /> From 91109b7a4a460048c954167c48369a0a025fbb3a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 23 Aug 2023 11:25:17 +0200 Subject: [PATCH 528/858] chore(deps): update dependency serilog.settings.configuration to v7.0.1 (#10143) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index c7c3561978..92b28537d2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -59,7 +59,7 @@ <PackageVersion Include="prometheus-net" Version="8.0.1" /> <PackageVersion Include="Serilog.AspNetCore" Version="7.0.0" /> <PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" /> - <PackageVersion Include="Serilog.Settings.Configuration" Version="7.0.0" /> + <PackageVersion Include="Serilog.Settings.Configuration" Version="7.0.1" /> <PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" /> <PackageVersion Include="Serilog.Sinks.Console" Version="4.1.0" /> <PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" /> From 9a246166b0bddaeacc98c16072a7a714322504f0 Mon Sep 17 00:00:00 2001 From: cvium <clausvium@gmail.com> Date: Wed, 23 Aug 2023 12:15:21 +0200 Subject: [PATCH 529/858] move a computation out of transaction and skip season updates if name matches --- Emby.Server.Implementations/Data/SqliteItemRepository.cs | 3 ++- MediaBrowser.Providers/TV/SeriesMetadataService.cs | 9 ++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index ca121f8a20..40657a0a51 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -564,11 +564,12 @@ namespace Emby.Server.Implementations.Data CheckDisposed(); + var images = SerializeImages(item.ImageInfos); using var connection = GetConnection(); using var transaction = connection.BeginTransaction(); using var saveImagesStatement = PrepareStatement(connection, "Update TypedBaseItems set Images=@Images where guid=@Id"); saveImagesStatement.TryBind("@Id", item.Id); - saveImagesStatement.TryBind("@Images", SerializeImages(item.ImageInfos)); + saveImagesStatement.TryBind("@Images", images); saveImagesStatement.ExecuteNonQuery(); transaction.Commit(); diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs index 9016e5de0c..a4e2a3333b 100644 --- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs +++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs @@ -1,5 +1,6 @@ #pragma warning disable CS1591 +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -227,7 +228,13 @@ namespace MediaBrowser.Providers.TV } else { - existingSeason.Name = GetValidSeasonNameForSeries(series, seasonName, seasonNumber); + var name = GetValidSeasonNameForSeries(series, seasonName, seasonNumber); + if (string.Equals(existingSeason.Name, name, StringComparison.Ordinal)) + { + continue; + } + + existingSeason.Name = name; await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false); } } From 7689990ad116a4438a4f7b23e47e2b935abeafbc Mon Sep 17 00:00:00 2001 From: cvium <clausvium@gmail.com> Date: Wed, 23 Aug 2023 12:22:35 +0200 Subject: [PATCH 530/858] reduce calls to GetValidSeasonNameForSeries --- MediaBrowser.Providers/TV/SeriesMetadataService.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs index a4e2a3333b..1bbef16608 100644 --- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs +++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs @@ -214,11 +214,10 @@ namespace MediaBrowser.Providers.TV { // Null season numbers will have a 'dummy' season created because seasons are always required. var existingSeason = seasons.FirstOrDefault(i => i.IndexNumber == seasonNumber); - string? seasonName = null; - if (seasonNumber.HasValue && seasonNames.TryGetValue(seasonNumber.Value, out var tmp)) + if (!seasonNumber.HasValue || !seasonNames.TryGetValue(seasonNumber.Value, out var seasonName)) { - seasonName = tmp; + seasonName = GetValidSeasonNameForSeries(series, null, seasonNumber); } if (existingSeason is null) @@ -228,13 +227,12 @@ namespace MediaBrowser.Providers.TV } else { - var name = GetValidSeasonNameForSeries(series, seasonName, seasonNumber); - if (string.Equals(existingSeason.Name, name, StringComparison.Ordinal)) + if (string.Equals(existingSeason.Name, seasonName, StringComparison.Ordinal)) { continue; } - existingSeason.Name = name; + existingSeason.Name = seasonName; await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false); } } @@ -254,7 +252,6 @@ namespace MediaBrowser.Providers.TV int? seasonNumber, CancellationToken cancellationToken) { - seasonName = GetValidSeasonNameForSeries(series, seasonName, seasonNumber); Logger.LogInformation("Creating Season {SeasonName} entry for {SeriesName}", seasonName, series.Name); var season = new Season From c76026600e26f626d05897b11baa5c19222f7a20 Mon Sep 17 00:00:00 2001 From: cvium <clausvium@gmail.com> Date: Wed, 23 Aug 2023 13:36:08 +0200 Subject: [PATCH 531/858] simplify if --- MediaBrowser.Providers/TV/SeriesMetadataService.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs index 1bbef16608..e01c0f4830 100644 --- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs +++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs @@ -225,13 +225,8 @@ namespace MediaBrowser.Providers.TV var season = await CreateSeasonAsync(series, seasonName, seasonNumber, cancellationToken).ConfigureAwait(false); series.AddChild(season); } - else + else if (!string.Equals(existingSeason.Name, seasonName, StringComparison.Ordinal)) { - if (string.Equals(existingSeason.Name, seasonName, StringComparison.Ordinal)) - { - continue; - } - existingSeason.Name = seasonName; await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false); } From 75a151fea78af162f9609da6bfb0ed94694d8228 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 23 Aug 2023 21:24:02 +0200 Subject: [PATCH 532/858] chore(deps): update skiasharp monorepo (#10146) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 92b28537d2..df11d5b71e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -66,11 +66,11 @@ <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.0.2" /> <PackageVersion Include="SerilogAnalyzer" Version="0.15.0" /> <PackageVersion Include="SharpFuzz" Version="2.1.1" /> - <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.4" /> + <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.5" /> <PackageVersion Include="SkiaSharp.Svg" Version="1.60.0" /> - <PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.4" /> - <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.8.2.4" /> - <PackageVersion Include="SkiaSharp" Version="2.88.4" /> + <PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.5" /> + <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.8.2.5" /> + <PackageVersion Include="SkiaSharp" Version="2.88.5" /> <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> <PackageVersion Include="SQLitePCL.pretty.netstandard" Version="3.1.0" /> <PackageVersion Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.5" /> From a1f527be007685332f35ff3d02420e5e32ef5bd4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 24 Aug 2023 14:15:05 +0200 Subject: [PATCH 533/858] chore(deps): update dependency sqlitepclraw.bundle_e_sqlite3 to v2.1.6 (#10147) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index df11d5b71e..dde15cf7d3 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -73,7 +73,7 @@ <PackageVersion Include="SkiaSharp" Version="2.88.5" /> <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> <PackageVersion Include="SQLitePCL.pretty.netstandard" Version="3.1.0" /> - <PackageVersion Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.5" /> + <PackageVersion Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.6" /> <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.507" /> <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.4.0" /> <PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" /> From b40f5d0b7d9bb7359214431c1dcb2202902ef2ad Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 24 Aug 2023 18:35:57 +0200 Subject: [PATCH 534/858] chore(deps): update actions/checkout action to v3.6.0 (#10149) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/commands.yml | 4 ++-- .github/workflows/openapi.yml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 1f81a332d5..72381fd3d5 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - name: Setup .NET uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0 with: diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index 178959afc9..3b7f7b85b4 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -24,7 +24,7 @@ jobs: reactions: '+1' - name: Checkout the latest code - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: token: ${{ secrets.JF_BOT_TOKEN }} fetch-depth: 0 @@ -51,7 +51,7 @@ jobs: reactions: eyes - name: Checkout the latest code - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: token: ${{ secrets.JF_BOT_TOKEN }} fetch-depth: 0 diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index d3dfd0a6aa..ee64a522e7 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -14,7 +14,7 @@ jobs: permissions: read-all steps: - name: Checkout repository - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} @@ -39,7 +39,7 @@ jobs: permissions: read-all steps: - name: Checkout repository - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} From 4fa7672d75bbc15a8581f1e5370cc1fe190be9f5 Mon Sep 17 00:00:00 2001 From: cvium <clausvium@gmail.com> Date: Fri, 25 Aug 2023 19:49:01 +0200 Subject: [PATCH 535/858] fix todos and add graylog back --- Emby.Server.Implementations/Data/SqliteItemRepository.cs | 9 ++------- Jellyfin.Server/Jellyfin.Server.csproj | 1 + 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 40657a0a51..6c342495ba 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -624,7 +624,8 @@ namespace Emby.Server.Implementations.Data { if (requiresReset) { - // TODO saveItemStatement.Parameters.Clear(); + saveItemStatement.Parameters.Clear(); + deleteAncestorsStatement.Parameters.Clear(); } var item = tuple.Item; @@ -2037,7 +2038,6 @@ namespace Emby.Server.Implementations.Data chapterIndex++; } - // TODO statement.Parameters.Clear(); statement.ExecuteNonQuery(); } @@ -4793,7 +4793,6 @@ AND Type = @InternalPersonType)"); CheckDisposed(); // First delete - // TODO deleteAncestorsStatement.Parameters.Clear(); deleteAncestorsStatement.TryBind("@ItemId", itemId); deleteAncestorsStatement.ExecuteNonQuery(); @@ -4829,7 +4828,6 @@ AND Type = @InternalPersonType)"); statement.TryBind("@AncestorIdText" + index, ancestorId.ToString("N", CultureInfo.InvariantCulture)); } - // TODO statement.Parameters.Clear(); statement.ExecuteNonQuery(); } } @@ -5363,7 +5361,6 @@ AND Type = @InternalPersonType)"); statement.TryBind("@CleanValue" + index, GetCleanValue(itemValue)); } - // TODO statement.Parameters.Clear(); statement.ExecuteNonQuery(); } @@ -5642,7 +5639,6 @@ AND Type = @InternalPersonType)"); statement.TryBind("@IsHearingImpaired" + index, stream.IsHearingImpaired); } - // TODO statement.Parameters.Clear(); statement.ExecuteNonQuery(); } @@ -5979,7 +5975,6 @@ AND Type = @InternalPersonType)"); statement.TryBind("@MIMEType" + index, attachment.MimeType); } - // TODO statement.Parameters.Clear(); statement.ExecuteNonQuery(); } diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 60d9849a32..62abb89355 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -47,6 +47,7 @@ <PackageReference Include="Serilog.Sinks.Async" /> <PackageReference Include="Serilog.Sinks.Console" /> <PackageReference Include="Serilog.Sinks.File" /> + <PackageReference Include="Serilog.Sinks.Graylog" /> </ItemGroup> <ItemGroup> From 0ed2aa6f0670477bac28562d93c085bd14def9af Mon Sep 17 00:00:00 2001 From: Stepan Goremykin <goremukin@gmail.com> Date: Sat, 26 Aug 2023 16:57:27 +0200 Subject: [PATCH 536/858] Fix a few multiple enumerations --- Emby.Server.Implementations/Library/LibraryManager.cs | 2 +- MediaBrowser.Providers/Manager/ProviderManager.cs | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 8f88113b7f..808cedd678 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -358,7 +358,7 @@ namespace Emby.Server.Implementations.Library var children = item.IsFolder ? ((Folder)item).GetRecursiveChildren(false) - : Enumerable.Empty<BaseItem>(); + : Array.Empty<BaseItem>(); foreach (var metadataPath in GetMetadataPaths(item, children)) { diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 5cb28402e8..121be88d13 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -807,13 +807,11 @@ namespace MediaBrowser.Providers.Manager where TLookupType : ItemLookupInfo { var results = await provider.GetSearchResults(searchInfo, cancellationToken).ConfigureAwait(false); - - foreach (var item in results) + return results.Select(result => { - item.SearchProviderName = provider.Name; - } - - return results; + result.SearchProviderName = provider.Name; + return result; + }); } private IEnumerable<IExternalId> GetExternalIds(IHasProviderIds item) From ee83e4cca56d086476b5cb6f92d931fb9bbb677f Mon Sep 17 00:00:00 2001 From: Stepan Goremykin <goremukin@gmail.com> Date: Sat, 26 Aug 2023 17:29:00 +0200 Subject: [PATCH 537/858] Remove redundant method --- .../Manager/ProviderManager.cs | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 121be88d13..f3211ba450 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -765,10 +765,12 @@ namespace MediaBrowser.Providers.Manager { try { - var results = await GetSearchResults(provider, searchInfo.SearchInfo, cancellationToken).ConfigureAwait(false); + var results = await provider.GetSearchResults(searchInfo.SearchInfo, cancellationToken).ConfigureAwait(false); foreach (var result in results) { + result.SearchProviderName = provider.Name; + var existingMatch = resultList.FirstOrDefault(i => i.ProviderIds.Any(p => string.Equals(result.GetProviderId(p.Key), p.Value, StringComparison.OrdinalIgnoreCase))); if (existingMatch is null) @@ -800,20 +802,6 @@ namespace MediaBrowser.Providers.Manager return resultList; } - private async Task<IEnumerable<RemoteSearchResult>> GetSearchResults<TLookupType>( - IRemoteSearchProvider<TLookupType> provider, - TLookupType searchInfo, - CancellationToken cancellationToken) - where TLookupType : ItemLookupInfo - { - var results = await provider.GetSearchResults(searchInfo, cancellationToken).ConfigureAwait(false); - return results.Select(result => - { - result.SearchProviderName = provider.Name; - return result; - }); - } - private IEnumerable<IExternalId> GetExternalIds(IHasProviderIds item) { return _externalIds.Where(i => From 1f083c0de2a1be669c2af7e77cd4e7313ee8f449 Mon Sep 17 00:00:00 2001 From: NickSkier <nikita.vasiliev.02@gmail.com> Date: Sat, 26 Aug 2023 09:11:12 +0000 Subject: [PATCH 538/858] Translated using Weblate (Russian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ru/ --- Emby.Server.Implementations/Localization/Core/ru.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json index 421513341a..fa6c753b60 100644 --- a/Emby.Server.Implementations/Localization/Core/ru.json +++ b/Emby.Server.Implementations/Localization/Core/ru.json @@ -31,13 +31,13 @@ "ItemRemovedWithName": "{0} - изъято из медиатеки", "LabelIpAddressValue": "IP-адрес: {0}", "LabelRunningTimeValue": "Длительность: {0}", - "Latest": "Новое", + "Latest": "Последние добавленные", "MessageApplicationUpdated": "Jellyfin Server был обновлён", "MessageApplicationUpdatedTo": "Jellyfin Server был обновлён до {0}", "MessageNamedServerConfigurationUpdatedWithValue": "Конфигурация сервера (раздел {0}) была обновлена", "MessageServerConfigurationUpdated": "Конфигурация сервера была обновлена", "MixedContent": "Смешанное содержание", - "Movies": "Кино", + "Movies": "Фильмы", "Music": "Музыка", "MusicVideos": "Муз. видео", "NameInstallFailed": "Установка {0} неудачна", @@ -77,7 +77,7 @@ "SubtitleDownloadFailureFromForItem": "Субтитры к {1} не удалось загрузить с {0}", "Sync": "Синхронизация", "System": "Система", - "TvShows": "ТВ", + "TvShows": "Телесериалы", "User": "Пользователь", "UserCreatedWithName": "Пользователь {0} был создан", "UserDeletedWithName": "Пользователь {0} был удалён", From 97d92e70875ebcb68ff76bdcd8c96e50cdd25947 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Sun, 27 Aug 2023 17:27:03 +0200 Subject: [PATCH 539/858] Use the correct trancode path EncodingOptions.TranscodingTempPath can be empty (and is by default), the correct way to get the trancode path is EncodingConfigurationExtensions.GetTranscodePath which falls back to $CACHEPATH/transcodes when EncodingOptions.TranscodingTempPath is null or empty. --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index fd2b495919..1905ffb1cd 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -37,6 +37,7 @@ namespace MediaBrowser.Controller.MediaEncoding private readonly IMediaEncoder _mediaEncoder; private readonly ISubtitleEncoder _subtitleEncoder; private readonly IConfiguration _config; + private readonly IConfigurationManager _configurationManager; // i915 hang was fixed by linux 6.2 (3f882f2) private readonly Version _minKerneli915Hang = new Version(5, 18); @@ -112,12 +113,14 @@ namespace MediaBrowser.Controller.MediaEncoding IApplicationPaths appPaths, IMediaEncoder mediaEncoder, ISubtitleEncoder subtitleEncoder, - IConfiguration config) + IConfiguration config, + IConfigurationManager configurationManager) { _appPaths = appPaths; _mediaEncoder = mediaEncoder; _subtitleEncoder = subtitleEncoder; _config = config; + _configurationManager = configurationManager; } [GeneratedRegex(@"\s+")] @@ -1058,7 +1061,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (state.MediaSource.VideoType == VideoType.Dvd || state.MediaSource.VideoType == VideoType.BluRay) { - var tmpConcatPath = Path.Join(options.TranscodingTempPath, state.MediaSource.Id + ".concat"); + var tmpConcatPath = Path.Join(_configurationManager.GetTranscodePath(), state.MediaSource.Id + ".concat"); _mediaEncoder.GenerateConcatConfig(state.MediaSource, tmpConcatPath); arg.Append(" -f concat -safe 0 -i ") .Append(tmpConcatPath); From c8ba70fbbc206da96b10a5a91d1c4bce7d08e46e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 28 Aug 2023 21:33:35 -0600 Subject: [PATCH 540/858] chore(deps): update github/codeql-action action to v2.21.5 (#10163) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 72381fd3d5..86d9ed0e10 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@a09933a12a80f87b87005513f0abb1494c27a716 # v2.21.4 + uses: github/codeql-action/init@00e563ead9f72a8461b24876bee2d0c2e8bd2ee8 # v2.21.5 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@a09933a12a80f87b87005513f0abb1494c27a716 # v2.21.4 + uses: github/codeql-action/autobuild@00e563ead9f72a8461b24876bee2d0c2e8bd2ee8 # v2.21.5 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@a09933a12a80f87b87005513f0abb1494c27a716 # v2.21.4 + uses: github/codeql-action/analyze@00e563ead9f72a8461b24876bee2d0c2e8bd2ee8 # v2.21.5 From 1aa0ce961b21419333ea5195cf53b1646344c44f Mon Sep 17 00:00:00 2001 From: Daniel Martin Gonzalez <danimart1991@outlook.com> Date: Tue, 29 Aug 2023 11:22:55 +0200 Subject: [PATCH 541/858] Add new Spain content rating ERI Add the content rating ERI to Spain list used in some providers and channels as Specially Recommended for Toddlers. --- Emby.Server.Implementations/Localization/Ratings/es.csv | 1 + 1 file changed, 1 insertion(+) diff --git a/Emby.Server.Implementations/Localization/Ratings/es.csv b/Emby.Server.Implementations/Localization/Ratings/es.csv index 0bc1d3f7d0..619e948d88 100644 --- a/Emby.Server.Implementations/Localization/Ratings/es.csv +++ b/Emby.Server.Implementations/Localization/Ratings/es.csv @@ -3,6 +3,7 @@ A/fig,0 A/i,0 A/fig/i,0 APTA,0 +ERI,0 TP,0 0+,0 6+,6 From 0e6f95591224c00752be194cf80826d4556813ac Mon Sep 17 00:00:00 2001 From: Gandihar <gandihar@gmail.com> Date: Tue, 29 Aug 2023 17:10:41 -0600 Subject: [PATCH 542/858] Add a small Bash script to launch Jellyfin, instead of a symlink. - The symlink causes a problem with SELinux because it understands symlinks. - This shell script automatically gets the correct SELinux context. --- fedora/jellyfin-selinux-launcher.sh | 3 +++ fedora/jellyfin.spec | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 fedora/jellyfin-selinux-launcher.sh diff --git a/fedora/jellyfin-selinux-launcher.sh b/fedora/jellyfin-selinux-launcher.sh new file mode 100644 index 0000000000..a735a238ff --- /dev/null +++ b/fedora/jellyfin-selinux-launcher.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +exec /usr/lib64/jellyfin/jellyfin ${@} diff --git a/fedora/jellyfin.spec b/fedora/jellyfin.spec index a759b29b13..d9d3d48694 100644 --- a/fedora/jellyfin.spec +++ b/fedora/jellyfin.spec @@ -14,6 +14,7 @@ License: GPLv2 URL: https://jellyfin.org # Jellyfin Server tarball created by `make -f .copr/Makefile srpm`, real URL ends with `v%%{version}.tar.gz` Source0: jellyfin-server-%{version}.tar.gz +Source10: jellyfin-selinux-launcher.sh Source11: jellyfin.service Source12: jellyfin.env Source13: jellyfin.override.conf @@ -73,7 +74,7 @@ dotnet publish --configuration Release --self-contained --runtime %{dotnet_runti # Jellyfin files %{__mkdir} -p %{buildroot}%{_libdir}/jellyfin %{buildroot}%{_bindir} %{__cp} -r Jellyfin.Server/bin/Release/net7.0/%{dotnet_runtime}/publish/* %{buildroot}%{_libdir}/jellyfin -ln -srf %{_libdir}/jellyfin/jellyfin %{buildroot}%{_bindir}/jellyfin +%{__install} -D %{SOURCE10} %{buildroot}%{_bindir}/jellyfin # Jellyfin config %{__install} -D Jellyfin.Server/Resources/Configuration/logging.json %{buildroot}%{_sysconfdir}/jellyfin/logging.json @@ -106,6 +107,7 @@ ln -srf %{_libdir}/jellyfin/jellyfin %{buildroot}%{_bindir}/jellyfin %attr(755,root,root) %{_libdir}/jellyfin/createdump %attr(755,root,root) %{_libdir}/jellyfin/jellyfin %{_libdir}/jellyfin/* +%attr(755,root,root) %{_bindir}/jellyfin # Jellyfin config %config(noreplace) %attr(644,jellyfin,jellyfin) %{_sysconfdir}/jellyfin/logging.json From c74d3e62d0989eaaa0f19f383fd153746348a234 Mon Sep 17 00:00:00 2001 From: Nyanmisaka <nst799610810@gmail.com> Date: Thu, 31 Aug 2023 07:18:18 +0800 Subject: [PATCH 543/858] Fix the issue that audio bsf only takes effect for remuxing (#10172) Signed-off-by: nyanmisaka <nst799610810@gmail.com> --- Jellyfin.Api/Controllers/DynamicHlsController.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index ce684e457c..5ba0119389 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1703,19 +1703,18 @@ public class DynamicHlsController : BaseJellyfinApiController } var audioCodec = _encodingHelper.GetAudioEncoder(state); + var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container); if (!state.IsOutputVideo) { if (EncodingHelper.IsCopyCodec(audioCodec)) { - var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container); - return "-acodec copy -strict -2" + bitStreamArgs; } var audioTranscodeParams = string.Empty; - audioTranscodeParams += "-acodec " + audioCodec; + audioTranscodeParams += "-acodec " + audioCodec + bitStreamArgs; var audioBitrate = state.OutputAudioBitrate; var audioChannels = state.OutputAudioChannels; @@ -1761,7 +1760,6 @@ public class DynamicHlsController : BaseJellyfinApiController if (EncodingHelper.IsCopyCodec(audioCodec)) { var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions); - var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container); var copyArgs = "-codec:a:0 copy" + bitStreamArgs + strictArgs; if (EncodingHelper.IsCopyCodec(videoCodec) && state.EnableBreakOnNonKeyFrames(videoCodec)) @@ -1772,7 +1770,7 @@ public class DynamicHlsController : BaseJellyfinApiController return copyArgs; } - var args = "-codec:a:0 " + audioCodec + strictArgs; + var args = "-codec:a:0 " + audioCodec + bitStreamArgs + strictArgs; var channels = state.OutputAudioChannels; From debbfaa502cae91212f2386d237616ad2a671a88 Mon Sep 17 00:00:00 2001 From: Nyanmisaka <nst799610810@gmail.com> Date: Thu, 31 Aug 2023 07:19:52 +0800 Subject: [PATCH 544/858] Fix MJPEG video is recognized as embedded image (#10173) fixes 1d729b2 Signed-off-by: nyanmisaka <nst799610810@gmail.com> --- .../Probing/ProbeResultNormalizer.cs | 8 +++++--- MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs | 4 +++- .../MediaInfo/EmbeddedImageProviderTests.cs | 4 +++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index aeb08cea35..3055e6cde3 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -762,9 +762,11 @@ namespace MediaBrowser.MediaEncoding.Probing && !string.Equals(streamInfo.FieldOrder, "progressive", StringComparison.OrdinalIgnoreCase); if (isAudio - || string.Equals(stream.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase) - || string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase) - || string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase)) + && (string.Equals(stream.Codec, "bmp", StringComparison.OrdinalIgnoreCase) + || string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase) + || string.Equals(stream.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase) + || string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase) + || string.Equals(stream.Codec, "webp", StringComparison.OrdinalIgnoreCase))) { stream.Type = MediaStreamType.EmbeddedImage; } diff --git a/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs index f58f5f7a33..c24f4e2fcc 100644 --- a/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs @@ -177,9 +177,11 @@ namespace MediaBrowser.Providers.MediaInfo var format = imageStream.Codec switch { + "bmp" => ImageFormat.Bmp, + "gif" => ImageFormat.Gif, "mjpeg" => ImageFormat.Jpg, "png" => ImageFormat.Png, - "gif" => ImageFormat.Gif, + "webp" => ImageFormat.Webp, _ => ImageFormat.Jpg }; diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs index 6b2d9021c9..2bc686a337 100644 --- a/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs +++ b/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs @@ -98,9 +98,11 @@ namespace Jellyfin.Providers.Tests.MediaInfo [InlineData(null, null, 1, ImageType.Primary, ImageFormat.Jpg)] // no label, finds primary [InlineData("backdrop", null, 2, ImageType.Backdrop, ImageFormat.Jpg)] // uses label to find index 2, not just pulling first stream [InlineData("cover", null, 2, ImageType.Primary, ImageFormat.Jpg)] // uses label to find index 2, not just pulling first stream + [InlineData(null, "bmp", 1, ImageType.Primary, ImageFormat.Bmp)] + [InlineData(null, "gif", 1, ImageType.Primary, ImageFormat.Gif)] [InlineData(null, "mjpeg", 1, ImageType.Primary, ImageFormat.Jpg)] [InlineData(null, "png", 1, ImageType.Primary, ImageFormat.Png)] - [InlineData(null, "gif", 1, ImageType.Primary, ImageFormat.Gif)] + [InlineData(null, "webp", 1, ImageType.Primary, ImageFormat.Webp)] public async void GetImage_Embedded_ReturnsCorrectSelection(string label, string? codec, int targetIndex, ImageType type, ImageFormat? expectedFormat) { var streams = new List<MediaStream>(); From da6a3c2bd32bbd46d26da3f45f3c047844007043 Mon Sep 17 00:00:00 2001 From: queeup <queeup@zoho.com> Date: Thu, 31 Aug 2023 11:23:00 +0000 Subject: [PATCH 545/858] Translated using Weblate (Turkish) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/tr/ --- .../Localization/Core/tr.json | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json index 9a140f8712..3ce928859a 100644 --- a/Emby.Server.Implementations/Localization/Core/tr.json +++ b/Emby.Server.Implementations/Localization/Core/tr.json @@ -3,19 +3,19 @@ "AppDeviceValues": "Uygulama: {0}, Aygıt: {1}", "Application": "Uygulama", "Artists": "Sanatçılar", - "AuthenticationSucceededWithUserName": "{0} kimlik başarıyla doğrulandı", + "AuthenticationSucceededWithUserName": "{0} kimliği başarıyla doğrulandı", "Books": "Kitaplar", "CameraImageUploadedFrom": "{0} 'den yeni bir kamera resmi yüklendi", "Channels": "Kanallar", - "ChapterNameValue": "Bölüm {0}", + "ChapterNameValue": "{0}. Bölüm", "Collections": "Koleksiyonlar", "DeviceOfflineWithName": "{0} bağlantısı kesildi", "DeviceOnlineWithName": "{0} bağlı", - "FailedLoginAttemptWithUserName": "{0} adresinden giriş denemesi başarısız oldu", + "FailedLoginAttemptWithUserName": "{0} kullanıcısının giriş denemesi başarısız oldu", "Favorites": "Favoriler", "Folders": "Klasörler", "Genres": "Türler", - "HeaderAlbumArtists": "Albüm Sanatçıları", + "HeaderAlbumArtists": "Albüm sanatçıları", "HeaderContinueWatching": "İzlemeye Devam Et", "HeaderFavoriteAlbums": "Favori Albümler", "HeaderFavoriteArtists": "Favori Sanatçılar", @@ -25,7 +25,7 @@ "HeaderLiveTV": "Canlı TV", "HeaderNextUp": "Gelecek Hafta", "HeaderRecordingGroups": "Kayıt Grupları", - "HomeVideos": "Ana sayfa videoları", + "HomeVideos": "Ana Sayfa Videoları", "Inherit": "Devral", "ItemAddedWithName": "{0} kütüphaneye eklendi", "ItemRemovedWithName": "{0} kütüphaneden silindi", @@ -34,14 +34,14 @@ "Latest": "En son", "MessageApplicationUpdated": "Jellyfin Sunucusu güncellendi", "MessageApplicationUpdatedTo": "Jellyfin Sunucusu {0} sürümüne güncellendi", - "MessageNamedServerConfigurationUpdatedWithValue": "Sunucu ayar kısmı {0} güncellendi", - "MessageServerConfigurationUpdated": "Sunucu ayarları güncellendi", + "MessageNamedServerConfigurationUpdatedWithValue": "Sunucu yapılandırma bölümü {0} güncellendi", + "MessageServerConfigurationUpdated": "Sunucu yapılandırması güncellendi", "MixedContent": "Karışık içerik", "Movies": "Filmler", "Music": "Müzik", - "MusicVideos": "Müzik videoları", + "MusicVideos": "Müzik Videoları", "NameInstallFailed": "{0} kurulumu başarısız", - "NameSeasonNumber": "Sezon {0}", + "NameSeasonNumber": "{0}. Sezon", "NameSeasonUnknown": "Bilinmeyen Sezon", "NewVersionIsAvailable": "Jellyfin Sunucusunun yeni bir sürümü indirmek için hazır.", "NotificationOptionApplicationUpdateAvailable": "Uygulama güncellemesi mevcut", @@ -55,9 +55,9 @@ "NotificationOptionPluginInstalled": "Eklenti yüklendi", "NotificationOptionPluginUninstalled": "Eklenti kaldırıldı", "NotificationOptionPluginUpdateInstalled": "Eklenti güncellemesi yüklendi", - "NotificationOptionServerRestartRequired": "Sunucu yeniden başlatma gerekli", + "NotificationOptionServerRestartRequired": "Sunucunun yeniden başlatılması gerekiyor", "NotificationOptionTaskFailed": "Zamanlanmış görev hatası", - "NotificationOptionUserLockedOut": "Kullanıcı kitlendi", + "NotificationOptionUserLockedOut": "Kullanıcı kilitlendi", "NotificationOptionVideoPlayback": "Video oynatma başladı", "NotificationOptionVideoPlaybackStopped": "Video oynatma durduruldu", "Photos": "Fotoğraflar", @@ -74,36 +74,36 @@ "Songs": "Şarkılar", "StartupEmbyServerIsLoading": "Jellyfin Sunucusu yükleniyor. Lütfen kısa süre sonra tekrar deneyin.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", - "SubtitleDownloadFailureFromForItem": "{1} için alt yazılar {0} 'dan indirilemedi", + "SubtitleDownloadFailureFromForItem": "{1} için alt yazılar {0} sağlayıcısından indirilemedi", "Sync": "Eşzamanlama", "System": "Sistem", "TvShows": "Diziler", "User": "Kullanıcı", "UserCreatedWithName": "{0} kullanıcısı oluşturuldu", - "UserDeletedWithName": "Kullanıcı {0} silindi", - "UserDownloadingItemWithValues": "{0} indiriliyor {1}", - "UserLockedOutWithName": "Kullanıcı {0} kitlendi", - "UserOfflineFromDevice": "{0}, {1} ile bağlantısı kesildi", - "UserOnlineFromDevice": "{0}, {1} çevrimiçi", - "UserPasswordChangedWithName": "{0} kullanıcısı için şifre değiştirildi", - "UserPolicyUpdatedWithName": "Kullanıcı politikası {0} için güncellendi", + "UserDeletedWithName": "{0} kullanıcısı silindi", + "UserDownloadingItemWithValues": "{0} {1} medyasını indiriyor", + "UserLockedOutWithName": "{0} adlı kullanıcı kilitlendi", + "UserOfflineFromDevice": "{0} kullanıcısının {1} ile bağlantısı kesildi", + "UserOnlineFromDevice": "{0} kullanıcısı {1} ile çevrimiçi", + "UserPasswordChangedWithName": "{0} kullanıcısının parolası değiştirildi", + "UserPolicyUpdatedWithName": "{0} için kullanıcı politikası güncellendi", "UserStartedPlayingItemWithValues": "{0}, {2} cihazında {1} izliyor", "UserStoppedPlayingItemWithValues": "{0}, {2} cihazında {1} izlemeyi bitirdi", "ValueHasBeenAddedToLibrary": "Medya kütüphanenize {0} eklendi", "ValueSpecialEpisodeName": "Özel - {0}", "VersionNumber": "Sürüm {0}", - "TaskCleanCache": "Geçici dosya klasörünü temizle", - "TasksChannelsCategory": "İnternet kanalları", + "TaskCleanCache": "Geçici Dosya Klasörünü Temizle", + "TasksChannelsCategory": "İnternet Kanalları", "TasksApplicationCategory": "Uygulama", "TasksLibraryCategory": "Kütüphane", "TasksMaintenanceCategory": "Bakım", "TaskRefreshPeopleDescription": "Medya kütüphanenizdeki videoların oyuncu ve yönetmen bilgilerini günceller.", - "TaskDownloadMissingSubtitlesDescription": "Metadata ayarlarını baz alarak eksik altyazıları internette arar.", + "TaskDownloadMissingSubtitlesDescription": "Meta veri yapılandırmasına dayalı olarak eksik altyazılar için internette arama yapar.", "TaskDownloadMissingSubtitles": "Eksik altyazıları indir", "TaskRefreshChannelsDescription": "Internet kanal bilgilerini yenile.", "TaskRefreshChannels": "Kanalları Yenile", - "TaskCleanTranscodeDescription": "Bir günden daha eski dönüştürme dosyalarını siler.", - "TaskCleanTranscode": "Dönüşüm Dizinini Temizle", + "TaskCleanTranscodeDescription": "Bir günden daha eski kod dönüştürme dosyalarını siler.", + "TaskCleanTranscode": "Kod Dönüştürme Dizinini Temizle", "TaskUpdatePluginsDescription": "Otomatik güncellenmeye ayarlanmış eklentilerin güncellemelerini indirir ve kurar.", "TaskUpdatePlugins": "Eklentileri Güncelle", "TaskRefreshPeople": "Kullanıcıları Yenile", From 5491840c2a8a49a950736f139cc2c50727c6bc73 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 1 Sep 2023 09:24:52 -0600 Subject: [PATCH 546/858] chore(deps): update dependency microsoft.net.test.sdk to v17.7.2 (#10166) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 66ce19cb7a..ff8b42f75d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -47,7 +47,7 @@ <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" /> <PackageVersion Include="Microsoft.Extensions.Logging" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Options" Version="7.0.1" /> - <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.7.1" /> + <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.7.2" /> <PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" /> <PackageVersion Include="MimeTypes" Version="2.4.0" /> <PackageVersion Include="Mono.Nat" Version="3.0.4" /> From 86380da8c6d1b889864f78223405405dcc03929f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 1 Sep 2023 15:26:47 +0000 Subject: [PATCH 547/858] chore(deps): update dependency xunit to v2.5.0 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index ff8b42f75d..6b9b51826f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -87,6 +87,6 @@ <PackageVersion Include="Xunit.Priority" Version="1.1.6" /> <PackageVersion Include="xunit.runner.visualstudio" Version="2.5.0" /> <PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" /> - <PackageVersion Include="xunit" Version="2.4.2" /> + <PackageVersion Include="xunit" Version="2.5.0" /> </ItemGroup> </Project> From 35a9feaf7038d26823dbd6d7252e0b0f9a09bb75 Mon Sep 17 00:00:00 2001 From: Cody Robibero <cody@robibe.ro> Date: Fri, 1 Sep 2023 09:35:48 -0600 Subject: [PATCH 548/858] Disable xUnit1028 --- tests/jellyfin-tests.ruleset | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/jellyfin-tests.ruleset b/tests/jellyfin-tests.ruleset index e2abaf5bb8..9d133da568 100644 --- a/tests/jellyfin-tests.ruleset +++ b/tests/jellyfin-tests.ruleset @@ -19,4 +19,10 @@ <!-- CA2234: Pass system uri objects instead of strings --> <Rule Id="CA2234" Action="Info" /> </Rules> + + <!-- xUnit --> + <Rules AnalyzerId="xUnit" RuleNamespace="xUnit"> + <!-- Test methods must have a supported return type. --> + <Rule Id="xUnit1028" Action="None" /> + </Rules> </RuleSet> From fb8b11276d530e12e3c7f604c5b5311cbc3d0905 Mon Sep 17 00:00:00 2001 From: Cody Robibero <cody@robibe.ro> Date: Fri, 1 Sep 2023 09:35:57 -0600 Subject: [PATCH 549/858] fix build --- .../SchedulesDirect/SchedulesDirectDeserializeTests.cs | 10 +++++----- .../UrlDecodeQueryFeatureTests.cs | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/SchedulesDirect/SchedulesDirectDeserializeTests.cs b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/SchedulesDirect/SchedulesDirectDeserializeTests.cs index e1d2bb2d58..d4f28f327f 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/SchedulesDirect/SchedulesDirectDeserializeTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/SchedulesDirect/SchedulesDirectDeserializeTests.cs @@ -96,7 +96,7 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv.SchedulesDirect var days = JsonSerializer.Deserialize<IReadOnlyList<DayDto>>(bytes, _jsonOptions); Assert.NotNull(days); - Assert.Equal(1, days!.Count); + Assert.Single(days); var dayDto = days[0]; Assert.Equal("20454", dayDto.StationId); @@ -110,7 +110,7 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv.SchedulesDirect Assert.Equal(2, dayDto.Programs[0].AudioProperties.Count); Assert.Equal("stereo", dayDto.Programs[0].AudioProperties[0]); Assert.Equal("cc", dayDto.Programs[0].AudioProperties[1]); - Assert.Equal(1, dayDto.Programs[0].VideoProperties.Count); + Assert.Single(dayDto.Programs[0].VideoProperties); Assert.Equal("hdtv", dayDto.Programs[0].VideoProperties[0]); } @@ -126,13 +126,13 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv.SchedulesDirect Assert.NotNull(programDtos); Assert.Equal(2, programDtos!.Count); Assert.Equal("EP000000060003", programDtos[0].ProgramId); - Assert.Equal(1, programDtos[0].Titles.Count); + Assert.Single(programDtos[0].Titles); Assert.Equal("'Allo 'Allo!", programDtos[0].Titles[0].Title120); Assert.Equal("Series", programDtos[0].EventDetails?.SubType); Assert.Equal("en", programDtos[0].Descriptions?.Description1000[0].DescriptionLanguage); Assert.Equal("A disguised British Intelligence officer is sent to help the airmen.", programDtos[0].Descriptions?.Description1000[0].Description); Assert.Equal(new DateTime(1985, 11, 04), programDtos[0].OriginalAirDate); - Assert.Equal(1, programDtos[0].Genres.Count); + Assert.Single(programDtos[0].Genres); Assert.Equal("Sitcom", programDtos[0].Genres[0]); Assert.Equal("The Poloceman Cometh", programDtos[0].EpisodeTitle150); Assert.Equal(2, programDtos[0].Metadata[0].Gracenote?.Season); @@ -161,7 +161,7 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv.SchedulesDirect var showImagesDtos = JsonSerializer.Deserialize<IReadOnlyList<ShowImagesDto>>(bytes, _jsonOptions); Assert.NotNull(showImagesDtos); - Assert.Equal(1, showImagesDtos!.Count); + Assert.Single(showImagesDtos!); Assert.Equal("SH00712240", showImagesDtos[0].ProgramId); Assert.Equal(4, showImagesDtos[0].Data.Count); Assert.Equal("135", showImagesDtos[0].Data[0].Width); diff --git a/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs b/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs index 797fc8f64b..93e0656856 100644 --- a/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs +++ b/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs @@ -22,7 +22,7 @@ namespace Jellyfin.Server.Tests Assert.Single(test.Query); var (k, v) = test.Query.First(); Assert.Equal(key, k); - Assert.Empty(v); + Assert.True(StringValues.IsNullOrEmpty(v)); } } } From b2dcc7c90e42160adab28e3dac31e2bc4fd2c193 Mon Sep 17 00:00:00 2001 From: nyanmisaka <nst799610810@gmail.com> Date: Sat, 2 Sep 2023 00:05:00 +0800 Subject: [PATCH 550/858] Fix AV1 playback in LiveTV AV1 in fMP4 requires global_header data for parsing. Only disable global_header in TS since it has no global_header. Signed-off-by: nyanmisaka <nst799610810@gmail.com> --- Jellyfin.Api/Controllers/DynamicHlsController.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 5ba0119389..065a4ce5c6 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1651,7 +1651,7 @@ public class DynamicHlsController : BaseJellyfinApiController _encodingHelper.GetInputArgument(state, _encodingOptions, segmentContainer), threads, mapArgs, - GetVideoArguments(state, startNumber, isEventPlaylist), + GetVideoArguments(state, startNumber, isEventPlaylist, segmentContainer), GetAudioArguments(state), maxMuxingQueueSize, state.SegmentLength.ToString(CultureInfo.InvariantCulture), @@ -1814,8 +1814,9 @@ public class DynamicHlsController : BaseJellyfinApiController /// <param name="state">The <see cref="StreamState"/>.</param> /// <param name="startNumber">The first number in the hls sequence.</param> /// <param name="isEventPlaylist">Whether the playlist is EVENT or VOD.</param> + /// <param name="segmentContainer">The segment container.</param> /// <returns>The command line arguments for video transcoding.</returns> - private string GetVideoArguments(StreamState state, int startNumber, bool isEventPlaylist) + private string GetVideoArguments(StreamState state, int startNumber, bool isEventPlaylist, string segmentContainer) { if (state.VideoStream is null) { @@ -1907,7 +1908,7 @@ public class DynamicHlsController : BaseJellyfinApiController } // TODO why was this not enabled for VOD? - if (isEventPlaylist) + if (isEventPlaylist && string.Equals(segmentContainer, "ts", StringComparison.OrdinalIgnoreCase)) { args += " -flags -global_header"; } From 31d2f653fadda410ecb5b2bc768af51c25485589 Mon Sep 17 00:00:00 2001 From: nyanmisaka <nst799610810@gmail.com> Date: Sat, 2 Sep 2023 00:18:32 +0800 Subject: [PATCH 551/858] Fix H264 QSV encoding when the bitrate is too low h264_qsv expects a bitrate equal or higher than 1000k, or it fails. Signed-off-by: nyanmisaka <nst799610810@gmail.com> --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 1905ffb1cd..d2eb54bf47 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -1216,6 +1216,12 @@ namespace MediaBrowser.Controller.MediaEncoding int bitrate = state.OutputVideoBitrate.Value; + // Bit rate under 1000k is not allowed in h264_qsv + if (string.Equals(videoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)) + { + bitrate = Math.Max(bitrate, 1000); + } + // Currently use the same buffer size for all encoders int bufsize = bitrate * 2; From 25bc4b875ccdf681b113e4261cd2b1e379795dbe Mon Sep 17 00:00:00 2001 From: Slug-Cat <Conrad.toms@outlook.com> Date: Sat, 2 Sep 2023 04:42:52 +0000 Subject: [PATCH 552/858] Translated using Weblate (Pirate (pr)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pr/ --- Emby.Server.Implementations/Localization/Core/pr.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/pr.json b/Emby.Server.Implementations/Localization/Core/pr.json index 87800a2fe8..d2446a7fdc 100644 --- a/Emby.Server.Implementations/Localization/Core/pr.json +++ b/Emby.Server.Implementations/Localization/Core/pr.json @@ -24,5 +24,10 @@ "TaskDownloadMissingSubtitlesDescription": "Scours the seven seas o' the internet for subtitles that be missin' based on the captain's map o' metadata.", "HeaderAlbumArtists": "Buccaneers o' the musical arts", "HeaderFavoriteAlbums": "Beloved booty o' musical adventures", - "HeaderFavoriteArtists": "Treasured scallywags o' the creative seas" + "HeaderFavoriteArtists": "Treasured scallywags o' the creative seas", + "Channels": "Channels", + "Forced": "Pressed", + "External": "Outboard", + "HeaderFavoriteEpisodes": "Treasured Tales", + "HeaderFavoriteShows": "Treasured Tales" } From 7d649d2f317a40630caa8add2d1b5b464f5e2e8d Mon Sep 17 00:00:00 2001 From: Ikko Eltociear Ashimine <eltociear@gmail.com> Date: Mon, 4 Sep 2023 02:59:13 +0900 Subject: [PATCH 553/858] Fix typo in NetworkConfiguration.cs conjuntion -> conjunction --- Jellyfin.Networking/Configuration/NetworkConfiguration.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs index 573c36be79..90ebcd390e 100644 --- a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs +++ b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs @@ -164,7 +164,7 @@ namespace Jellyfin.Networking.Configuration public string[] PublishedServerUriBySubnet { get; set; } = Array.Empty<string>(); /// <summary> - /// Gets or sets the filter for remote IP connectivity. Used in conjuntion with <seealso cref="IsRemoteIPFilterBlacklist"/>. + /// Gets or sets the filter for remote IP connectivity. Used in conjunction with <seealso cref="IsRemoteIPFilterBlacklist"/>. /// </summary> public string[] RemoteIPFilter { get; set; } = Array.Empty<string>(); From a892c72d7786988b4eceb76f98b669bc49ad0320 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 4 Sep 2023 13:52:33 +0000 Subject: [PATCH 554/858] chore(deps): update actions/checkout action to v4 --- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/commands.yml | 4 ++-- .github/workflows/openapi.yml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 86d9ed0e10..cdb7f5c732 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 - name: Setup .NET uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0 with: diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index 3b7f7b85b4..02aeefdbd4 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -24,7 +24,7 @@ jobs: reactions: '+1' - name: Checkout the latest code - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 with: token: ${{ secrets.JF_BOT_TOKEN }} fetch-depth: 0 @@ -51,7 +51,7 @@ jobs: reactions: eyes - name: Checkout the latest code - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 with: token: ${{ secrets.JF_BOT_TOKEN }} fetch-depth: 0 diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index ee64a522e7..95d492bd3c 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -14,7 +14,7 @@ jobs: permissions: read-all steps: - name: Checkout repository - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 with: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} @@ -39,7 +39,7 @@ jobs: permissions: read-all steps: - name: Checkout repository - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 with: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} From 5a860710a849b9d67d12731f1a616c4a3904487d Mon Sep 17 00:00:00 2001 From: Nick <20588554+nicknsy@users.noreply.github.com> Date: Mon, 4 Sep 2023 12:30:20 -0700 Subject: [PATCH 555/858] Make TrickplayManifest dictionary key a string rather than Guid --- Emby.Server.Implementations/Dto/DtoService.cs | 6 +----- .../Trickplay/TrickplayManager.cs | 6 +++--- MediaBrowser.Controller/Trickplay/ITrickplayManager.cs | 2 +- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 19ae396628..be361c4d1c 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -1064,11 +1064,7 @@ namespace Emby.Server.Implementations.Dto if (options.ContainsField(ItemFields.Trickplay)) { - var manifest = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult(); - - // To stay consistent with other fields, this must go from a Guid to a non-dashed string. - // This does not seem to occur automatically to dictionaries like it does with other Guid fields. - dto.Trickplay = manifest.ToDictionary(x => x.Key.ToString("N", CultureInfo.InvariantCulture), y => y.Value); + dto.Trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult(); } if (video.ExtraType.HasValue) diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs index e3bfdd50d6..107f8e5100 100644 --- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs +++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs @@ -349,9 +349,9 @@ public class TrickplayManager : ITrickplayManager } /// <inheritdoc /> - public async Task<Dictionary<Guid, Dictionary<int, TrickplayInfo>>> GetTrickplayManifest(BaseItem item) + public async Task<Dictionary<string, Dictionary<int, TrickplayInfo>>> GetTrickplayManifest(BaseItem item) { - var trickplayManifest = new Dictionary<Guid, Dictionary<int, TrickplayInfo>>(); + var trickplayManifest = new Dictionary<string, Dictionary<int, TrickplayInfo>>(); foreach (var mediaSource in item.GetMediaSources(false)) { var mediaSourceId = Guid.Parse(mediaSource.Id); @@ -359,7 +359,7 @@ public class TrickplayManager : ITrickplayManager if (trickplayResolutions.Count > 0) { - trickplayManifest[mediaSourceId] = trickplayResolutions; + trickplayManifest[mediaSource.Id] = trickplayResolutions; } } diff --git a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs index 7bea54fbab..0c41f30235 100644 --- a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs +++ b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs @@ -54,7 +54,7 @@ public interface ITrickplayManager /// </summary> /// <param name="item">The item.</param> /// <returns>A map of media source id to a map of tile width to trickplay info.</returns> - Task<Dictionary<Guid, Dictionary<int, TrickplayInfo>>> GetTrickplayManifest(BaseItem item); + Task<Dictionary<string, Dictionary<int, TrickplayInfo>>> GetTrickplayManifest(BaseItem item); /// <summary> /// Gets the path to a trickplay tile image. From f97e844c4fc2fb7626cc2e9fd706f9ce27d7c115 Mon Sep 17 00:00:00 2001 From: Nick <20588554+nicknsy@users.noreply.github.com> Date: Mon, 4 Sep 2023 13:14:45 -0700 Subject: [PATCH 556/858] Minor code review changes (cvium) --- Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs | 2 +- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs index 107f8e5100..b960feb7f3 100644 --- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs +++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs @@ -239,7 +239,7 @@ public class TrickplayManager : ITrickplayManager var tilePath = Path.Combine(workDir, $"{i}.jpg"); imageOptions.OutputPath = tilePath; - imageOptions.InputPaths = images.Skip(i * thumbnailsPerTile).Take(thumbnailsPerTile).ToList(); + imageOptions.InputPaths = images.GetRange(i * thumbnailsPerTile, Math.Min(thumbnailsPerTile, images.Count - (i * thumbnailsPerTile))); // Generate image and use returned height for tiles info var height = _imageEncoder.CreateTrickplayTile(imageOptions, options.JpegQuality, trickplayInfo.Width, trickplayInfo.Height != 0 ? trickplayInfo.Height : null); diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 000831fe2d..fa695bbdc2 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -925,8 +925,7 @@ namespace MediaBrowser.MediaEncoding.Encoder cancellationToken.ThrowIfCancellationRequested(); - var jpegCount = _fileSystem.GetFilePaths(targetDirectory) - .Count(i => string.Equals(Path.GetExtension(i), ".jpg", StringComparison.OrdinalIgnoreCase)); + var jpegCount = _fileSystem.GetFilePaths(targetDirectory).Count(); isResponsive = jpegCount > lastCount; lastCount = jpegCount; From 5c5daac98cd336de7acb47b9cd4c7850c55b1331 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Kucharczyk?= <lukas@kucharczyk.xyz> Date: Mon, 4 Sep 2023 05:55:35 +0000 Subject: [PATCH 557/858] Translated using Weblate (Czech) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/cs/ --- Emby.Server.Implementations/Localization/Core/cs.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json index 08db5a30e0..f33ea2fc95 100644 --- a/Emby.Server.Implementations/Localization/Core/cs.json +++ b/Emby.Server.Implementations/Localization/Core/cs.json @@ -22,7 +22,7 @@ "HeaderFavoriteEpisodes": "Oblíbené epizody", "HeaderFavoriteShows": "Oblíbené seriály", "HeaderFavoriteSongs": "Oblíbená hudba", - "HeaderLiveTV": "Televize", + "HeaderLiveTV": "Živý přenos", "HeaderNextUp": "Další díly", "HeaderRecordingGroups": "Skupiny nahrávek", "HomeVideos": "Domácí videa", From 8d6e7d893b4f814cdd82d235c4a2cfc4df6dad05 Mon Sep 17 00:00:00 2001 From: Bill Thornton <billt2006@gmail.com> Date: Tue, 5 Sep 2023 16:49:28 -0400 Subject: [PATCH 558/858] Remove one session per device id limitation --- .../Session/SessionManager.cs | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 03ff96b19a..f7015b31e5 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1519,24 +1519,9 @@ namespace Emby.Server.Implementations.Session DeviceId = deviceId }).ConfigureAwait(false)).Items; - foreach (var auth in allExistingForDevice) - { - if (existing is null || !string.Equals(auth.AccessToken, existing.AccessToken, StringComparison.Ordinal)) - { - try - { - await Logout(auth).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error while logging out."); - } - } - } - if (existing is not null) { - _logger.LogInformation("Reissuing access token: {Token}", existing.AccessToken); + _logger.LogInformation("Reusing existing access token: {Token}", existing.AccessToken); return existing.AccessToken; } From aea57c1a4a557d30c6169b530b6dccb0b0220b64 Mon Sep 17 00:00:00 2001 From: Bill Thornton <billt2006@gmail.com> Date: Wed, 6 Sep 2023 00:06:08 -0400 Subject: [PATCH 559/858] Remove unused variable --- Emby.Server.Implementations/Session/SessionManager.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index f7015b31e5..bbc096a876 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1513,12 +1513,6 @@ namespace Emby.Server.Implementations.Session Limit = 1 }).ConfigureAwait(false)).Items.FirstOrDefault(); - var allExistingForDevice = (await _deviceManager.GetDevices( - new DeviceQuery - { - DeviceId = deviceId - }).ConfigureAwait(false)).Items; - if (existing is not null) { _logger.LogInformation("Reusing existing access token: {Token}", existing.AccessToken); From 9c64f94458b955da8dfb7761d781ce2f45a8dfca Mon Sep 17 00:00:00 2001 From: Bill Thornton <billt2006@gmail.com> Date: Tue, 5 Sep 2023 23:30:06 -0400 Subject: [PATCH 560/858] Add option to include resumable items in next up requests --- .../TV/TVSeriesManager.cs | 20 +++++++++---------- Jellyfin.Api/Controllers/TvShowsController.cs | 5 ++++- MediaBrowser.Model/Querying/NextUpQuery.cs | 6 ++++++ 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs index f0e173f0b1..ef890aeb4f 100644 --- a/Emby.Server.Implementations/TV/TVSeriesManager.cs +++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs @@ -135,13 +135,13 @@ namespace Emby.Server.Implementations.TV private IEnumerable<Episode> GetNextUpEpisodes(NextUpQuery request, User user, IReadOnlyList<string> seriesKeys, DtoOptions dtoOptions) { - var allNextUp = seriesKeys.Select(i => GetNextUp(i, user, dtoOptions, false)); + var allNextUp = seriesKeys.Select(i => GetNextUp(i, user, dtoOptions, request.EnableResumable, false)); if (request.EnableRewatching) { - allNextUp = allNextUp.Concat( - seriesKeys.Select(i => GetNextUp(i, user, dtoOptions, true))) - .OrderByDescending(i => i.LastWatchedDate); + allNextUp = allNextUp + .Concat(seriesKeys.Select(i => GetNextUp(i, user, dtoOptions, false, true))) + .OrderByDescending(i => i.LastWatchedDate); } // If viewing all next up for all series, remove first episodes @@ -183,7 +183,7 @@ namespace Emby.Server.Implementations.TV /// Gets the next up. /// </summary> /// <returns>Task{Episode}.</returns> - private (DateTime LastWatchedDate, Func<Episode?> GetEpisodeFunction) GetNextUp(string seriesKey, User user, DtoOptions dtoOptions, bool rewatching) + private (DateTime LastWatchedDate, Func<Episode?> GetEpisodeFunction) GetNextUp(string seriesKey, User user, DtoOptions dtoOptions, bool includeResumable, bool includePlayed) { var lastQuery = new InternalItemsQuery(user) { @@ -200,8 +200,8 @@ namespace Emby.Server.Implementations.TV } }; - // If rewatching is enabled, sort first by date played and then by season and episode numbers - lastQuery.OrderBy = rewatching + // If including played results, sort first by date played and then by season and episode numbers + lastQuery.OrderBy = includePlayed ? new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) } : new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) }; @@ -216,7 +216,7 @@ namespace Emby.Server.Implementations.TV IncludeItemTypes = new[] { BaseItemKind.Episode }, OrderBy = new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Ascending), (ItemSortBy.IndexNumber, SortOrder.Ascending) }, Limit = 1, - IsPlayed = rewatching, + IsPlayed = includePlayed, IsVirtualItem = false, ParentIndexNumberNotEquals = 0, DtoOptions = dtoOptions @@ -240,7 +240,7 @@ namespace Emby.Server.Implementations.TV SeriesPresentationUniqueKey = seriesKey, ParentIndexNumber = 0, IncludeItemTypes = new[] { BaseItemKind.Episode }, - IsPlayed = rewatching, + IsPlayed = includePlayed, IsVirtualItem = false, DtoOptions = dtoOptions }) @@ -269,7 +269,7 @@ namespace Emby.Server.Implementations.TV nextEpisode = sortedConsideredEpisodes.FirstOrDefault(); } - if (nextEpisode is not null) + if (nextEpisode is not null && !includeResumable) { var userData = _userDataManager.GetUserData(user, nextEpisode); diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index 7d23281f2c..bdbbd1e0db 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -68,7 +68,8 @@ public class TvShowsController : BaseJellyfinApiController /// <param name="nextUpDateCutoff">Optional. Starting date of shows to show in Next Up section.</param> /// <param name="enableTotalRecordCount">Whether to enable the total records count. Defaults to true.</param> /// <param name="disableFirstEpisode">Whether to disable sending the first episode in a series as next up.</param> - /// <param name="enableRewatching">Whether to include watched episode in next up results.</param> + /// <param name="enableResumable">Whether to include resumable episodes in next up results.</param> + /// <param name="enableRewatching">Whether to include watched episodes in next up results.</param> /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns> [HttpGet("NextUp")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -86,6 +87,7 @@ public class TvShowsController : BaseJellyfinApiController [FromQuery] DateTime? nextUpDateCutoff, [FromQuery] bool enableTotalRecordCount = true, [FromQuery] bool disableFirstEpisode = false, + [FromQuery] bool enableResumable = true, [FromQuery] bool enableRewatching = false) { userId = RequestHelpers.GetUserId(User, userId); @@ -104,6 +106,7 @@ public class TvShowsController : BaseJellyfinApiController EnableTotalRecordCount = enableTotalRecordCount, DisableFirstEpisode = disableFirstEpisode, NextUpDateCutoff = nextUpDateCutoff ?? DateTime.MinValue, + EnableResumable = enableResumable, EnableRewatching = enableRewatching }, options); diff --git a/MediaBrowser.Model/Querying/NextUpQuery.cs b/MediaBrowser.Model/Querying/NextUpQuery.cs index 0fb996df97..35353e6fa6 100644 --- a/MediaBrowser.Model/Querying/NextUpQuery.cs +++ b/MediaBrowser.Model/Querying/NextUpQuery.cs @@ -14,6 +14,7 @@ namespace MediaBrowser.Model.Querying EnableTotalRecordCount = true; DisableFirstEpisode = false; NextUpDateCutoff = DateTime.MinValue; + EnableResumable = false; EnableRewatching = false; } @@ -83,6 +84,11 @@ namespace MediaBrowser.Model.Querying /// </summary> public DateTime NextUpDateCutoff { get; set; } + /// <summary> + /// Gets or sets a value indicating whether to include resumable episodes as next up. + /// </summary> + public bool EnableResumable { get; set; } + /// <summary> /// Gets or sets a value indicating whether getting rewatching next up list. /// </summary> From dca72cc275695ec9ef6b94fb45fb0973bd0deedb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 8 Sep 2023 18:19:53 -0600 Subject: [PATCH 561/858] chore(deps): update actions/upload-artifact action to v3.1.3 (#10201) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/openapi.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index ee64a522e7..fe65874dfb 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -25,7 +25,7 @@ jobs: - name: Generate openapi.json run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests" - name: Upload openapi.json - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 + uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 with: name: openapi-head retention-days: 14 @@ -59,7 +59,7 @@ jobs: - name: Generate openapi.json run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests" - name: Upload openapi.json - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 + uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 with: name: openapi-base retention-days: 14 From 3c2b1b5e9732d5d2f431ed830c700a486f2c6ac2 Mon Sep 17 00:00:00 2001 From: Nyanmisaka <nst799610810@gmail.com> Date: Sat, 9 Sep 2023 08:20:11 +0800 Subject: [PATCH 562/858] Fix AV1 NVENC encoder profile option (#10199) --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index d2eb54bf47..e619e690d2 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -1916,7 +1916,9 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(profile)) { - if (!string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase)) + // Currently there's no profile option in av1_nvenc encoder + if (!(string.Equals(videoEncoder, "av1_nvenc", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase))) { param += " -profile:v:0 " + profile; } From bc959270b762d1a6f68fc4f46a7c42138b39710c Mon Sep 17 00:00:00 2001 From: Lehonti Ramos <17771375+Lehonti@users.noreply.github.com> Date: Mon, 11 Sep 2023 12:12:40 +0200 Subject: [PATCH 563/858] Removed nesting levels through block-scoped `using` statement (#10025) Co-authored-by: John Doe <john@doe> Co-authored-by: Lehonti Ramos <lehonti@ramos> --- .../MigrateMusicBrainzTimeout.cs | 16 ++--- .../MigrateNetworkConfiguration.cs | 12 ++-- src/Jellyfin.Drawing.Skia/SkiaEncoder.cs | 6 +- src/Jellyfin.Drawing/ImageProcessor.cs | 6 +- src/Jellyfin.Extensions/StreamExtensions.cs | 6 +- .../Subtitles/AssParserTests.cs | 19 +++--- .../Subtitles/SrtParserTests.cs | 58 +++++++++---------- .../Subtitles/SsaParserTests.cs | 46 +++++++-------- .../Dlna/StreamBuilderTests.cs | 16 ++--- 9 files changed, 83 insertions(+), 102 deletions(-) diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs index bee135efda..0544fe561a 100644 --- a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs +++ b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Xml; using System.Xml.Serialization; @@ -59,21 +59,17 @@ public class MigrateMusicBrainzTimeout : IMigrationRoutine private OldMusicBrainzConfiguration? ReadOld(string path) { - using (var xmlReader = XmlReader.Create(path)) - { - var serverConfigSerializer = new XmlSerializer(typeof(OldMusicBrainzConfiguration), new XmlRootAttribute("PluginConfiguration")); - return serverConfigSerializer.Deserialize(xmlReader) as OldMusicBrainzConfiguration; - } + using var xmlReader = XmlReader.Create(path); + var serverConfigSerializer = new XmlSerializer(typeof(OldMusicBrainzConfiguration), new XmlRootAttribute("PluginConfiguration")); + return serverConfigSerializer.Deserialize(xmlReader) as OldMusicBrainzConfiguration; } private void WriteNew(string path, PluginConfiguration newPluginConfiguration) { var pluginConfigurationSerializer = new XmlSerializer(typeof(PluginConfiguration), new XmlRootAttribute("PluginConfiguration")); var xmlWriterSettings = new XmlWriterSettings { Indent = true }; - using (var xmlWriter = XmlWriter.Create(path, xmlWriterSettings)) - { - pluginConfigurationSerializer.Serialize(xmlWriter, newPluginConfiguration); - } + using var xmlWriter = XmlWriter.Create(path, xmlWriterSettings); + pluginConfigurationSerializer.Serialize(xmlWriter, newPluginConfiguration); } #pragma warning disable diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs index a4379197cd..c6d86b8cdb 100644 --- a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs +++ b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs @@ -43,10 +43,8 @@ public class MigrateNetworkConfiguration : IMigrationRoutine try { - using (var xmlReader = XmlReader.Create(path)) - { - oldNetworkConfiguration = (OldNetworkConfiguration?)oldNetworkConfigSerializer.Deserialize(xmlReader); - } + using var xmlReader = XmlReader.Create(path); + oldNetworkConfiguration = (OldNetworkConfiguration?)oldNetworkConfigSerializer.Deserialize(xmlReader); } catch (InvalidOperationException ex) { @@ -97,10 +95,8 @@ public class MigrateNetworkConfiguration : IMigrationRoutine var networkConfigSerializer = new XmlSerializer(typeof(NetworkConfiguration)); var xmlWriterSettings = new XmlWriterSettings { Indent = true }; - using (var xmlWriter = XmlWriter.Create(path, xmlWriterSettings)) - { - networkConfigSerializer.Serialize(xmlWriter, networkConfiguration); - } + using var xmlWriter = XmlWriter.Create(path, xmlWriterSettings); + networkConfigSerializer.Serialize(xmlWriter, networkConfiguration); } } diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 2d980db181..5a1d3dc5f9 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -489,10 +489,8 @@ public class SkiaEncoder : IImageEncoder Directory.CreateDirectory(directory); using (var outputStream = new SKFileWStream(outputPath)) { - using (var pixmap = new SKPixmap(new SKImageInfo(width, height), saveBitmap.GetPixels())) - { - pixmap.Encode(outputStream, skiaOutputFormat, quality); - } + using var pixmap = new SKPixmap(new SKImageInfo(width, height), saveBitmap.GetPixels()); + pixmap.Encode(outputStream, skiaOutputFormat, quality); } return outputPath; diff --git a/src/Jellyfin.Drawing/ImageProcessor.cs b/src/Jellyfin.Drawing/ImageProcessor.cs index 44e06bb521..4f16e294bc 100644 --- a/src/Jellyfin.Drawing/ImageProcessor.cs +++ b/src/Jellyfin.Drawing/ImageProcessor.cs @@ -111,10 +111,8 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable public async Task ProcessImage(ImageProcessingOptions options, Stream toStream) { var file = await ProcessImage(options).ConfigureAwait(false); - using (var fileStream = AsyncFile.OpenRead(file.Path)) - { - await fileStream.CopyToAsync(toStream).ConfigureAwait(false); - } + using var fileStream = AsyncFile.OpenRead(file.Path); + await fileStream.CopyToAsync(toStream).ConfigureAwait(false); } /// <inheritdoc /> diff --git a/src/Jellyfin.Extensions/StreamExtensions.cs b/src/Jellyfin.Extensions/StreamExtensions.cs index d76558ded4..1829968522 100644 --- a/src/Jellyfin.Extensions/StreamExtensions.cs +++ b/src/Jellyfin.Extensions/StreamExtensions.cs @@ -26,10 +26,8 @@ namespace Jellyfin.Extensions /// <returns>All lines in the stream.</returns> public static string[] ReadAllLines(this Stream stream, Encoding encoding) { - using (StreamReader reader = new StreamReader(stream, encoding)) - { - return ReadAllLines(reader).ToArray(); - } + using StreamReader reader = new StreamReader(stream, encoding); + return ReadAllLines(reader).ToArray(); } /// <summary> diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs index fe0d7fc90f..1f908d7e0e 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs @@ -12,17 +12,16 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests [Fact] public void Parse_Valid_Success() { - using (var stream = File.OpenRead("Test Data/example.ass")) - { - var parsed = new SubtitleEditParser(new NullLogger<SubtitleEditParser>()).Parse(stream, "ass"); - Assert.Single(parsed.TrackEvents); - var trackEvent = parsed.TrackEvents[0]; + using var stream = File.OpenRead("Test Data/example.ass"); - Assert.Equal("1", trackEvent.Id); - Assert.Equal(TimeSpan.Parse("00:00:01.18", CultureInfo.InvariantCulture).Ticks, trackEvent.StartPositionTicks); - Assert.Equal(TimeSpan.Parse("00:00:06.85", CultureInfo.InvariantCulture).Ticks, trackEvent.EndPositionTicks); - Assert.Equal("{\\pos(400,570)}Like an Angel with pity on nobody" + Environment.NewLine + "The second line in subtitle", trackEvent.Text); - } + var parsed = new SubtitleEditParser(new NullLogger<SubtitleEditParser>()).Parse(stream, "ass"); + Assert.Single(parsed.TrackEvents); + var trackEvent = parsed.TrackEvents[0]; + + Assert.Equal("1", trackEvent.Id); + Assert.Equal(TimeSpan.Parse("00:00:01.18", CultureInfo.InvariantCulture).Ticks, trackEvent.StartPositionTicks); + Assert.Equal(TimeSpan.Parse("00:00:06.85", CultureInfo.InvariantCulture).Ticks, trackEvent.EndPositionTicks); + Assert.Equal("{\\pos(400,570)}Like an Angel with pity on nobody" + Environment.NewLine + "The second line in subtitle", trackEvent.Text); } } } diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs index 2aebee5562..b7152961cd 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs @@ -12,45 +12,43 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests [Fact] public void Parse_Valid_Success() { - using (var stream = File.OpenRead("Test Data/example.srt")) - { - var parsed = new SubtitleEditParser(new NullLogger<SubtitleEditParser>()).Parse(stream, "srt"); - Assert.Equal(2, parsed.TrackEvents.Count); + using var stream = File.OpenRead("Test Data/example.srt"); - var trackEvent1 = parsed.TrackEvents[0]; - Assert.Equal("1", trackEvent1.Id); - Assert.Equal(TimeSpan.Parse("00:02:17.440", CultureInfo.InvariantCulture).Ticks, trackEvent1.StartPositionTicks); - Assert.Equal(TimeSpan.Parse("00:02:20.375", CultureInfo.InvariantCulture).Ticks, trackEvent1.EndPositionTicks); - Assert.Equal("Senator, we're making" + Environment.NewLine + "our final approach into Coruscant.", trackEvent1.Text); + var parsed = new SubtitleEditParser(new NullLogger<SubtitleEditParser>()).Parse(stream, "srt"); + Assert.Equal(2, parsed.TrackEvents.Count); - var trackEvent2 = parsed.TrackEvents[1]; - Assert.Equal("2", trackEvent2.Id); - Assert.Equal(TimeSpan.Parse("00:02:20.476", CultureInfo.InvariantCulture).Ticks, trackEvent2.StartPositionTicks); - Assert.Equal(TimeSpan.Parse("00:02:22.501", CultureInfo.InvariantCulture).Ticks, trackEvent2.EndPositionTicks); - Assert.Equal("Very good, Lieutenant.", trackEvent2.Text); - } + var trackEvent1 = parsed.TrackEvents[0]; + Assert.Equal("1", trackEvent1.Id); + Assert.Equal(TimeSpan.Parse("00:02:17.440", CultureInfo.InvariantCulture).Ticks, trackEvent1.StartPositionTicks); + Assert.Equal(TimeSpan.Parse("00:02:20.375", CultureInfo.InvariantCulture).Ticks, trackEvent1.EndPositionTicks); + Assert.Equal("Senator, we're making" + Environment.NewLine + "our final approach into Coruscant.", trackEvent1.Text); + + var trackEvent2 = parsed.TrackEvents[1]; + Assert.Equal("2", trackEvent2.Id); + Assert.Equal(TimeSpan.Parse("00:02:20.476", CultureInfo.InvariantCulture).Ticks, trackEvent2.StartPositionTicks); + Assert.Equal(TimeSpan.Parse("00:02:22.501", CultureInfo.InvariantCulture).Ticks, trackEvent2.EndPositionTicks); + Assert.Equal("Very good, Lieutenant.", trackEvent2.Text); } [Fact] public void Parse_EmptyNewlineBetweenText_Success() { - using (var stream = File.OpenRead("Test Data/example2.srt")) - { - var parsed = new SubtitleEditParser(new NullLogger<SubtitleEditParser>()).Parse(stream, "srt"); - Assert.Equal(2, parsed.TrackEvents.Count); + using var stream = File.OpenRead("Test Data/example2.srt"); - var trackEvent1 = parsed.TrackEvents[0]; - Assert.Equal("311", trackEvent1.Id); - Assert.Equal(TimeSpan.Parse("00:16:46.465", CultureInfo.InvariantCulture).Ticks, trackEvent1.StartPositionTicks); - Assert.Equal(TimeSpan.Parse("00:16:49.009", CultureInfo.InvariantCulture).Ticks, trackEvent1.EndPositionTicks); - Assert.Equal("Una vez que la gente se entere" + Environment.NewLine + Environment.NewLine + "de que ustedes están aquí,", trackEvent1.Text); + var parsed = new SubtitleEditParser(new NullLogger<SubtitleEditParser>()).Parse(stream, "srt"); + Assert.Equal(2, parsed.TrackEvents.Count); - var trackEvent2 = parsed.TrackEvents[1]; - Assert.Equal("312", trackEvent2.Id); - Assert.Equal(TimeSpan.Parse("00:16:49.092", CultureInfo.InvariantCulture).Ticks, trackEvent2.StartPositionTicks); - Assert.Equal(TimeSpan.Parse("00:16:51.470", CultureInfo.InvariantCulture).Ticks, trackEvent2.EndPositionTicks); - Assert.Equal("este lugar se convertirá" + Environment.NewLine + Environment.NewLine + "en un maldito zoológico.", trackEvent2.Text); - } + var trackEvent1 = parsed.TrackEvents[0]; + Assert.Equal("311", trackEvent1.Id); + Assert.Equal(TimeSpan.Parse("00:16:46.465", CultureInfo.InvariantCulture).Ticks, trackEvent1.StartPositionTicks); + Assert.Equal(TimeSpan.Parse("00:16:49.009", CultureInfo.InvariantCulture).Ticks, trackEvent1.EndPositionTicks); + Assert.Equal("Una vez que la gente se entere" + Environment.NewLine + Environment.NewLine + "de que ustedes están aquí,", trackEvent1.Text); + + var trackEvent2 = parsed.TrackEvents[1]; + Assert.Equal("312", trackEvent2.Id); + Assert.Equal(TimeSpan.Parse("00:16:49.092", CultureInfo.InvariantCulture).Ticks, trackEvent2.StartPositionTicks); + Assert.Equal(TimeSpan.Parse("00:16:51.470", CultureInfo.InvariantCulture).Ticks, trackEvent2.EndPositionTicks); + Assert.Equal("este lugar se convertirá" + Environment.NewLine + Environment.NewLine + "en un maldito zoológico.", trackEvent2.Text); } } } diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs index 6abf2d26cb..5b7aa7eaa9 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs @@ -18,22 +18,21 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests [MemberData(nameof(Parse_MultipleDialogues_TestData))] public void Parse_MultipleDialogues_Success(string ssa, IReadOnlyList<SubtitleTrackEvent> expectedSubtitleTrackEvents) { - using (Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(ssa))) + using Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(ssa)); + + SubtitleTrackInfo subtitleTrackInfo = _parser.Parse(stream, "ssa"); + + Assert.Equal(expectedSubtitleTrackEvents.Count, subtitleTrackInfo.TrackEvents.Count); + + for (int i = 0; i < expectedSubtitleTrackEvents.Count; ++i) { - SubtitleTrackInfo subtitleTrackInfo = _parser.Parse(stream, "ssa"); + SubtitleTrackEvent expected = expectedSubtitleTrackEvents[i]; + SubtitleTrackEvent actual = subtitleTrackInfo.TrackEvents[i]; - Assert.Equal(expectedSubtitleTrackEvents.Count, subtitleTrackInfo.TrackEvents.Count); - - for (int i = 0; i < expectedSubtitleTrackEvents.Count; ++i) - { - SubtitleTrackEvent expected = expectedSubtitleTrackEvents[i]; - SubtitleTrackEvent actual = subtitleTrackInfo.TrackEvents[i]; - - Assert.Equal(expected.Id, actual.Id); - Assert.Equal(expected.Text, actual.Text); - Assert.Equal(expected.StartPositionTicks, actual.StartPositionTicks); - Assert.Equal(expected.EndPositionTicks, actual.EndPositionTicks); - } + Assert.Equal(expected.Id, actual.Id); + Assert.Equal(expected.Text, actual.Text); + Assert.Equal(expected.StartPositionTicks, actual.StartPositionTicks); + Assert.Equal(expected.EndPositionTicks, actual.EndPositionTicks); } } @@ -73,17 +72,16 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests [Fact] public void Parse_Valid_Success() { - using (var stream = File.OpenRead("Test Data/example.ssa")) - { - var parsed = _parser.Parse(stream, "ssa"); - Assert.Single(parsed.TrackEvents); - var trackEvent = parsed.TrackEvents[0]; + using var stream = File.OpenRead("Test Data/example.ssa"); - Assert.Equal("1", trackEvent.Id); - Assert.Equal(TimeSpan.Parse("00:00:01.18", CultureInfo.InvariantCulture).Ticks, trackEvent.StartPositionTicks); - Assert.Equal(TimeSpan.Parse("00:00:06.85", CultureInfo.InvariantCulture).Ticks, trackEvent.EndPositionTicks); - Assert.Equal("{\\pos(400,570)}Like an angel with pity on nobody", trackEvent.Text); - } + var parsed = _parser.Parse(stream, "ssa"); + Assert.Single(parsed.TrackEvents); + var trackEvent = parsed.TrackEvents[0]; + + Assert.Equal("1", trackEvent.Id); + Assert.Equal(TimeSpan.Parse("00:00:01.18", CultureInfo.InvariantCulture).Ticks, trackEvent.StartPositionTicks); + Assert.Equal(TimeSpan.Parse("00:00:06.85", CultureInfo.InvariantCulture).Ticks, trackEvent.EndPositionTicks); + Assert.Equal("{\\pos(400,570)}Like an angel with pity on nobody", trackEvent.Text); } } } diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs index c30dad6f90..4c727428b4 100644 --- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs +++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs @@ -488,16 +488,16 @@ namespace Jellyfin.Model.Tests private static async ValueTask<T> TestData<T>(string name) { var path = Path.Join("Test Data", typeof(T).Name + "-" + name + ".json"); - using (var stream = File.OpenRead(path)) - { - var value = await JsonSerializer.DeserializeAsync<T>(stream, JsonDefaults.Options); - if (value is not null) - { - return value; - } - throw new SerializationException("Invalid test data: " + name); + using var stream = File.OpenRead(path); + + var value = await JsonSerializer.DeserializeAsync<T>(stream, JsonDefaults.Options); + if (value is not null) + { + return value; } + + throw new SerializationException("Invalid test data: " + name); } private StreamBuilder GetStreamBuilder() From 136a4abbd3c7304479bb7d7218d7c1b39955e5d1 Mon Sep 17 00:00:00 2001 From: LJQ <leejunquan7@gmail.com> Date: Mon, 11 Sep 2023 20:29:49 +0800 Subject: [PATCH 564/858] Applied Suggested Changes --- MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs index 319364b395..69ffd7859c 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs @@ -139,8 +139,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV for (int i = 1; i < result.Count; i++) { - name.Append(" / " + result[i].Name); - overview.Append(" / " + result[i].Overview); + name.Append(" / ").Append(result[i].Name); + overview.Append(" / ").Append(result[i].Overview); } episodeResult.Name = name.ToString(); From f40dd22dd5be36272e6a522b38ee6d36e440dddd Mon Sep 17 00:00:00 2001 From: Cody Robibero <cody@robibe.ro> Date: Mon, 11 Sep 2023 08:16:43 -0600 Subject: [PATCH 565/858] Add global.json to specify dotnet version --- global.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 global.json diff --git a/global.json b/global.json new file mode 100644 index 0000000000..24335d7a0f --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "7.0.0", + "rollForward": "latestMinor" + } +} From 9ea46b9e17c82ae276d13d32647bf4c8d0c10ac3 Mon Sep 17 00:00:00 2001 From: Bill Thornton <billt2006@gmail.com> Date: Mon, 11 Sep 2023 10:49:01 -0400 Subject: [PATCH 566/858] Remove existing sessions for a user on the same device on login --- .../Session/SessionManager.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index bbc096a876..b4a622ccf4 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1509,14 +1509,20 @@ namespace Emby.Server.Implementations.Session new DeviceQuery { DeviceId = deviceId, - UserId = user.Id, - Limit = 1 - }).ConfigureAwait(false)).Items.FirstOrDefault(); + UserId = user.Id + }).ConfigureAwait(false)).Items; - if (existing is not null) + foreach (var auth in existing) { - _logger.LogInformation("Reusing existing access token: {Token}", existing.AccessToken); - return existing.AccessToken; + try + { + // Logout any existing sessions for the user on this device + await Logout(auth).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while logging out existing session."); + } } _logger.LogInformation("Creating new access token for user {0}", user.Id); From 9d5e6108fb6446834e7d7db12abc10f03a8657f5 Mon Sep 17 00:00:00 2001 From: "Brian J. Murrell" <brian@interlinx.bc.ca> Date: Tue, 12 Sep 2023 07:57:09 -0400 Subject: [PATCH 567/858] Make startup script more portable Make the creation of the startup script more portable and future-proof. --- fedora/jellyfin.spec | 1 + 1 file changed, 1 insertion(+) diff --git a/fedora/jellyfin.spec b/fedora/jellyfin.spec index d9d3d48694..1f7262e660 100644 --- a/fedora/jellyfin.spec +++ b/fedora/jellyfin.spec @@ -75,6 +75,7 @@ dotnet publish --configuration Release --self-contained --runtime %{dotnet_runti %{__mkdir} -p %{buildroot}%{_libdir}/jellyfin %{buildroot}%{_bindir} %{__cp} -r Jellyfin.Server/bin/Release/net7.0/%{dotnet_runtime}/publish/* %{buildroot}%{_libdir}/jellyfin %{__install} -D %{SOURCE10} %{buildroot}%{_bindir}/jellyfin +sed -i -e 's/\/usr\/lib64/%{_libdir}/g' %{buildroot}%{_bindir}/jellyfin # Jellyfin config %{__install} -D Jellyfin.Server/Resources/Configuration/logging.json %{buildroot}%{_sysconfdir}/jellyfin/logging.json From 47b21bd781385edb2bdbde96b180005e99d9919c Mon Sep 17 00:00:00 2001 From: Bond-009 <bond.009@outlook.com> Date: Tue, 12 Sep 2023 14:23:12 +0200 Subject: [PATCH 568/858] Update Swashbuckle.AspNetCore.ReDoc to 6.5.0 (#10210) --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 6b9b51826f..28f9188ecd 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -74,7 +74,7 @@ <PackageVersion Include="SkiaSharp" Version="2.88.5" /> <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.507" /> - <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.4.0" /> + <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" /> <PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" /> <PackageVersion Include="System.Globalization" Version="4.3.0" /> <PackageVersion Include="System.Linq.Async" Version="6.0.1" /> From 3f19befc594670d72c2611f103e703633960e0aa Mon Sep 17 00:00:00 2001 From: "Brian J. Murrell" <brian@interlinx.bc.ca> Date: Tue, 12 Sep 2023 15:09:40 -0400 Subject: [PATCH 569/858] Avoid shell expansion issues (#10211) --- fedora/jellyfin-selinux-launcher.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fedora/jellyfin-selinux-launcher.sh b/fedora/jellyfin-selinux-launcher.sh index a735a238ff..e07a351d9a 100644 --- a/fedora/jellyfin-selinux-launcher.sh +++ b/fedora/jellyfin-selinux-launcher.sh @@ -1,3 +1,3 @@ #!/bin/sh -exec /usr/lib64/jellyfin/jellyfin ${@} +exec /usr/lib64/jellyfin/jellyfin "${@}" From 745a7eb4ae042f4ad9fbf58321c459dd5990278a Mon Sep 17 00:00:00 2001 From: Bill Thornton <billt2006@gmail.com> Date: Wed, 13 Sep 2023 08:39:02 -0400 Subject: [PATCH 570/858] Run collect script on failures --- .ci/azure-pipelines-package.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml index c28b1bf7f0..c91a084e58 100644 --- a/.ci/azure-pipelines-package.yml +++ b/.ci/azure-pipelines-package.yml @@ -168,6 +168,7 @@ jobs: - job: CollectArtifacts timeoutInMinutes: 20 displayName: 'Collect Artifacts' + condition: succeededOrFailed() continueOnError: true dependsOn: - BuildPackage From 767a42fbdbbb2db30313d0935f322f162ebeced4 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Wed, 13 Sep 2023 17:30:50 +0200 Subject: [PATCH 571/858] Minor LibraryMonitor improvements * Enable nullable * Add a fast return to ReportFileSystemChanged when path should be ignored * Use Span overloads of Path.* functions where possible * IFileSystem: remove NormalizePath as Path.TrimEndingDirectorySeparator already checks if it's a root path --- .../ApplicationHost.cs | 2 +- .../IO/FileRefresher.cs | 9 +-- .../IO/LibraryMonitor.cs | 68 ++++++++++--------- .../IO/ManagedFileSystem.cs | 18 +---- .../IO/MbLinkShortcutHandler.cs | 11 +-- .../Library/LibraryManager.cs | 2 +- MediaBrowser.Model/IO/IFileSystem.cs | 7 -- jellyfin.ruleset | 2 + 8 files changed, 44 insertions(+), 75 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 8b13ccadab..f8208af704 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -157,7 +157,7 @@ namespace Emby.Server.Implementations _fileSystemManager = new ManagedFileSystem(LoggerFactory.CreateLogger<ManagedFileSystem>(), applicationPaths); Logger = LoggerFactory.CreateLogger<ApplicationHost>(); - _fileSystemManager.AddShortcutHandler(new MbLinkShortcutHandler(_fileSystemManager)); + _fileSystemManager.AddShortcutHandler(new MbLinkShortcutHandler()); _deviceId = new DeviceId(ApplicationPaths, LoggerFactory); ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version; diff --git a/Emby.Server.Implementations/IO/FileRefresher.cs b/Emby.Server.Implementations/IO/FileRefresher.cs index 0ad81b653c..15b1836eba 100644 --- a/Emby.Server.Implementations/IO/FileRefresher.cs +++ b/Emby.Server.Implementations/IO/FileRefresher.cs @@ -85,7 +85,7 @@ namespace Emby.Server.Implementations.IO } } - public void ResetPath(string path, string affectedFile) + public void ResetPath(string path, string? affectedFile) { lock (_timerLock) { @@ -148,13 +148,6 @@ namespace Emby.Server.Implementations.IO { item.ChangedExternally(); } - catch (IOException ex) - { - // For now swallow and log. - // Research item: If an IOException occurs, the item may be in a disconnected state (media unavailable) - // Should we remove it from it's parent? - _logger.LogError(ex, "Error refreshing {Name}", item.Name); - } catch (Exception ex) { _logger.LogError(ex, "Error refreshing {Name}", item.Name); diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs index f67a02be83..1b6e454878 100644 --- a/Emby.Server.Implementations/IO/LibraryMonitor.cs +++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -160,7 +158,7 @@ namespace Emby.Server.Implementations.IO /// </summary> /// <param name="sender">The source of the event.</param> /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param> - private void OnLibraryManagerItemRemoved(object sender, ItemChangeEventArgs e) + private void OnLibraryManagerItemRemoved(object? sender, ItemChangeEventArgs e) { if (e.Parent is AggregateFolder) { @@ -173,7 +171,7 @@ namespace Emby.Server.Implementations.IO /// </summary> /// <param name="sender">The source of the event.</param> /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param> - private void OnLibraryManagerItemAdded(object sender, ItemChangeEventArgs e) + private void OnLibraryManagerItemAdded(object? sender, ItemChangeEventArgs e) { if (e.Parent is AggregateFolder) { @@ -189,19 +187,28 @@ namespace Emby.Server.Implementations.IO /// <param name="path">The path.</param> /// <returns><c>true</c> if [contains parent folder] [the specified LST]; otherwise, <c>false</c>.</returns> /// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c>.</exception> - private static bool ContainsParentFolder(IEnumerable<string> lst, string path) + private static bool ContainsParentFolder(IReadOnlyList<string> lst, ReadOnlySpan<char> path) { - ArgumentException.ThrowIfNullOrEmpty(path); + if (path.IsEmpty) + { + throw new ArgumentException("Path can't be empty", nameof(path)); + } path = path.TrimEnd(Path.DirectorySeparatorChar); - return lst.Any(str => + foreach (var str in lst) { // this should be a little quicker than examining each actual parent folder... - var compare = str.TrimEnd(Path.DirectorySeparatorChar); + var compare = str.AsSpan().TrimEnd(Path.DirectorySeparatorChar); - return path.Equals(compare, StringComparison.OrdinalIgnoreCase) || (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == Path.DirectorySeparatorChar); - }); + if (path.Equals(compare, StringComparison.OrdinalIgnoreCase) + || (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == Path.DirectorySeparatorChar)) + { + return true; + } + } + + return false; } /// <summary> @@ -349,21 +356,19 @@ namespace Emby.Server.Implementations.IO { ArgumentException.ThrowIfNullOrEmpty(path); - var monitorPath = !IgnorePatterns.ShouldIgnore(path); + if (IgnorePatterns.ShouldIgnore(path)) + { + return; + } // Ignore certain files, If the parent of an ignored path has a change event, ignore that too - if (_tempIgnoredPaths.Keys.Any(i => + foreach (var i in _tempIgnoredPaths.Keys) { - if (_fileSystem.AreEqual(i, path)) + if (_fileSystem.AreEqual(i, path) + || _fileSystem.ContainsSubPath(i, path)) { _logger.LogDebug("Ignoring change to {Path}", path); - return true; - } - - if (_fileSystem.ContainsSubPath(i, path)) - { - _logger.LogDebug("Ignoring change to {Path}", path); - return true; + return; } // Go up a level @@ -371,20 +376,12 @@ namespace Emby.Server.Implementations.IO if (!string.IsNullOrEmpty(parent) && _fileSystem.AreEqual(parent, path)) { _logger.LogDebug("Ignoring change to {Path}", path); - return true; + return; } - - return false; - })) - { - monitorPath = false; } - if (monitorPath) - { - // Avoid implicitly captured closure - CreateRefresher(path); - } + // Avoid implicitly captured closure + CreateRefresher(path); } private void CreateRefresher(string path) @@ -417,7 +414,7 @@ namespace Emby.Server.Implementations.IO } // They are siblings. Rebase the refresher to the parent folder. - if (string.Equals(parentPath, Path.GetDirectoryName(refresher.Path), StringComparison.Ordinal)) + if (parentPath is not null && string.Equals(parentPath, Path.GetDirectoryName(refresher.Path), StringComparison.Ordinal)) { refresher.ResetPath(parentPath, path); return; @@ -430,8 +427,13 @@ namespace Emby.Server.Implementations.IO } } - private void OnNewRefresherCompleted(object sender, EventArgs e) + private void OnNewRefresherCompleted(object? sender, EventArgs e) { + if (sender is null) + { + return; + } + var refresher = (FileRefresher)sender; DisposeRefresher(refresher); } diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 0ba4a488b0..3aa5233ed1 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -485,25 +485,11 @@ namespace Emby.Server.Implementations.IO _isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); } - /// <inheritdoc /> - public virtual string NormalizePath(string path) - { - ArgumentException.ThrowIfNullOrEmpty(path); - - if (path.EndsWith(":\\", StringComparison.OrdinalIgnoreCase)) - { - return path; - } - - return Path.TrimEndingDirectorySeparator(path); - } - /// <inheritdoc /> public virtual bool AreEqual(string path1, string path2) { - return string.Equals( - NormalizePath(path1), - NormalizePath(path2), + return Path.TrimEndingDirectorySeparator(path1).Equals( + Path.TrimEndingDirectorySeparator(path2), _isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); } diff --git a/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs b/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs index c2aab38798..5776c7a7c4 100644 --- a/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs +++ b/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs @@ -8,24 +8,17 @@ namespace Emby.Server.Implementations.IO { public class MbLinkShortcutHandler : IShortcutHandler { - private readonly IFileSystem _fileSystem; - - public MbLinkShortcutHandler(IFileSystem fileSystem) - { - _fileSystem = fileSystem; - } - public string Extension => ".mblink"; public string? Resolve(string shortcutPath) { ArgumentException.ThrowIfNullOrEmpty(shortcutPath); - if (string.Equals(Path.GetExtension(shortcutPath), ".mblink", StringComparison.OrdinalIgnoreCase)) + if (Path.GetExtension(shortcutPath.AsSpan()).Equals(".mblink", StringComparison.OrdinalIgnoreCase)) { var path = File.ReadAllText(shortcutPath); - return _fileSystem.NormalizePath(path); + return Path.TrimEndingDirectorySeparator(path); } return null; diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 808cedd678..b0a4a4151a 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -608,7 +608,7 @@ namespace Emby.Server.Implementations.Library var originalList = paths.ToList(); var list = originalList.Where(i => i.IsDirectory) - .Select(i => _fileSystem.NormalizePath(i.FullName)) + .Select(i => Path.TrimEndingDirectorySeparator(i.FullName)) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); diff --git a/MediaBrowser.Model/IO/IFileSystem.cs b/MediaBrowser.Model/IO/IFileSystem.cs index 786b20e9e6..5bdda2b240 100644 --- a/MediaBrowser.Model/IO/IFileSystem.cs +++ b/MediaBrowser.Model/IO/IFileSystem.cs @@ -116,13 +116,6 @@ namespace MediaBrowser.Model.IO /// <returns><c>true</c> if [contains sub path] [the specified parent path]; otherwise, <c>false</c>.</returns> bool ContainsSubPath(string parentPath, string path); - /// <summary> - /// Normalizes the path. - /// </summary> - /// <param name="path">The path.</param> - /// <returns>System.String.</returns> - string NormalizePath(string path); - /// <summary> /// Gets the file name without extension. /// </summary> diff --git a/jellyfin.ruleset b/jellyfin.ruleset index 505d35481f..4f01695888 100644 --- a/jellyfin.ruleset +++ b/jellyfin.ruleset @@ -52,6 +52,8 @@ <Rule Id="SA1204" Action="None" /> <!-- disable warning SA1309: Fields must not begin with an underscore --> <Rule Id="SA1309" Action="None" /> + <!-- disable warning SA1311: Static readonly fields should begin with upper-case letter --> + <Rule Id="SA1311" Action="None" /> <!-- disable warning SA1413: Use trailing comma in multi-line initializers --> <Rule Id="SA1413" Action="None" /> <!-- disable warning SA1512: Single-line comments must not be followed by blank line --> From 985b115754ddfb4237b4fffa2033ec4ec8e84e0c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 13 Sep 2023 17:33:53 -0600 Subject: [PATCH 572/858] chore(deps): update dotnet monorepo to v7.0.11 (#10213) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Cody Robibero <cody@robibe.ro> --- Directory.Packages.props | 18 +++++++++--------- deployment/Dockerfile.centos.amd64 | 2 +- deployment/Dockerfile.fedora.amd64 | 2 +- deployment/Dockerfile.ubuntu.amd64 | 2 +- deployment/Dockerfile.ubuntu.arm64 | 2 +- deployment/Dockerfile.ubuntu.armhf | 2 +- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 28f9188ecd..2341b824fd 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -23,15 +23,15 @@ <PackageVersion Include="libse" Version="3.6.13" /> <PackageVersion Include="LrcParser" Version="2023.524.0" /> <PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" /> - <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.10" /> + <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.11" /> <PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" /> - <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.10" /> + <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.11" /> <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" /> - <PackageVersion Include="Microsoft.Data.Sqlite" Version="7.0.10" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.10" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.10" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.10" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.10" /> + <PackageVersion Include="Microsoft.Data.Sqlite" Version="7.0.11" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.11" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.11" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.11" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.11" /> <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" /> @@ -40,8 +40,8 @@ <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" /> - <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.10" /> - <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.10" /> + <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.11" /> + <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.11" /> <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Http" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" /> diff --git a/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64 index 28b6310ef1..28cf585e98 100644 --- a/deployment/Dockerfile.centos.amd64 +++ b/deployment/Dockerfile.centos.amd64 @@ -13,7 +13,7 @@ RUN yum update -yq \ && yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget # Install DotNET SDK -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/dbfe6cc7-dd82-4cec-b267-31ed988b1652/c60ab4793c3714be878abcb9aa834b63/dotnet-sdk-7.0.400-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/61f29db0-10a5-4816-8fd8-ca2f71beaea3/e15fb7288eb5bc0053b91ea7b0bfd580/dotnet-sdk-7.0.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.fedora.amd64 b/deployment/Dockerfile.fedora.amd64 index 8a52587141..0e8166f46c 100644 --- a/deployment/Dockerfile.fedora.amd64 +++ b/deployment/Dockerfile.fedora.amd64 @@ -12,7 +12,7 @@ RUN dnf update -yq \ && dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd wget make # Install DotNET SDK -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/dbfe6cc7-dd82-4cec-b267-31ed988b1652/c60ab4793c3714be878abcb9aa834b63/dotnet-sdk-7.0.400-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/61f29db0-10a5-4816-8fd8-ca2f71beaea3/e15fb7288eb5bc0053b91ea7b0bfd580/dotnet-sdk-7.0.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64 index e06179ac9b..04c748d096 100644 --- a/deployment/Dockerfile.ubuntu.amd64 +++ b/deployment/Dockerfile.ubuntu.amd64 @@ -17,7 +17,7 @@ RUN apt-get update -yqq \ libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/dbfe6cc7-dd82-4cec-b267-31ed988b1652/c60ab4793c3714be878abcb9aa834b63/dotnet-sdk-7.0.400-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/61f29db0-10a5-4816-8fd8-ca2f71beaea3/e15fb7288eb5bc0053b91ea7b0bfd580/dotnet-sdk-7.0.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.arm64 b/deployment/Dockerfile.ubuntu.arm64 index 964c3fca7b..5bc197679b 100644 --- a/deployment/Dockerfile.ubuntu.arm64 +++ b/deployment/Dockerfile.ubuntu.arm64 @@ -16,7 +16,7 @@ RUN apt-get update -yqq \ mmv build-essential lsb-release # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/dbfe6cc7-dd82-4cec-b267-31ed988b1652/c60ab4793c3714be878abcb9aa834b63/dotnet-sdk-7.0.400-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/61f29db0-10a5-4816-8fd8-ca2f71beaea3/e15fb7288eb5bc0053b91ea7b0bfd580/dotnet-sdk-7.0.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.armhf b/deployment/Dockerfile.ubuntu.armhf index 4eb1343ac5..fab869a6b8 100644 --- a/deployment/Dockerfile.ubuntu.armhf +++ b/deployment/Dockerfile.ubuntu.armhf @@ -16,7 +16,7 @@ RUN apt-get update -yqq \ mmv build-essential lsb-release # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/dbfe6cc7-dd82-4cec-b267-31ed988b1652/c60ab4793c3714be878abcb9aa834b63/dotnet-sdk-7.0.400-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/61f29db0-10a5-4816-8fd8-ca2f71beaea3/e15fb7288eb5bc0053b91ea7b0bfd580/dotnet-sdk-7.0.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet From b8b7f5dd330c868b01a20ad639312942b84aeeaa Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 14 Sep 2023 19:20:33 +0000 Subject: [PATCH 573/858] chore(deps): update github/codeql-action action to v2.21.7 --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index cdb7f5c732..30c241224b 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@00e563ead9f72a8461b24876bee2d0c2e8bd2ee8 # v2.21.5 + uses: github/codeql-action/init@04daf014b50eaf774287bf3f0f1869d4b4c4b913 # v2.21.7 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@00e563ead9f72a8461b24876bee2d0c2e8bd2ee8 # v2.21.5 + uses: github/codeql-action/autobuild@04daf014b50eaf774287bf3f0f1869d4b4c4b913 # v2.21.7 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@00e563ead9f72a8461b24876bee2d0c2e8bd2ee8 # v2.21.5 + uses: github/codeql-action/analyze@04daf014b50eaf774287bf3f0f1869d4b4c4b913 # v2.21.7 From ba928d872e823ff679117724aebecbec0fa0f9f6 Mon Sep 17 00:00:00 2001 From: cvium <clausvium@gmail.com> Date: Sat, 16 Sep 2023 07:25:29 +0200 Subject: [PATCH 574/858] fix: open the connection when using SqliteConnection directly --- Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs | 2 ++ Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs | 1 + Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs | 3 ++- Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs | 1 + Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs | 3 ++- 5 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs index c7c9c12501..f4456c215a 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs @@ -64,6 +64,8 @@ namespace Jellyfin.Server.Migrations.Routines using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}")) { using var userDbConnection = new SqliteConnection($"Filename={Path.Combine(dataPath, "users.db")}"); + connection.Open(); + userDbConnection.Open(); _logger.LogWarning("Migrating the activity database may take a while, do not stop Jellyfin."); using var dbContext = _provider.CreateDbContext(); diff --git a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs index 63cbe49503..c845beef2f 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs @@ -58,6 +58,7 @@ namespace Jellyfin.Server.Migrations.Routines var dataPath = _appPaths.DataPath; using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}")) { + connection.Open(); using var dbContext = _dbProvider.CreateDbContext(); var authenticatedDevices = connection.Query("SELECT * FROM Tokens"); diff --git a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs index 06eda329cb..996f3fe8a6 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs @@ -66,7 +66,8 @@ namespace Jellyfin.Server.Migrations.Routines // Migrate parental rating strings to new levels _logger.LogInformation("Recalculating parental rating levels based on rating string."); - using (var connection = new SqliteConnection($"Filename={dbPath}")) + using var connection = new SqliteConnection($"Filename={dbPath}"); + connection.Open(); using (var transaction = connection.BeginTransaction()) { var queryResult = connection.Query("SELECT DISTINCT OfficialRating FROM TypedBaseItems"); diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs index 9cf888d62b..4fee88b68c 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs @@ -66,6 +66,7 @@ namespace Jellyfin.Server.Migrations.Routines using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}")) { + connection.Open(); var dbContext = _provider.CreateDbContext(); var queryResult = connection.Query("SELECT * FROM LocalUsersv2"); diff --git a/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs b/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs index 6c34e1f5bb..7b0d9456dc 100644 --- a/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs +++ b/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs @@ -38,7 +38,8 @@ namespace Jellyfin.Server.Migrations.Routines { var dataPath = _paths.DataPath; var dbPath = Path.Combine(dataPath, DbFilename); - using (var connection = new SqliteConnection($"Filename={dbPath}")) + using var connection = new SqliteConnection($"Filename={dbPath}"); + connection.Open(); using (var transaction = connection.BeginTransaction()) { // Query the database for the ids of duplicate extras From 4fe641b55dd3157c5c56c0223a8a525b850f31da Mon Sep 17 00:00:00 2001 From: cvium <clausvium@gmail.com> Date: Sat, 16 Sep 2023 07:27:22 +0200 Subject: [PATCH 575/858] missed a spot --- Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs | 3 ++- .../Migrations/Routines/MigrateDisplayPreferencesDb.cs | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs index f4456c215a..2f23cb1f8f 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs @@ -63,8 +63,9 @@ namespace Jellyfin.Server.Migrations.Routines var dataPath = _paths.DataPath; using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}")) { - using var userDbConnection = new SqliteConnection($"Filename={Path.Combine(dataPath, "users.db")}"); connection.Open(); + + using var userDbConnection = new SqliteConnection($"Filename={Path.Combine(dataPath, "users.db")}"); userDbConnection.Open(); _logger.LogWarning("Migrating the activity database may take a while, do not stop Jellyfin."); using var dbContext = _provider.CreateDbContext(); diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs index a036fe9bb5..249b39ae4b 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs @@ -86,6 +86,7 @@ namespace Jellyfin.Server.Migrations.Routines var dbFilePath = Path.Combine(_paths.DataPath, DbFilename); using (var connection = new SqliteConnection($"Filename={dbFilePath}")) { + connection.Open(); using var dbContext = _provider.CreateDbContext(); var results = connection.Query("SELECT * FROM userdisplaypreferences"); From e985133b37a53293e0d3f7d45af75444bf3fb166 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 16 Sep 2023 05:41:01 +0000 Subject: [PATCH 576/858] chore(deps): update actions/checkout action to v4 --- .github/workflows/repo-bump-version.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/repo-bump-version.yaml b/.github/workflows/repo-bump-version.yaml index 351e24576f..75578536ff 100644 --- a/.github/workflows/repo-bump-version.yaml +++ b/.github/workflows/repo-bump-version.yaml @@ -33,7 +33,7 @@ jobs: yq-version: v4.9.8 - name: Checkout Repository - uses: actions/checkout@v2 + uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 with: ref: ${{ env.TAG_BRANCH }} @@ -66,7 +66,7 @@ jobs: NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }} steps: - name: Checkout Repository - uses: actions/checkout@v2 + uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 with: ref: ${{ env.TAG_BRANCH }} From 6e82fe3a83d800d02b8f3c097e69c3968140c119 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 16 Sep 2023 09:32:36 +0000 Subject: [PATCH 577/858] chore(deps): pin jitterbit/await-check-suites action to 292a541 --- .github/workflows/repo-bump-version.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/repo-bump-version.yaml b/.github/workflows/repo-bump-version.yaml index 75578536ff..1713b484db 100644 --- a/.github/workflows/repo-bump-version.yaml +++ b/.github/workflows/repo-bump-version.yaml @@ -21,7 +21,7 @@ jobs: TAG_BRANCH: ${{ github.event.release.target_commitish }} steps: - name: Wait for deploy checks to finish - uses: jitterbit/await-check-suites@v1 + uses: jitterbit/await-check-suites@292a541bb7618078395b2ce711a0d89cfb8a568a # v1 with: ref: ${{ env.TAG_BRANCH }} intervalSeconds: 60 From 1635d82345a035c007daffe980cc09de17984813 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20M=C3=BCller?= <github@lonebyte.de> Date: Sat, 16 Sep 2023 12:57:20 +0200 Subject: [PATCH 578/858] Remove workaround for codec capitalization This is not required anymore as Shaka Player now supports the correct codec strings. --- Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 40 +----------------------- 1 file changed, 1 insertion(+), 39 deletions(-) diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index 03b723ef1b..276a09f41e 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -198,13 +198,6 @@ public class DynamicHlsHelper var basicPlaylist = AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup); - // Provide a workaround for alternative codec string capitalization. - var alternativeCodecCapitalizationPlaylist = ApplyCodecCapitalizationWorkaround(state, basicPlaylist.ToString()); - if (!string.IsNullOrEmpty(alternativeCodecCapitalizationPlaylist)) - { - builder.Append(alternativeCodecCapitalizationPlaylist); - } - if (state.VideoStream is not null && state.VideoRequest is not null) { var encodingOptions = _serverConfigurationManager.GetEncodingOptions(); @@ -236,14 +229,7 @@ public class DynamicHlsHelper } var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate; - var sdrPlaylist = AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup); - - // Provide a workaround for alternative codec string capitalization. - alternativeCodecCapitalizationPlaylist = ApplyCodecCapitalizationWorkaround(state, sdrPlaylist.ToString()); - if (!string.IsNullOrEmpty(alternativeCodecCapitalizationPlaylist)) - { - builder.Append(alternativeCodecCapitalizationPlaylist); - } + AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup); // Restore the video codec state.OutputVideoCodec = "copy"; @@ -274,13 +260,6 @@ public class DynamicHlsHelper state.VideoStream.Level = originalLevel; var newPlaylist = ReplacePlaylistCodecsField(basicPlaylist, playlistCodecsField, newPlaylistCodecsField); builder.Append(newPlaylist); - - // Provide a workaround for alternative codec string capitalization. - alternativeCodecCapitalizationPlaylist = ApplyCodecCapitalizationWorkaround(state, newPlaylist); - if (!string.IsNullOrEmpty(alternativeCodecCapitalizationPlaylist)) - { - builder.Append(alternativeCodecCapitalizationPlaylist); - } } } @@ -767,21 +746,4 @@ public class DynamicHlsHelper newValue.ToString(), StringComparison.Ordinal); } - - private string ApplyCodecCapitalizationWorkaround(StreamState state, string srcPlaylist) - { - if (!string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)) - { - return string.Empty; - } - - var newPlaylist = srcPlaylist; - - newPlaylist = newPlaylist.Replace(",fLaC\"", ",flac\"", StringComparison.Ordinal); - newPlaylist = newPlaylist.Replace("\"fLaC\"", "\"flac\"", StringComparison.Ordinal); - newPlaylist = newPlaylist.Replace(",Opus\"", ",opus\"", StringComparison.Ordinal); - newPlaylist = newPlaylist.Replace("\"Opus\"", "\"opus\"", StringComparison.Ordinal); - - return string.Equals(srcPlaylist, newPlaylist, StringComparison.Ordinal) ? string.Empty : newPlaylist; - } } From f7720e7c99e0e0f84f6492670dc1bcf83fa0f2fb Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Sat, 16 Sep 2023 15:08:45 +0200 Subject: [PATCH 579/858] Extend collections cleanup task to include playlists too --- .../Playlists/PlaylistManager.cs | 5 + .../CleanupCollectionAndPlaylistPathsTask.cs | 147 ++++++++++++++++++ .../Tasks/CleanupCollectionPathsTask.cs | 119 -------------- .../Playlists/IPlaylistManager.cs | 6 + 4 files changed, 158 insertions(+), 119 deletions(-) create mode 100644 Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs delete mode 100644 Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 702f8d45bc..0cb450cdf3 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -518,6 +518,11 @@ namespace Emby.Server.Implementations.Playlists return relativePath; } + public Folder GetPlaylistsFolder() + { + return GetPlaylistsFolder(Guid.Empty); + } + public Folder GetPlaylistsFolder(Guid userId) { const string TypeName = "PlaylistsFolder"; diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs new file mode 100644 index 0000000000..3daa2b6d0a --- /dev/null +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Collections; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Playlists; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Tasks; +using Microsoft.Extensions.Logging; + +namespace Emby.Server.Implementations.ScheduledTasks.Tasks; + +/// <summary> +/// Deletes path references from collections and playlists that no longer exists. +/// </summary> +public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask +{ + private readonly ILocalizationManager _localization; + private readonly ICollectionManager _collectionManager; + private readonly IPlaylistManager _playlistManager; + private readonly ILogger<CleanupCollectionAndPlaylistPathsTask> _logger; + private readonly IProviderManager _providerManager; + private readonly IFileSystem _fileSystem; + + /// <summary> + /// Initializes a new instance of the <see cref="CleanupCollectionAndPlaylistPathsTask"/> class. + /// </summary> + /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param> + /// <param name="collectionManager">Instance of the <see cref="ICollectionManager"/> interface.</param> + /// <param name="playlistManager">Instance of the <see cref="IPlaylistManager"/> interface.</param> + /// <param name="logger">The logger.</param> + /// <param name="providerManager">The provider manager.</param> + /// <param name="fileSystem">The filesystem.</param> + public CleanupCollectionAndPlaylistPathsTask( + ILocalizationManager localization, + ICollectionManager collectionManager, + IPlaylistManager playlistManager, + ILogger<CleanupCollectionAndPlaylistPathsTask> logger, + IProviderManager providerManager, + IFileSystem fileSystem) + { + _localization = localization; + _collectionManager = collectionManager; + _playlistManager = playlistManager; + _logger = logger; + _providerManager = providerManager; + _fileSystem = fileSystem; + } + + /// <inheritdoc /> + public string Name => _localization.GetLocalizedString("TaskCleanCollectionsAndPlaylists"); + + /// <inheritdoc /> + public string Key => "CleanCollectionsAndPlaylists"; + + /// <inheritdoc /> + public string Description => _localization.GetLocalizedString("TaskCleanCollectionsAndPlaylistsDescription"); + + /// <inheritdoc /> + public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory"); + + /// <inheritdoc /> + public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) + { + var collectionsFolder = await _collectionManager.GetCollectionsFolder(false).ConfigureAwait(false); + if (collectionsFolder is null) + { + _logger.LogDebug("There is no collection folder to be found"); + } + else + { + var collections = collectionsFolder.Children.OfType<BoxSet>().ToArray(); + _logger.LogDebug("Found {CollectionLength} boxsets", collections.Length); + + for (var index = 0; index < collections.Length; index++) + { + var collection = collections[index]; + _logger.LogDebug("Checking boxset {CollectionName}", collection.Name); + + CleanupLinkedChildren(collection, cancellationToken); + progress.Report(50D / collections.Length * (index + 1)); + } + } + + var playlistsFolder = _playlistManager.GetPlaylistsFolder(); + if (playlistsFolder is null) + { + _logger.LogDebug("There is no collection folder to be found"); + return; + } + + var playlists = playlistsFolder.Children.OfType<Playlist>().ToArray(); + _logger.LogDebug("Found {PlaylistLength} boxsets", playlists.Length); + + for (var index = 0; index < playlists.Length; index++) + { + var playlist = playlists[index]; + _logger.LogDebug("Checking playlist {PlaylistName}", playlist.Name); + + CleanupLinkedChildren(playlist, cancellationToken); + progress.Report(50D / playlists.Length * (index + 1)); + } + } + + private void CleanupLinkedChildren<T>(T folder, CancellationToken cancellationToken) where T : Folder + { + var itemsToRemove = new List<LinkedChild>(); + foreach (var linkedChild in folder.LinkedChildren) + { + if (!File.Exists(folder.Path)) + { + _logger.LogInformation("Item in {FolderName} cannot be found at {ItemPath}", folder.Name, linkedChild.Path); + itemsToRemove.Add(linkedChild); + } + } + + if (itemsToRemove.Count != 0) + { + _logger.LogDebug("Updating {FolderName}", folder.Name); + folder.LinkedChildren = folder.LinkedChildren.Except(itemsToRemove).ToArray(); + folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken); + + _providerManager.QueueRefresh( + folder.Id, + new MetadataRefreshOptions(new DirectoryService(_fileSystem)) + { + ForceSave = true + }, + RefreshPriority.High); + + itemsToRemove.Clear(); + } + } + + /// <inheritdoc /> + public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() + { + return new[] { new TaskTriggerInfo() { Type = TaskTriggerInfo.TriggerStartup } }; + } +} diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs deleted file mode 100644 index f78fc6f970..0000000000 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs +++ /dev/null @@ -1,119 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Collections; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Movies; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Tasks; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.ScheduledTasks.Tasks; - -/// <summary> -/// Deletes Path references from collections that no longer exists. -/// </summary> -public class CleanupCollectionPathsTask : IScheduledTask -{ - private readonly ILocalizationManager _localization; - private readonly ICollectionManager _collectionManager; - private readonly ILogger<CleanupCollectionPathsTask> _logger; - private readonly IProviderManager _providerManager; - private readonly IFileSystem _fileSystem; - - /// <summary> - /// Initializes a new instance of the <see cref="CleanupCollectionPathsTask"/> class. - /// </summary> - /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param> - /// <param name="collectionManager">Instance of the <see cref="ICollectionManager"/> interface.</param> - /// <param name="logger">The logger.</param> - /// <param name="providerManager">The provider manager.</param> - /// <param name="fileSystem">The filesystem.</param> - public CleanupCollectionPathsTask( - ILocalizationManager localization, - ICollectionManager collectionManager, - ILogger<CleanupCollectionPathsTask> logger, - IProviderManager providerManager, - IFileSystem fileSystem) - { - _localization = localization; - _collectionManager = collectionManager; - _logger = logger; - _providerManager = providerManager; - _fileSystem = fileSystem; - } - - /// <inheritdoc /> - public string Name => _localization.GetLocalizedString("TaskCleanCollections"); - - /// <inheritdoc /> - public string Key => "CleanCollections"; - - /// <inheritdoc /> - public string Description => _localization.GetLocalizedString("TaskCleanCollectionsDescription"); - - /// <inheritdoc /> - public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory"); - - /// <inheritdoc /> - public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) - { - var collectionsFolder = await _collectionManager.GetCollectionsFolder(false).ConfigureAwait(false); - if (collectionsFolder is null) - { - _logger.LogDebug("There is no collection folder to be found"); - return; - } - - var collections = collectionsFolder.Children.OfType<BoxSet>().ToArray(); - _logger.LogDebug("Found {CollectionLength} Boxsets", collections.Length); - - var itemsToRemove = new List<LinkedChild>(); - for (var index = 0; index < collections.Length; index++) - { - var collection = collections[index]; - _logger.LogDebug("Check Boxset {CollectionName}", collection.Name); - - foreach (var collectionLinkedChild in collection.LinkedChildren) - { - if (!File.Exists(collectionLinkedChild.Path)) - { - _logger.LogInformation("Item in boxset {CollectionName} cannot be found at {ItemPath}", collection.Name, collectionLinkedChild.Path); - itemsToRemove.Add(collectionLinkedChild); - } - } - - if (itemsToRemove.Count != 0) - { - _logger.LogDebug("Update Boxset {CollectionName}", collection.Name); - collection.LinkedChildren = collection.LinkedChildren.Except(itemsToRemove).ToArray(); - await collection.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken) - .ConfigureAwait(false); - - _providerManager.QueueRefresh( - collection.Id, - new MetadataRefreshOptions(new DirectoryService(_fileSystem)) - { - ForceSave = true - }, - RefreshPriority.High); - - itemsToRemove.Clear(); - } - - progress.Report(100D / collections.Length * (index + 1)); - } - } - - /// <inheritdoc /> - public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() - { - return new[] { new TaskTriggerInfo() { Type = TaskTriggerInfo.TriggerStartup } }; - } -} diff --git a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs index d1a51c2cf6..bb68a3b6dd 100644 --- a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs +++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs @@ -44,6 +44,12 @@ namespace MediaBrowser.Controller.Playlists /// <summary> /// Gets the playlists folder. /// </summary> + /// <returns>Folder.</returns> + Folder GetPlaylistsFolder(); + + /// <summary> + /// Gets the playlists folder for a user. + /// </summary> /// <param name="userId">The user identifier.</param> /// <returns>Folder.</returns> Folder GetPlaylistsFolder(Guid userId); From 783bb8a8cbd2b105e64d5e33891fd754c5c83182 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Sat, 16 Sep 2023 17:05:54 +0200 Subject: [PATCH 580/858] Apply review suggestions --- .../Tasks/CleanupCollectionAndPlaylistPathsTask.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs index 3daa2b6d0a..f30a26863d 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs @@ -72,7 +72,7 @@ public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask var collectionsFolder = await _collectionManager.GetCollectionsFolder(false).ConfigureAwait(false); if (collectionsFolder is null) { - _logger.LogDebug("There is no collection folder to be found"); + _logger.LogDebug("There is no collections folder to be found"); } else { @@ -92,12 +92,12 @@ public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask var playlistsFolder = _playlistManager.GetPlaylistsFolder(); if (playlistsFolder is null) { - _logger.LogDebug("There is no collection folder to be found"); + _logger.LogDebug("There is no playlists folder to be found"); return; } var playlists = playlistsFolder.Children.OfType<Playlist>().ToArray(); - _logger.LogDebug("Found {PlaylistLength} boxsets", playlists.Length); + _logger.LogDebug("Found {PlaylistLength} playlists", playlists.Length); for (var index = 0; index < playlists.Length; index++) { @@ -109,7 +109,8 @@ public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask } } - private void CleanupLinkedChildren<T>(T folder, CancellationToken cancellationToken) where T : Folder + private void CleanupLinkedChildren<T>(T folder, CancellationToken cancellationToken) + where T : Folder { var itemsToRemove = new List<LinkedChild>(); foreach (var linkedChild in folder.LinkedChildren) From db83bed9dae12d9282b0754139232a0523003b9f Mon Sep 17 00:00:00 2001 From: Tycho Brouwer <tychobrouwer33@gmail.com> Date: Fri, 15 Sep 2023 22:43:44 +0000 Subject: [PATCH 581/858] Translated using Weblate (Dutch) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/nl/ --- Emby.Server.Implementations/Localization/Core/nl.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index 4eb00d2896..ac7b92de60 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -1,9 +1,9 @@ { "Albums": "Albums", "AppDeviceValues": "App: {0}, Apparaat: {1}", - "Application": "Toepassing", + "Application": "Applicatie", "Artists": "Artiesten", - "AuthenticationSucceededWithUserName": "{0} is succesvol geauthenticeerd", + "AuthenticationSucceededWithUserName": "{0} succesvol geauthenticeerd", "Books": "Boeken", "CameraImageUploadedFrom": "Nieuwe camera-afbeelding toegevoegd vanaf {0}", "Channels": "Kanalen", From fccea4625d29feb1b91345c6b944e5dc6f062a85 Mon Sep 17 00:00:00 2001 From: Tim Eisele <Shadowghost@users.noreply.github.com> Date: Sun, 17 Sep 2023 14:13:19 +0200 Subject: [PATCH 582/858] Update Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs Co-authored-by: Bond-009 <bond.009@outlook.com> --- .../Tasks/CleanupCollectionAndPlaylistPathsTask.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs index f30a26863d..e5af22b319 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs @@ -112,17 +112,17 @@ public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask private void CleanupLinkedChildren<T>(T folder, CancellationToken cancellationToken) where T : Folder { - var itemsToRemove = new List<LinkedChild>(); + List<LinkedChild> itemsToRemove = null; foreach (var linkedChild in folder.LinkedChildren) { if (!File.Exists(folder.Path)) { _logger.LogInformation("Item in {FolderName} cannot be found at {ItemPath}", folder.Name, linkedChild.Path); - itemsToRemove.Add(linkedChild); + (itemsToRemove ??= new List<LinkedChild>()).Add(linkedChild); } } - if (itemsToRemove.Count != 0) + if (itemsToRemove is not null) { _logger.LogDebug("Updating {FolderName}", folder.Name); folder.LinkedChildren = folder.LinkedChildren.Except(itemsToRemove).ToArray(); From 61a49e94c4753eb8dbc421530656962f4a7a33c8 Mon Sep 17 00:00:00 2001 From: Tim Eisele <Shadowghost@users.noreply.github.com> Date: Sun, 17 Sep 2023 14:13:25 +0200 Subject: [PATCH 583/858] Update Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs Co-authored-by: Bond-009 <bond.009@outlook.com> --- .../Tasks/CleanupCollectionAndPlaylistPathsTask.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs index e5af22b319..91f3c435c1 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs @@ -136,7 +136,6 @@ public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask }, RefreshPriority.High); - itemsToRemove.Clear(); } } From bce45992d9fa5fd121cbc35af00d2512dbe912fa Mon Sep 17 00:00:00 2001 From: Tim Eisele <Shadowghost@users.noreply.github.com> Date: Sun, 17 Sep 2023 16:35:41 +0200 Subject: [PATCH 584/858] Update Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs Co-authored-by: Bond-009 <bond.009@outlook.com> --- .../Tasks/CleanupCollectionAndPlaylistPathsTask.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs index 91f3c435c1..ef2d1942a5 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs @@ -112,7 +112,7 @@ public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask private void CleanupLinkedChildren<T>(T folder, CancellationToken cancellationToken) where T : Folder { - List<LinkedChild> itemsToRemove = null; + List<LinkedChild>? itemsToRemove = null; foreach (var linkedChild in folder.LinkedChildren) { if (!File.Exists(folder.Path)) From 5669955aca4e7f187c0849095b35a5f93325d812 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 18 Sep 2023 02:22:16 +0000 Subject: [PATCH 585/858] chore(deps): update xunit-dotnet monorepo to v2.5.1 --- Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 2341b824fd..d7b424d3fe 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -85,8 +85,8 @@ <PackageVersion Include="TMDbLib" Version="2.0.0" /> <PackageVersion Include="UTF.Unknown" Version="2.5.1" /> <PackageVersion Include="Xunit.Priority" Version="1.1.6" /> - <PackageVersion Include="xunit.runner.visualstudio" Version="2.5.0" /> + <PackageVersion Include="xunit.runner.visualstudio" Version="2.5.1" /> <PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" /> - <PackageVersion Include="xunit" Version="2.5.0" /> + <PackageVersion Include="xunit" Version="2.5.1" /> </ItemGroup> </Project> From 043fc387e08d5828864c8dcf4f5bcc2a64ecf761 Mon Sep 17 00:00:00 2001 From: atomicmind <arnold.marko@gmail.com> Date: Sun, 17 Sep 2023 10:54:58 +0000 Subject: [PATCH 586/858] Translated using Weblate (Slovenian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/sl/ --- Emby.Server.Implementations/Localization/Core/sl-SI.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/sl-SI.json b/Emby.Server.Implementations/Localization/Core/sl-SI.json index 4c23f71efa..1944e072cb 100644 --- a/Emby.Server.Implementations/Localization/Core/sl-SI.json +++ b/Emby.Server.Implementations/Localization/Core/sl-SI.json @@ -11,7 +11,7 @@ "Collections": "Zbirke", "DeviceOfflineWithName": "{0} je prekinil povezavo", "DeviceOnlineWithName": "{0} je povezan", - "FailedLoginAttemptWithUserName": "Neuspešen poskus prijave iz {0}", + "FailedLoginAttemptWithUserName": "Neuspešen poskus prijave z {0}", "Favorites": "Priljubljeno", "Folders": "Mape", "Genres": "Zvrsti", From 03b6adf068f007a66abbda515be940e702849d6b Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Mon, 18 Sep 2023 17:55:52 +0200 Subject: [PATCH 587/858] Fix xUnit1030: Do not call ConfigureAwait in test method --- .../Subtitles/SubtitleEncoderTests.cs | 2 +- .../Manager/ProviderManagerTests.cs | 4 +- .../LiveTv/HdHomerunHostTests.cs | 10 ++-- .../Updates/InstallationManagerTests.cs | 4 +- .../AuthHelper.cs | 22 ++++---- .../Controllers/ActivityLogControllerTests.cs | 4 +- .../Controllers/DashboardControllerTests.cs | 16 +++--- .../Controllers/DlnaControllerTests.cs | 44 +++++++-------- .../Controllers/ItemsControllerTests.cs | 18 +++--- .../Controllers/LibraryControllerTests.cs | 10 ++-- .../Controllers/MediaInfoControllerTests.cs | 12 ++-- .../MediaStructureControllerTests.cs | 28 +++++----- .../Controllers/MusicGenreControllerTests.cs | 4 +- .../Controllers/PlaystateControllerTests.cs | 20 +++---- .../Controllers/SessionControllerTests.cs | 4 +- .../Controllers/StartupControllerTests.cs | 26 ++++----- .../Controllers/UserControllerTests.cs | 30 +++++----- .../Controllers/UserLibraryControllerTests.cs | 56 +++++++++---------- .../Controllers/VideosControllerTests.cs | 4 +- .../EncodedQueryStringTest.cs | 8 +-- .../RobotsRedirectionMiddlewareTests.cs | 2 +- 21 files changed, 164 insertions(+), 164 deletions(-) diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs index 9ace80bbd2..ce1f005f40 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs @@ -97,7 +97,7 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests { var fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true }); var subtitleEncoder = fixture.Create<SubtitleEncoder>(); - var result = await subtitleEncoder.GetReadableFile(mediaSource, subtitleStream, CancellationToken.None).ConfigureAwait(false); + var result = await subtitleEncoder.GetReadableFile(mediaSource, subtitleStream, CancellationToken.None); Assert.Equal(subtitleInfo.Path, result.Path); Assert.Equal(subtitleInfo.Protocol, result.Protocol); Assert.Equal(subtitleInfo.Format, result.Format); diff --git a/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs b/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs index 400e30bd63..1e0851993b 100644 --- a/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs +++ b/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs @@ -82,7 +82,7 @@ namespace Jellyfin.Providers.Tests.Manager AddParts(providerManager, metadataServices: servicesList.Select(s => s.Object).ToArray()); var refreshOptions = new MetadataRefreshOptions(Mock.Of<IDirectoryService>(MockBehavior.Strict)); - var actual = await providerManager.RefreshSingleItem(item, refreshOptions, CancellationToken.None).ConfigureAwait(false); + var actual = await providerManager.RefreshSingleItem(item, refreshOptions, CancellationToken.None); Assert.Equal(ItemUpdateType.MetadataDownload, actual); for (var i = 0; i < servicesList.Length; i++) @@ -105,7 +105,7 @@ namespace Jellyfin.Providers.Tests.Manager AddParts(providerManager, metadataServices: servicesList.Select(s => s.Object).ToArray()); var refreshOptions = new MetadataRefreshOptions(Mock.Of<IDirectoryService>(MockBehavior.Strict)); - var actual = await providerManager.RefreshSingleItem(item, refreshOptions, CancellationToken.None).ConfigureAwait(false); + var actual = await providerManager.RefreshSingleItem(item, refreshOptions, CancellationToken.None); var expectedResult = serviceFound ? ItemUpdateType.MetadataDownload : ItemUpdateType.None; Assert.Equal(expectedResult, actual); diff --git a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunHostTests.cs b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunHostTests.cs index c859d11c69..13ac3ddb0f 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunHostTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunHostTests.cs @@ -52,7 +52,7 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv Url = "192.168.1.182" }; - var modelInfo = await _hdHomerunHost.GetModelInfo(host, true, CancellationToken.None).ConfigureAwait(false); + var modelInfo = await _hdHomerunHost.GetModelInfo(host, true, CancellationToken.None); Assert.Equal("HDHomeRun PRIME", modelInfo.FriendlyName); Assert.Equal("HDHR3-CC", modelInfo.ModelNumber); Assert.Equal("hdhomerun3_cablecard", modelInfo.FirmwareName); @@ -72,7 +72,7 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv Url = "10.10.10.100" }; - var modelInfo = await _hdHomerunHost.GetModelInfo(host, true, CancellationToken.None).ConfigureAwait(false); + var modelInfo = await _hdHomerunHost.GetModelInfo(host, true, CancellationToken.None); Assert.Equal("HDHomeRun DUAL", modelInfo.FriendlyName); Assert.Equal("HDHR3-US", modelInfo.ModelNumber); Assert.Equal("hdhomerun3_atsc", modelInfo.FirmwareName); @@ -103,7 +103,7 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv Url = "192.168.1.182" }; - var channels = await _hdHomerunHost.GetLineup(host, CancellationToken.None).ConfigureAwait(false); + var channels = await _hdHomerunHost.GetLineup(host, CancellationToken.None); Assert.Equal(6, channels.Count); Assert.Equal("4.1", channels[0].GuideNumber); Assert.Equal("WCMH-DT", channels[0].GuideName); @@ -133,7 +133,7 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv ImportFavoritesOnly = true }; - var channels = await _hdHomerunHost.GetLineup(host, CancellationToken.None).ConfigureAwait(false); + var channels = await _hdHomerunHost.GetLineup(host, CancellationToken.None); Assert.Single(channels); Assert.Equal("4.1", channels[0].GuideNumber); Assert.Equal("WCMH-DT", channels[0].GuideName); @@ -145,7 +145,7 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv [Fact] public async Task TryGetTunerHostInfo_Valid_Success() { - var host = await _hdHomerunHost.TryGetTunerHostInfo("192.168.1.182", CancellationToken.None).ConfigureAwait(false); + var host = await _hdHomerunHost.TryGetTunerHostInfo("192.168.1.182", CancellationToken.None); Assert.Equal(_hdHomerunHost.Type, host.Type); Assert.Equal("192.168.1.182", host.Url); Assert.Equal("HDHomeRun PRIME", host.FriendlyName); diff --git a/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs index 7abd2e685f..5caf7d124e 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs @@ -90,7 +90,7 @@ namespace Jellyfin.Server.Implementations.Tests.Updates Checksum = "InvalidChecksum" }; - await Assert.ThrowsAsync<InvalidDataException>(() => _installationManager.InstallPackage(packageInfo, CancellationToken.None)).ConfigureAwait(false); + await Assert.ThrowsAsync<InvalidDataException>(() => _installationManager.InstallPackage(packageInfo, CancellationToken.None)); } [Fact] @@ -103,7 +103,7 @@ namespace Jellyfin.Server.Implementations.Tests.Updates Checksum = "11b5b2f1a9ebc4f66d6ef19018543361" }; - var ex = await Record.ExceptionAsync(() => _installationManager.InstallPackage(packageInfo, CancellationToken.None)).ConfigureAwait(false); + var ex = await Record.ExceptionAsync(() => _installationManager.InstallPackage(packageInfo, CancellationToken.None)); Assert.Null(ex); } } diff --git a/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs b/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs index 3737fee0ac..3dc62afaf8 100644 --- a/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs +++ b/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs @@ -21,10 +21,10 @@ namespace Jellyfin.Server.Integration.Tests public static async Task<string> CompleteStartupAsync(HttpClient client) { var jsonOptions = JsonDefaults.Options; - var userResponse = await client.GetByteArrayAsync("/Startup/User").ConfigureAwait(false); + var userResponse = await client.GetByteArrayAsync("/Startup/User"); var user = JsonSerializer.Deserialize<StartupUserDto>(userResponse, jsonOptions); - using var completeResponse = await client.PostAsync("/Startup/Complete", new ByteArrayContent(Array.Empty<byte>())).ConfigureAwait(false); + using var completeResponse = await client.PostAsync("/Startup/Complete", new ByteArrayContent(Array.Empty<byte>())); Assert.Equal(HttpStatusCode.NoContent, completeResponse.StatusCode); using var content = JsonContent.Create( @@ -36,20 +36,20 @@ namespace Jellyfin.Server.Integration.Tests options: jsonOptions); content.Headers.Add("X-Emby-Authorization", DummyAuthHeader); - using var authResponse = await client.PostAsync("/Users/AuthenticateByName", content).ConfigureAwait(false); + using var authResponse = await client.PostAsync("/Users/AuthenticateByName", content); var auth = await JsonSerializer.DeserializeAsync<AuthenticationResultDto>( - await authResponse.Content.ReadAsStreamAsync().ConfigureAwait(false), - jsonOptions).ConfigureAwait(false); + await authResponse.Content.ReadAsStreamAsync(), + jsonOptions); return auth!.AccessToken; } public static async Task<UserDto> GetUserDtoAsync(HttpClient client) { - using var response = await client.GetAsync("Users/Me").ConfigureAwait(false); + using var response = await client.GetAsync("Users/Me"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var userDto = await JsonSerializer.DeserializeAsync<UserDto>( - await response.Content.ReadAsStreamAsync().ConfigureAwait(false), JsonDefaults.Options).ConfigureAwait(false); + await response.Content.ReadAsStreamAsync(), JsonDefaults.Options); Assert.NotNull(userDto); return userDto; } @@ -58,15 +58,15 @@ namespace Jellyfin.Server.Integration.Tests { if (userId.Equals(default)) { - var userDto = await GetUserDtoAsync(client).ConfigureAwait(false); + var userDto = await GetUserDtoAsync(client); userId = userDto.Id; } - var response = await client.GetAsync($"Users/{userId}/Items/Root").ConfigureAwait(false); + var response = await client.GetAsync($"Users/{userId}/Items/Root"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var rootDto = await JsonSerializer.DeserializeAsync<BaseItemDto>( - await response.Content.ReadAsStreamAsync().ConfigureAwait(false), - JsonDefaults.Options).ConfigureAwait(false); + await response.Content.ReadAsStreamAsync(), + JsonDefaults.Options); Assert.NotNull(rootDto); return rootDto; } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/ActivityLogControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/ActivityLogControllerTests.cs index be89fbc9a9..96ca96558d 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/ActivityLogControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/ActivityLogControllerTests.cs @@ -19,9 +19,9 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task ActivityLog_GetEntries_Ok() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.GetAsync("System/ActivityLog/Entries").ConfigureAwait(false); + var response = await client.GetAsync("System/ActivityLog/Entries"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType); diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs index 52df1cd603..bd6e1b690a 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs @@ -26,7 +26,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers { var client = _factory.CreateClient(); - var response = await client.GetAsync("web/ConfigurationPage?name=ThisPageDoesntExists").ConfigureAwait(false); + var response = await client.GetAsync("web/ConfigurationPage?name=ThisPageDoesntExists"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -36,12 +36,12 @@ namespace Jellyfin.Server.Integration.Tests.Controllers { var client = _factory.CreateClient(); - var response = await client.GetAsync("/web/ConfigurationPage?name=TestPlugin").ConfigureAwait(false); + var response = await client.GetAsync("/web/ConfigurationPage?name=TestPlugin"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(MediaTypeNames.Text.Html, response.Content.Headers.ContentType?.MediaType); StreamReader reader = new StreamReader(typeof(TestPlugin).Assembly.GetManifestResourceStream("Jellyfin.Server.Integration.Tests.TestPage.html")!); - Assert.Equal(await response.Content.ReadAsStringAsync().ConfigureAwait(false), await reader.ReadToEndAsync().ConfigureAwait(false)); + Assert.Equal(await response.Content.ReadAsStringAsync(), await reader.ReadToEndAsync()); } [Fact] @@ -49,7 +49,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers { var client = _factory.CreateClient(); - var response = await client.GetAsync("/web/ConfigurationPage?name=BrokenPage").ConfigureAwait(false); + var response = await client.GetAsync("/web/ConfigurationPage?name=BrokenPage"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -58,9 +58,9 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task GetConfigurationPages_NoParams_AllConfigurationPages() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.GetAsync("/web/ConfigurationPages").ConfigureAwait(false); + var response = await client.GetAsync("/web/ConfigurationPages"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -73,9 +73,9 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task GetConfigurationPages_True_MainMenuConfigurationPages() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.GetAsync("/web/ConfigurationPages?enableInMainMenu=true").ConfigureAwait(false); + var response = await client.GetAsync("/web/ConfigurationPages?enableInMainMenu=true"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType); diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/DlnaControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/DlnaControllerTests.cs index a65f65bb21..65e70caa02 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/DlnaControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/DlnaControllerTests.cs @@ -32,9 +32,9 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task GetProfile_DoesNotExist_NotFound() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - using var response = await client.GetAsync("/Dlna/Profiles/" + NonExistentProfile).ConfigureAwait(false); + using var response = await client.GetAsync("/Dlna/Profiles/" + NonExistentProfile); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -43,9 +43,9 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task DeleteProfile_DoesNotExist_NotFound() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - using var response = await client.DeleteAsync("/Dlna/Profiles/" + NonExistentProfile).ConfigureAwait(false); + using var response = await client.DeleteAsync("/Dlna/Profiles/" + NonExistentProfile); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -54,14 +54,14 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task UpdateProfile_DoesNotExist_NotFound() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); var deviceProfile = new DeviceProfile() { Name = "ThisProfileDoesNotExist" }; - using var response = await client.PostAsJsonAsync("/Dlna/Profiles/" + NonExistentProfile, deviceProfile, _jsonOptions).ConfigureAwait(false); + using var response = await client.PostAsJsonAsync("/Dlna/Profiles/" + NonExistentProfile, deviceProfile, _jsonOptions); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -70,14 +70,14 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task CreateProfile_Valid_NoContent() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); var deviceProfile = new DeviceProfile() { Name = "ThisProfileIsNew" }; - using var response = await client.PostAsJsonAsync("/Dlna/Profiles", deviceProfile, _jsonOptions).ConfigureAwait(false); + using var response = await client.PostAsJsonAsync("/Dlna/Profiles", deviceProfile, _jsonOptions); Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); } @@ -86,16 +86,16 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task GetProfileInfos_Valid_ContainsThisProfileIsNew() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - using var response = await client.GetAsync("/Dlna/ProfileInfos").ConfigureAwait(false); + using var response = await client.GetAsync("/Dlna/ProfileInfos"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType); Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet); var profiles = await JsonSerializer.DeserializeAsync<DeviceProfileInfo[]>( - await response.Content.ReadAsStreamAsync().ConfigureAwait(false), - _jsonOptions).ConfigureAwait(false); + await response.Content.ReadAsStreamAsync(), + _jsonOptions); var newProfile = profiles?.FirstOrDefault(x => string.Equals(x.Name, "ThisProfileIsNew", StringComparison.Ordinal)); Assert.NotNull(newProfile); @@ -107,7 +107,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task UpdateProfile_Valid_NoContent() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); var updatedProfile = new DeviceProfile() { @@ -115,18 +115,18 @@ namespace Jellyfin.Server.Integration.Tests.Controllers Id = _newDeviceProfileId }; - using var postResponse = await client.PostAsJsonAsync("/Dlna/Profiles/" + _newDeviceProfileId, updatedProfile, _jsonOptions).ConfigureAwait(false); + using var postResponse = await client.PostAsJsonAsync("/Dlna/Profiles/" + _newDeviceProfileId, updatedProfile, _jsonOptions); Assert.Equal(HttpStatusCode.NoContent, postResponse.StatusCode); // Verify that the profile got updated - using var response = await client.GetAsync("/Dlna/ProfileInfos").ConfigureAwait(false); + using var response = await client.GetAsync("/Dlna/ProfileInfos"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType); Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet); var profiles = await JsonSerializer.DeserializeAsync<DeviceProfileInfo[]>( - await response.Content.ReadAsStreamAsync().ConfigureAwait(false), - _jsonOptions).ConfigureAwait(false); + await response.Content.ReadAsStreamAsync(), + _jsonOptions); Assert.Null(profiles?.FirstOrDefault(x => string.Equals(x.Name, "ThisProfileIsNew", StringComparison.Ordinal))); var newProfile = profiles?.FirstOrDefault(x => string.Equals(x.Name, "ThisProfileIsUpdated", StringComparison.Ordinal)); @@ -139,20 +139,20 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task DeleteProfile_Valid_NoContent() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - using var deleteResponse = await client.DeleteAsync("/Dlna/Profiles/" + _newDeviceProfileId).ConfigureAwait(false); + using var deleteResponse = await client.DeleteAsync("/Dlna/Profiles/" + _newDeviceProfileId); Assert.Equal(HttpStatusCode.NoContent, deleteResponse.StatusCode); // Verify that the profile got deleted - using var response = await client.GetAsync("/Dlna/ProfileInfos").ConfigureAwait(false); + using var response = await client.GetAsync("/Dlna/ProfileInfos"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType); Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet); var profiles = await JsonSerializer.DeserializeAsync<DeviceProfileInfo[]>( - await response.Content.ReadAsStreamAsync().ConfigureAwait(false), - _jsonOptions).ConfigureAwait(false); + await response.Content.ReadAsStreamAsync(), + _jsonOptions); Assert.Null(profiles?.FirstOrDefault(x => string.Equals(x.Name, "ThisProfileIsUpdated", StringComparison.Ordinal))); } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs index 0780029949..a12e7ca0d8 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs @@ -25,9 +25,9 @@ public sealed class ItemsControllerTests : IClassFixture<JellyfinApplicationFact public async Task GetItems_NoApiKeyOrUserId_Success() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.GetAsync("Items").ConfigureAwait(false); + var response = await client.GetAsync("Items"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); } @@ -37,9 +37,9 @@ public sealed class ItemsControllerTests : IClassFixture<JellyfinApplicationFact public async Task GetUserItems_NonExistentUserId_NotFound(string format) { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid())).ConfigureAwait(false); + var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid())); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -50,15 +50,15 @@ public sealed class ItemsControllerTests : IClassFixture<JellyfinApplicationFact public async Task GetItems_UserId_Ok(string format) { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false); + var userDto = await AuthHelper.GetUserDtoAsync(client); - var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id)).ConfigureAwait(false); + var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id)); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var items = await JsonSerializer.DeserializeAsync<QueryResult<BaseItemDto>>( - await response.Content.ReadAsStreamAsync().ConfigureAwait(false), - _jsonOptions).ConfigureAwait(false); + await response.Content.ReadAsStreamAsync(), + _jsonOptions); Assert.NotNull(items); } } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs index 8998683a79..06abae14cf 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs @@ -32,9 +32,9 @@ public sealed class LibraryControllerTests : IClassFixture<JellyfinApplicationFa public async Task Get_NonExistentItemId_NotFound(string format) { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid())).ConfigureAwait(false); + var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid())); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -45,7 +45,7 @@ public sealed class LibraryControllerTests : IClassFixture<JellyfinApplicationFa { var client = _factory.CreateClient(); - var response = await client.DeleteAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid())).ConfigureAwait(false); + var response = await client.DeleteAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid())); Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); } @@ -55,9 +55,9 @@ public sealed class LibraryControllerTests : IClassFixture<JellyfinApplicationFa public async Task Delete_NonExistentItemId_NotFound(string format) { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.DeleteAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid())).ConfigureAwait(false); + var response = await client.DeleteAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid())); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaInfoControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaInfoControllerTests.cs index 34d26680ad..abc8b60099 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaInfoControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaInfoControllerTests.cs @@ -20,9 +20,9 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task BitrateTest_Default_Ok() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.GetAsync("Playback/BitrateTest").ConfigureAwait(false); + var response = await client.GetAsync("Playback/BitrateTest"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(MediaTypeNames.Application.Octet, response.Content.Headers.ContentType?.MediaType); @@ -34,9 +34,9 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task BitrateTest_WithValidParam_Ok(int size) { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.GetAsync("Playback/BitrateTest?size=" + size.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false); + var response = await client.GetAsync("Playback/BitrateTest?size=" + size.ToString(CultureInfo.InvariantCulture)); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(MediaTypeNames.Application.Octet, response.Content.Headers.ContentType?.MediaType); @@ -51,9 +51,9 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task BitrateTest_InvalidValue_BadRequest(int size) { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.GetAsync("Playback/BitrateTest?size=" + size.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false); + var response = await client.GetAsync("Playback/BitrateTest?size=" + size.ToString(CultureInfo.InvariantCulture)); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs index 24251013c5..6699c68346 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs @@ -26,10 +26,10 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task RenameVirtualFolder_WhiteSpaceName_ReturnsBadRequest() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); using var postContent = new ByteArrayContent(Array.Empty<byte>()); - var response = await client.PostAsync("Library/VirtualFolders/Name?name=+&newName=test", postContent).ConfigureAwait(false); + var response = await client.PostAsync("Library/VirtualFolders/Name?name=+&newName=test", postContent); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } @@ -38,10 +38,10 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task RenameVirtualFolder_WhiteSpaceNewName_ReturnsBadRequest() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); using var postContent = new ByteArrayContent(Array.Empty<byte>()); - var response = await client.PostAsync("Library/VirtualFolders/Name?name=test&newName=+", postContent).ConfigureAwait(false); + var response = await client.PostAsync("Library/VirtualFolders/Name?name=test&newName=+", postContent); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } @@ -50,10 +50,10 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task RenameVirtualFolder_NameDoesntExist_ReturnsNotFound() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); using var postContent = new ByteArrayContent(Array.Empty<byte>()); - var response = await client.PostAsync("Library/VirtualFolders/Name?name=doesnt+exist&newName=test", postContent).ConfigureAwait(false); + var response = await client.PostAsync("Library/VirtualFolders/Name?name=doesnt+exist&newName=test", postContent); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -62,7 +62,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task AddMediaPath_PathDoesntExist_ReturnsNotFound() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); var data = new MediaPathDto() { @@ -70,7 +70,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers Path = "/this/path/doesnt/exist" }; - var response = await client.PostAsJsonAsync("Library/VirtualFolders/Paths", data, _jsonOptions).ConfigureAwait(false); + var response = await client.PostAsJsonAsync("Library/VirtualFolders/Paths", data, _jsonOptions); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -79,7 +79,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task UpdateMediaPath_WhiteSpaceName_ReturnsBadRequest() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); var data = new UpdateMediaPathRequestDto() { @@ -87,7 +87,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers PathInfo = new MediaPathInfo("test") }; - var response = await client.PostAsJsonAsync("Library/VirtualFolders/Paths/Update", data, _jsonOptions).ConfigureAwait(false); + var response = await client.PostAsJsonAsync("Library/VirtualFolders/Paths/Update", data, _jsonOptions); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } @@ -96,9 +96,9 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task RemoveMediaPath_WhiteSpaceName_ReturnsBadRequest() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.DeleteAsync("Library/VirtualFolders/Paths?name=+").ConfigureAwait(false); + var response = await client.DeleteAsync("Library/VirtualFolders/Paths?name=+"); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } @@ -107,9 +107,9 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task RemoveMediaPath_PathDoesntExist_ReturnsNotFound() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.DeleteAsync("Library/VirtualFolders/Paths?name=none&path=%2Fthis%2Fpath%2Fdoesnt%2Fexist").ConfigureAwait(false); + var response = await client.DeleteAsync("Library/VirtualFolders/Paths?name=none&path=%2Fthis%2Fpath%2Fdoesnt%2Fexist"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/MusicGenreControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/MusicGenreControllerTests.cs index 17f3dc99fd..f9982cf12b 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/MusicGenreControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/MusicGenreControllerTests.cs @@ -18,9 +18,9 @@ public sealed class MusicGenreControllerTests : IClassFixture<JellyfinApplicatio public async Task MusicGenres_FakeMusicGenre_NotFound() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.GetAsync("MusicGenres/Fake-MusicGenre").ConfigureAwait(false); + var response = await client.GetAsync("MusicGenres/Fake-MusicGenre"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/PlaystateControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/PlaystateControllerTests.cs index 868ecd53f5..9554d3ebc6 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/PlaystateControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/PlaystateControllerTests.cs @@ -19,9 +19,9 @@ public class PlaystateControllerTests : IClassFixture<JellyfinApplicationFactory public async Task DeleteMarkUnplayedItem_NonExistentUserId_NotFound() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - using var response = await client.DeleteAsync($"Users/{Guid.NewGuid()}/PlayedItems/{Guid.NewGuid()}").ConfigureAwait(false); + using var response = await client.DeleteAsync($"Users/{Guid.NewGuid()}/PlayedItems/{Guid.NewGuid()}"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -29,9 +29,9 @@ public class PlaystateControllerTests : IClassFixture<JellyfinApplicationFactory public async Task PostMarkPlayedItem_NonExistentUserId_NotFound() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - using var response = await client.PostAsync($"Users/{Guid.NewGuid()}/PlayedItems/{Guid.NewGuid()}", null).ConfigureAwait(false); + using var response = await client.PostAsync($"Users/{Guid.NewGuid()}/PlayedItems/{Guid.NewGuid()}", null); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -39,11 +39,11 @@ public class PlaystateControllerTests : IClassFixture<JellyfinApplicationFactory public async Task DeleteMarkUnplayedItem_NonExistentItemId_NotFound() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false); + var userDto = await AuthHelper.GetUserDtoAsync(client); - using var response = await client.DeleteAsync($"Users/{userDto.Id}/PlayedItems/{Guid.NewGuid()}").ConfigureAwait(false); + using var response = await client.DeleteAsync($"Users/{userDto.Id}/PlayedItems/{Guid.NewGuid()}"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -51,11 +51,11 @@ public class PlaystateControllerTests : IClassFixture<JellyfinApplicationFactory public async Task PostMarkPlayedItem_NonExistentItemId_NotFound() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false); + var userDto = await AuthHelper.GetUserDtoAsync(client); - using var response = await client.PostAsync($"Users/{userDto.Id}/PlayedItems/{Guid.NewGuid()}", null).ConfigureAwait(false); + using var response = await client.PostAsync($"Users/{userDto.Id}/PlayedItems/{Guid.NewGuid()}", null); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/SessionControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/SessionControllerTests.cs index cb0a829e8e..b9def13f8a 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/SessionControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/SessionControllerTests.cs @@ -19,9 +19,9 @@ public class SessionControllerTests : IClassFixture<JellyfinApplicationFactory> public async Task GetSessions_NonExistentUserId_NotFound() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - using var response = await client.GetAsync($"Session/Sessions?userId={Guid.NewGuid()}").ConfigureAwait(false); + using var response = await client.GetAsync($"Session/Sessions?userId={Guid.NewGuid()}"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs index 0dd22644ac..2d3879bdb6 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs @@ -36,15 +36,15 @@ namespace Jellyfin.Server.Integration.Tests.Controllers PreferredMetadataLanguage = "nl" }; - using var postResponse = await client.PostAsJsonAsync("/Startup/Configuration", config, _jsonOptions).ConfigureAwait(false); + using var postResponse = await client.PostAsJsonAsync("/Startup/Configuration", config, _jsonOptions); Assert.Equal(HttpStatusCode.NoContent, postResponse.StatusCode); - using var getResponse = await client.GetAsync("/Startup/Configuration").ConfigureAwait(false); + using var getResponse = await client.GetAsync("/Startup/Configuration"); Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode); Assert.Equal(MediaTypeNames.Application.Json, getResponse.Content.Headers.ContentType?.MediaType); - using var responseStream = await getResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); - var newConfig = await JsonSerializer.DeserializeAsync<StartupConfigurationDto>(responseStream, _jsonOptions).ConfigureAwait(false); + using var responseStream = await getResponse.Content.ReadAsStreamAsync(); + var newConfig = await JsonSerializer.DeserializeAsync<StartupConfigurationDto>(responseStream, _jsonOptions); Assert.Equal(config.UICulture, newConfig!.UICulture); Assert.Equal(config.MetadataCountryCode, newConfig.MetadataCountryCode); Assert.Equal(config.PreferredMetadataLanguage, newConfig.PreferredMetadataLanguage); @@ -56,12 +56,12 @@ namespace Jellyfin.Server.Integration.Tests.Controllers { var client = _factory.CreateClient(); - using var response = await client.GetAsync("/Startup/User").ConfigureAwait(false); + using var response = await client.GetAsync("/Startup/User"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType); - using var contentStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); - var user = await JsonSerializer.DeserializeAsync<StartupUserDto>(contentStream, _jsonOptions).ConfigureAwait(false); + using var contentStream = await response.Content.ReadAsStreamAsync(); + var user = await JsonSerializer.DeserializeAsync<StartupUserDto>(contentStream, _jsonOptions); Assert.NotNull(user); Assert.NotNull(user.Name); Assert.NotEmpty(user.Name); @@ -80,15 +80,15 @@ namespace Jellyfin.Server.Integration.Tests.Controllers Password = "NewPassword" }; - var postResponse = await client.PostAsJsonAsync("/Startup/User", user, _jsonOptions).ConfigureAwait(false); + var postResponse = await client.PostAsJsonAsync("/Startup/User", user, _jsonOptions); Assert.Equal(HttpStatusCode.NoContent, postResponse.StatusCode); - var getResponse = await client.GetAsync("/Startup/User").ConfigureAwait(false); + var getResponse = await client.GetAsync("/Startup/User"); Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode); Assert.Equal(MediaTypeNames.Application.Json, getResponse.Content.Headers.ContentType?.MediaType); - var contentStream = await getResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); - var newUser = await JsonSerializer.DeserializeAsync<StartupUserDto>(contentStream, _jsonOptions).ConfigureAwait(false); + var contentStream = await getResponse.Content.ReadAsStreamAsync(); + var newUser = await JsonSerializer.DeserializeAsync<StartupUserDto>(contentStream, _jsonOptions); Assert.NotNull(newUser); Assert.Equal(user.Name, newUser.Name); Assert.NotNull(newUser.Password); @@ -102,7 +102,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers { var client = _factory.CreateClient(); - var response = await client.PostAsync("/Startup/Complete", new ByteArrayContent(Array.Empty<byte>())).ConfigureAwait(false); + var response = await client.PostAsync("/Startup/Complete", new ByteArrayContent(Array.Empty<byte>())); Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); } @@ -112,7 +112,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers { var client = _factory.CreateClient(); - using var response = await client.GetAsync("/Startup/User").ConfigureAwait(false); + using var response = await client.GetAsync("/Startup/User"); Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); } } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs index 2a3c53dbe4..79d03d539a 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs @@ -41,10 +41,10 @@ namespace Jellyfin.Server.Integration.Tests.Controllers { var client = _factory.CreateClient(); - using var response = await client.GetAsync("Users/Public").ConfigureAwait(false); + using var response = await client.GetAsync("Users/Public"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var users = await JsonSerializer.DeserializeAsync<UserDto[]>( - await response.Content.ReadAsStreamAsync().ConfigureAwait(false), _jsonOpions).ConfigureAwait(false); + await response.Content.ReadAsStreamAsync(), _jsonOpions); // User are hidden by default Assert.NotNull(users); Assert.Empty(users); @@ -55,12 +55,12 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task GetUsers_Valid_Success() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - using var response = await client.GetAsync("Users").ConfigureAwait(false); + using var response = await client.GetAsync("Users"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var users = await JsonSerializer.DeserializeAsync<UserDto[]>( - await response.Content.ReadAsStreamAsync().ConfigureAwait(false), _jsonOpions).ConfigureAwait(false); + await response.Content.ReadAsStreamAsync(), _jsonOpions); Assert.NotNull(users); Assert.Single(users); Assert.False(users![0].HasConfiguredPassword); @@ -71,9 +71,9 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task Me_Valid_Success() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - _ = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false); + _ = await AuthHelper.GetUserDtoAsync(client); } [Fact] @@ -90,10 +90,10 @@ namespace Jellyfin.Server.Integration.Tests.Controllers Name = TestUsername }; - using var response = await CreateUserByName(client, createRequest).ConfigureAwait(false); + using var response = await CreateUserByName(client, createRequest); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var user = await JsonSerializer.DeserializeAsync<UserDto>( - await response.Content.ReadAsStreamAsync().ConfigureAwait(false), _jsonOpions).ConfigureAwait(false); + await response.Content.ReadAsStreamAsync(), _jsonOpions); Assert.Equal(TestUsername, user!.Name); Assert.False(user.HasPassword); Assert.False(user.HasConfiguredPassword); @@ -121,7 +121,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers Name = username! }; - using var response = await CreateUserByName(client, createRequest).ConfigureAwait(false); + using var response = await CreateUserByName(client, createRequest); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } @@ -134,7 +134,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers // access token can't be null here as the previous test populated it client.DefaultRequestHeaders.AddAuthHeader(_accessToken!); - using var response = await client.DeleteAsync($"User/{Guid.NewGuid()}").ConfigureAwait(false); + using var response = await client.DeleteAsync($"User/{Guid.NewGuid()}"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -150,11 +150,11 @@ namespace Jellyfin.Server.Integration.Tests.Controllers NewPw = "4randomPa$$word" }; - using var response = await UpdateUserPassword(client, _testUserId, createRequest).ConfigureAwait(false); + using var response = await UpdateUserPassword(client, _testUserId, createRequest); Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); var users = await JsonSerializer.DeserializeAsync<UserDto[]>( - await client.GetStreamAsync("Users").ConfigureAwait(false), _jsonOpions).ConfigureAwait(false); + await client.GetStreamAsync("Users"), _jsonOpions); var user = users!.First(x => x.Id.Equals(_testUserId)); Assert.True(user.HasPassword); Assert.True(user.HasConfiguredPassword); @@ -173,11 +173,11 @@ namespace Jellyfin.Server.Integration.Tests.Controllers CurrentPw = "4randomPa$$word", }; - using var response = await UpdateUserPassword(client, _testUserId, createRequest).ConfigureAwait(false); + using var response = await UpdateUserPassword(client, _testUserId, createRequest); Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); var users = await JsonSerializer.DeserializeAsync<UserDto[]>( - await client.GetStreamAsync("Users").ConfigureAwait(false), _jsonOpions).ConfigureAwait(false); + await client.GetStreamAsync("Users"), _jsonOpions); var user = users!.First(x => x.Id.Equals(_testUserId)); Assert.False(user.HasPassword); Assert.False(user.HasConfiguredPassword); diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs index 69f2ccf339..826a0a69dd 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs @@ -25,9 +25,9 @@ public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicati public async Task GetRootFolder_NonExistenUserId_NotFound() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.GetAsync($"Users/{Guid.NewGuid()}/Items/Root").ConfigureAwait(false); + var response = await client.GetAsync($"Users/{Guid.NewGuid()}/Items/Root"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -35,9 +35,9 @@ public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicati public async Task GetRootFolder_UserId_Valid() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - _ = await AuthHelper.GetRootFolderDtoAsync(client).ConfigureAwait(false); + _ = await AuthHelper.GetRootFolderDtoAsync(client); } [Theory] @@ -49,11 +49,11 @@ public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicati public async Task GetItem_NonExistenUserId_NotFound(string format) { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client).ConfigureAwait(false); + var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client); - var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid(), rootFolderDto.Id)).ConfigureAwait(false); + var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid(), rootFolderDto.Id)); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -66,11 +66,11 @@ public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicati public async Task GetItem_NonExistentItemId_NotFound(string format) { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false); + var userDto = await AuthHelper.GetUserDtoAsync(client); - var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id, Guid.NewGuid())).ConfigureAwait(false); + var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id, Guid.NewGuid())); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -78,16 +78,16 @@ public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicati public async Task GetItem_UserIdAndItemId_Valid() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false); - var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client, userDto.Id).ConfigureAwait(false); + var userDto = await AuthHelper.GetUserDtoAsync(client); + var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client, userDto.Id); - var response = await client.GetAsync($"Users/{userDto.Id}/Items/{rootFolderDto.Id}").ConfigureAwait(false); + var response = await client.GetAsync($"Users/{userDto.Id}/Items/{rootFolderDto.Id}"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var rootDto = await JsonSerializer.DeserializeAsync<BaseItemDto>( - await response.Content.ReadAsStreamAsync().ConfigureAwait(false), - _jsonOptions).ConfigureAwait(false); + await response.Content.ReadAsStreamAsync(), + _jsonOptions); Assert.NotNull(rootDto); } @@ -95,16 +95,16 @@ public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicati public async Task GetIntros_UserIdAndItemId_Valid() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false); - var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client, userDto.Id).ConfigureAwait(false); + var userDto = await AuthHelper.GetUserDtoAsync(client); + var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client, userDto.Id); - var response = await client.GetAsync($"Users/{userDto.Id}/Items/{rootFolderDto.Id}/Intros").ConfigureAwait(false); + var response = await client.GetAsync($"Users/{userDto.Id}/Items/{rootFolderDto.Id}/Intros"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var rootDto = await JsonSerializer.DeserializeAsync<QueryResult<BaseItemDto>>( - await response.Content.ReadAsStreamAsync().ConfigureAwait(false), - _jsonOptions).ConfigureAwait(false); + await response.Content.ReadAsStreamAsync(), + _jsonOptions); Assert.NotNull(rootDto); } @@ -114,16 +114,16 @@ public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicati public async Task LocalTrailersAndSpecialFeatures_UserIdAndItemId_Valid(string format) { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false); - var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client, userDto.Id).ConfigureAwait(false); + var userDto = await AuthHelper.GetUserDtoAsync(client); + var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client, userDto.Id); - var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id, rootFolderDto.Id)).ConfigureAwait(false); + var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id, rootFolderDto.Id)); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var rootDto = await JsonSerializer.DeserializeAsync<BaseItemDto[]>( - await response.Content.ReadAsStreamAsync().ConfigureAwait(false), - _jsonOptions).ConfigureAwait(false); + await response.Content.ReadAsStreamAsync(), + _jsonOptions); Assert.NotNull(rootDto); } } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/VideosControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/VideosControllerTests.cs index 0f9a2e90aa..47bec5d79a 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/VideosControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/VideosControllerTests.cs @@ -19,9 +19,9 @@ public sealed class VideosControllerTests : IClassFixture<JellyfinApplicationFac public async Task DeleteAlternateSources_NonExistentItemId_NotFound() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.DeleteAsync($"Videos/{Guid.NewGuid()}").ConfigureAwait(false); + var response = await client.DeleteAsync($"Videos/{Guid.NewGuid()}"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } } diff --git a/tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs b/tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs index 2361e4aa4e..d2249cdc3b 100644 --- a/tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs +++ b/tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs @@ -27,9 +27,9 @@ namespace Jellyfin.Server.Integration.Tests { var client = _factory.CreateClient(); - var response = await client.GetAsync("Encoder/UrlDecode?" + sourceUrl).ConfigureAwait(false); + var response = await client.GetAsync("Encoder/UrlDecode?" + sourceUrl); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - string reply = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + string reply = await response.Content.ReadAsStringAsync(); Assert.Equal(unencodedUrl, reply); } @@ -40,9 +40,9 @@ namespace Jellyfin.Server.Integration.Tests { var client = _factory.CreateClient(); - var response = await client.GetAsync("Encoder/UrlArrayDecode?" + sourceUrl).ConfigureAwait(false); + var response = await client.GetAsync("Encoder/UrlArrayDecode?" + sourceUrl); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - string reply = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + string reply = await response.Content.ReadAsStringAsync(); Assert.Equal(unencodedUrl, reply); } } diff --git a/tests/Jellyfin.Server.Integration.Tests/Middleware/RobotsRedirectionMiddlewareTests.cs b/tests/Jellyfin.Server.Integration.Tests/Middleware/RobotsRedirectionMiddlewareTests.cs index 8c49a2e2b5..c8ad9d2a15 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Middleware/RobotsRedirectionMiddlewareTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Middleware/RobotsRedirectionMiddlewareTests.cs @@ -23,7 +23,7 @@ namespace Jellyfin.Server.Integration.Tests.Middleware AllowAutoRedirect = false }); - var response = await client.GetAsync("robots.txt").ConfigureAwait(false); + var response = await client.GetAsync("robots.txt"); Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); Assert.Equal("web/robots.txt", response.Headers.Location?.ToString()); From 151d678b0ea344ec589f7ce478911b8e079e4f84 Mon Sep 17 00:00:00 2001 From: Tim Eisele <Shadowghost@users.noreply.github.com> Date: Mon, 18 Sep 2023 19:13:33 +0200 Subject: [PATCH 588/858] Update Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs Co-authored-by: Bond-009 <bond.009@outlook.com> --- .../Tasks/CleanupCollectionAndPlaylistPathsTask.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs index ef2d1942a5..acd4bf9056 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs @@ -135,7 +135,6 @@ public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask ForceSave = true }, RefreshPriority.High); - } } From b8f42573c42b63937433ef12e5948275192acde4 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Mon, 18 Sep 2023 20:50:05 +0200 Subject: [PATCH 589/858] Address review comments --- Emby.Server.Implementations/IO/LibraryMonitor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs index 1b6e454878..dde38906f3 100644 --- a/Emby.Server.Implementations/IO/LibraryMonitor.cs +++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs @@ -380,7 +380,6 @@ namespace Emby.Server.Implementations.IO } } - // Avoid implicitly captured closure CreateRefresher(path); } @@ -414,7 +413,8 @@ namespace Emby.Server.Implementations.IO } // They are siblings. Rebase the refresher to the parent folder. - if (parentPath is not null && string.Equals(parentPath, Path.GetDirectoryName(refresher.Path), StringComparison.Ordinal)) + if (parentPath is not null + && Path.GetDirectoryName(refresher.Path.AsSpan()).Equals(parentPath, StringComparison.Ordinal)) { refresher.ResetPath(parentPath, path); return; From 672e0d6434b3689f5ad88f9879296dbb6c4c8ff4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 19 Sep 2023 11:53:03 +0000 Subject: [PATCH 590/858] chore(deps): update github/codeql-action action to v2.21.8 --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 30c241224b..14ba3995da 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@04daf014b50eaf774287bf3f0f1869d4b4c4b913 # v2.21.7 + uses: github/codeql-action/init@6a28655e3dcb49cb0840ea372fd6d17733edd8a4 # v2.21.8 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@04daf014b50eaf774287bf3f0f1869d4b4c4b913 # v2.21.7 + uses: github/codeql-action/autobuild@6a28655e3dcb49cb0840ea372fd6d17733edd8a4 # v2.21.8 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@04daf014b50eaf774287bf3f0f1869d4b4c4b913 # v2.21.7 + uses: github/codeql-action/analyze@6a28655e3dcb49cb0840ea372fd6d17733edd8a4 # v2.21.8 From d82f59ec97824dec15334a7678f41c7b40d05e89 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 21 Sep 2023 20:47:05 +0200 Subject: [PATCH 591/858] chore(deps): update skiasharp monorepo to v2.88.6 (#10251) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index d7b424d3fe..99b3b466d7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -67,11 +67,11 @@ <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.0.2" /> <PackageVersion Include="SerilogAnalyzer" Version="0.15.0" /> <PackageVersion Include="SharpFuzz" Version="2.1.1" /> - <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.5" /> + <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.6" /> <PackageVersion Include="SkiaSharp.Svg" Version="1.60.0" /> - <PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.5" /> + <PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.6" /> <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.8.2.5" /> - <PackageVersion Include="SkiaSharp" Version="2.88.5" /> + <PackageVersion Include="SkiaSharp" Version="2.88.6" /> <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.507" /> <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" /> From a9274a356c83b3532b54536a6a6607d10ec9942e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 21 Sep 2023 21:09:56 +0200 Subject: [PATCH 592/858] chore(deps): update dependency harfbuzzsharp.nativeassets.linux to v7 (#10252) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 99b3b466d7..5351d5aa67 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -70,7 +70,7 @@ <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.6" /> <PackageVersion Include="SkiaSharp.Svg" Version="1.60.0" /> <PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.6" /> - <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.8.2.5" /> + <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0" /> <PackageVersion Include="SkiaSharp" Version="2.88.6" /> <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.507" /> From 5ba855098de51d586e3c7b4db3056b23576b6af0 Mon Sep 17 00:00:00 2001 From: Nyanmisaka <nst799610810@gmail.com> Date: Thu, 21 Sep 2023 15:35:39 -0400 Subject: [PATCH 593/858] Backport pull request #10151 from jellyfin/release-10.8.z Fix performance loss of QSV HDR tone-mapping on Windows Original-merge: 757f88b1a20ed493aa6c579a69c37f60092e7b3e Merged-by: Bond-009 <bond.009@outlook.com> Backported-by: Bond_009 <bond.009@outlook.com> --- .../MediaEncoding/EncodingHelper.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index e619e690d2..b6e680ab97 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -3822,12 +3822,6 @@ namespace MediaBrowser.Controller.MediaEncoding // map from d3d11va to qsv. mainFilters.Add("hwmap=derive_device=qsv"); } - else - { - // Insert a qsv scaler to sync the decoder surface, - // msdk will passthrough this internally. - mainFilters.Add("hwmap=derive_device=qsv,scale_qsv"); - } } // hw deint @@ -5252,10 +5246,8 @@ namespace MediaBrowser.Controller.MediaEncoding if (isD3d11Supported && isCodecAvailable) { - // set -threads 3 to intel d3d11va decoder explicitly. Lower threads may result in dead lock. - // on newer devices such as Xe, the larger the init_pool_size, the longer the initialization time for opencl to derive from d3d11. return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11" : string.Empty) - + (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + " -threads 3" + (isAv1 ? " -c:v av1" : string.Empty); + + (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + " -threads 2" + (isAv1 ? " -c:v av1" : string.Empty); } } else From 1d8c3e088be75ce02b811afaeea20307df0487be Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Fri, 22 Sep 2023 09:50:29 -0400 Subject: [PATCH 594/858] Don't log unhandled exceptions twice --- Jellyfin.Server/Program.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 6e8b17a737..3a3dd97bd3 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -89,12 +89,6 @@ namespace Jellyfin.Server private static async Task StartApp(StartupOptions options) { _startTimestamp = Stopwatch.GetTimestamp(); - - // Log all uncaught exceptions to std error - static void UnhandledExceptionToConsole(object sender, UnhandledExceptionEventArgs e) => - Console.Error.WriteLine("Unhandled Exception\n" + e.ExceptionObject); - AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionToConsole; - ServerApplicationPaths appPaths = StartupHelpers.CreateApplicationPaths(options); // $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager @@ -112,8 +106,7 @@ namespace Jellyfin.Server StartupHelpers.InitializeLoggingFramework(startupConfig, appPaths); _logger = _loggerFactory.CreateLogger("Main"); - // Log uncaught exceptions to the logging instead of std error - AppDomain.CurrentDomain.UnhandledException -= UnhandledExceptionToConsole; + // Use the logging framework for uncaught exceptions instead of std error AppDomain.CurrentDomain.UnhandledException += (_, e) => _logger.LogCritical((Exception)e.ExceptionObject, "Unhandled Exception"); From 9e65e4daf6fb66d5e1301c952f78caf002616c5a Mon Sep 17 00:00:00 2001 From: Bond-009 <bond.009@outlook.com> Date: Fri, 22 Sep 2023 19:17:32 +0200 Subject: [PATCH 595/858] Downgrade SkiaSharp.NativeAssets.Linux to prevent segfault ``` Thread 16 ".NET ThreadPool" received signal SIGSEGV, Segmentation fault. [Switching to Thread 0x7fbeae7fc6c0 (LWP 15740)] 0x00007fbead7b33e0 in ?? () from /.../dev/jellyfin/Jellyfin.Server/bin/Release/net7.0/runtimes/linux-x64/native/libSkiaSharp.so ``` --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 5351d5aa67..61d794f08e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -67,7 +67,7 @@ <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.0.2" /> <PackageVersion Include="SerilogAnalyzer" Version="0.15.0" /> <PackageVersion Include="SharpFuzz" Version="2.1.1" /> - <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.6" /> + <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.5" /> <PackageVersion Include="SkiaSharp.Svg" Version="1.60.0" /> <PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.6" /> <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0" /> From 61686d2dccc21a0ecda58ccf099a28a4eb226c12 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 23 Sep 2023 13:45:35 +0200 Subject: [PATCH 596/858] chore(deps): update actions/checkout action to v4.1.0 (#10260) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/commands.yml | 4 ++-- .github/workflows/openapi.yml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 14ba3995da..7855ee4f79 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 - name: Setup .NET uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0 with: diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index 02aeefdbd4..ba7883a734 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -24,7 +24,7 @@ jobs: reactions: '+1' - name: Checkout the latest code - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 with: token: ${{ secrets.JF_BOT_TOKEN }} fetch-depth: 0 @@ -51,7 +51,7 @@ jobs: reactions: eyes - name: Checkout the latest code - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 with: token: ${{ secrets.JF_BOT_TOKEN }} fetch-depth: 0 diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index 72f1b1d1ff..693f98d160 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -14,7 +14,7 @@ jobs: permissions: read-all steps: - name: Checkout repository - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 with: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} @@ -39,7 +39,7 @@ jobs: permissions: read-all steps: - name: Checkout repository - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 with: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} From 744591329eb40b711c8b6da7bb547c15df3705c8 Mon Sep 17 00:00:00 2001 From: Joe Rogers <1337joe@users.noreply.github.com> Date: Sat, 23 Sep 2023 13:46:43 +0200 Subject: [PATCH 597/858] Fully specify version tags for renovate (#10263) --- .github/workflows/repo-bump-version.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/repo-bump-version.yaml b/.github/workflows/repo-bump-version.yaml index 1713b484db..0ba68dda36 100644 --- a/.github/workflows/repo-bump-version.yaml +++ b/.github/workflows/repo-bump-version.yaml @@ -33,7 +33,7 @@ jobs: yq-version: v4.9.8 - name: Checkout Repository - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 with: ref: ${{ env.TAG_BRANCH }} @@ -66,7 +66,7 @@ jobs: NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }} steps: - name: Checkout Repository - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 with: ref: ${{ env.TAG_BRANCH }} From afc195286ff3cc0e08d51d75d3031e17108b495d Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Sat, 23 Sep 2023 15:12:12 +0200 Subject: [PATCH 598/858] Start adding IDisposableAnalyzers to projects --- Directory.Packages.props | 1 + jellyfin.ruleset | 14 ++++++++++++++ .../Jellyfin.Drawing.Skia.csproj | 6 +++++- src/Jellyfin.Drawing.Skia/SkiaEncoder.cs | 7 ++++--- src/Jellyfin.Drawing.Skia/SkiaHelper.cs | 8 ++++---- src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs | 4 ++-- src/Jellyfin.Drawing/Jellyfin.Drawing.csproj | 6 +++++- src/Jellyfin.Extensions/Jellyfin.Extensions.csproj | 4 ++++ .../Jellyfin.MediaEncoding.Hls.csproj | 4 ++++ .../Jellyfin.MediaEncoding.Keyframes.csproj | 4 ++++ 10 files changed, 47 insertions(+), 11 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 61d794f08e..27851cdebd 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -19,6 +19,7 @@ <PackageVersion Include="DotNet.Glob" Version="3.1.3" /> <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.9.2" /> <PackageVersion Include="FsCheck.Xunit" Version="2.16.6" /> + <PackageVersion Include="IDisposableAnalyzers" Version="4.0.7" /> <PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" /> <PackageVersion Include="libse" Version="3.6.13" /> <PackageVersion Include="LrcParser" Version="2023.524.0" /> diff --git a/jellyfin.ruleset b/jellyfin.ruleset index 4f01695888..870cf253f2 100644 --- a/jellyfin.ruleset +++ b/jellyfin.ruleset @@ -173,4 +173,18 @@ <!-- error on RS0030: Do not used banned APIs --> <Rule Id="RS0030" Action="Error" /> </Rules> + + <Rules AnalyzerId="IDisposableAnalyzers" RuleNamespace="IDisposableAnalyzers.Correctness"> + <!-- disable warning IDISP001: Dispose created --> + <Rule Id="IDISP001" Action="Info" /> + <!-- TODO: Enable when false positives are fixed --> + <!-- disable warning IDISP003: Dispose previous before re-assigning --> + <Rule Id="IDISP003" Action="Info" /> + <!-- disable warning IDISP004: Don't ignore created IDisposable --> + <Rule Id="IDISP004" Action="Info" /> + <!-- disable warning IDISP007: Don't dispose injected --> + <Rule Id="IDISP007" Action="Info" /> + <!-- disable warning IDISP008: Don't assign member with injected and created disposables --> + <Rule Id="IDISP008" Action="Info" /> + </Rules> </RuleSet> diff --git a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj index 0346913226..c465c4ad09 100644 --- a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj +++ b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj @@ -31,8 +31,12 @@ <ProjectReference Include="..\..\MediaBrowser.Common\MediaBrowser.Common.csproj" /> </ItemGroup> - <!-- Code analysers--> + <!-- Code Analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="IDisposableAnalyzers"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> + </PackageReference> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 5a1d3dc5f9..126c0503e0 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -122,8 +122,8 @@ public class SkiaEncoder : IImageEncoder var svg = new SKSvg(); try { - svg.Load(path); - return new ImageDimensions(Convert.ToInt32(svg.Picture.CullRect.Width), Convert.ToInt32(svg.Picture.CullRect.Height)); + using var picture = svg.Load(path); + return new ImageDimensions(Convert.ToInt32(picture.CullRect.Width), Convert.ToInt32(picture.CullRect.Height)); } catch (FormatException skiaColorException) { @@ -432,7 +432,8 @@ public class SkiaEncoder : IImageEncoder // scale image (the FromImage creates a copy) var imageInfo = new SKImageInfo(width, height, bitmap.ColorType, bitmap.AlphaType, bitmap.ColorSpace); - using var resizedBitmap = SKBitmap.FromImage(ResizeImage(bitmap, imageInfo)); + using var resizedImage = ResizeImage(bitmap, imageInfo); + using var resizedBitmap = SKBitmap.FromImage(resizedImage); // If all we're doing is resizing then we can stop now if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator) diff --git a/src/Jellyfin.Drawing.Skia/SkiaHelper.cs b/src/Jellyfin.Drawing.Skia/SkiaHelper.cs index 00d224da94..bd1b2b0da0 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaHelper.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaHelper.cs @@ -19,7 +19,6 @@ public static class SkiaHelper public static SKBitmap? GetNextValidImage(SkiaEncoder skiaEncoder, IReadOnlyList<string> paths, int currentIndex, out int newIndex) { var imagesTested = new Dictionary<int, int>(); - SKBitmap? bitmap = null; while (imagesTested.Count < paths.Count) { @@ -28,7 +27,7 @@ public static class SkiaHelper currentIndex = 0; } - bitmap = skiaEncoder.Decode(paths[currentIndex], false, null, out _); + SKBitmap? bitmap = skiaEncoder.Decode(paths[currentIndex], false, null, out _); imagesTested[currentIndex] = 0; @@ -36,11 +35,12 @@ public static class SkiaHelper if (bitmap is not null) { - break; + newIndex = currentIndex; + return bitmap; } } newIndex = currentIndex; - return bitmap; + return null; } } diff --git a/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs index a8f80f7e21..6dff7aa9b4 100644 --- a/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs +++ b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs @@ -189,12 +189,12 @@ public partial class StripCollageBuilder // Scale image. The FromBitmap creates a copy var imageInfo = new SKImageInfo(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType, currentBitmap.ColorSpace); - using var resizedBitmap = SKBitmap.FromImage(SkiaEncoder.ResizeImage(currentBitmap, imageInfo)); + using var resizeImage = SkiaEncoder.ResizeImage(currentBitmap, imageInfo); // draw this image into the strip at the next position var xPos = x * cellWidth; var yPos = y * cellHeight; - canvas.DrawBitmap(resizedBitmap, xPos, yPos); + canvas.DrawImage(resizeImage, xPos, yPos); } } diff --git a/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj index e0963ac34e..2a5e24a449 100644 --- a/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj +++ b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj @@ -21,8 +21,12 @@ <Compile Include="..\..\SharedVersion.cs" /> </ItemGroup> - <!-- Code analysers--> + <!-- Code Analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="IDisposableAnalyzers"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> + </PackageReference> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> diff --git a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj index 4f80aa9416..36ae55ed2e 100644 --- a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj +++ b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj @@ -34,6 +34,10 @@ <!-- Code Analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="IDisposableAnalyzers"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> + </PackageReference> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> diff --git a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj index 3f4f55ee41..b792e7ec64 100644 --- a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj +++ b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj @@ -7,6 +7,10 @@ <!-- Code Analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="IDisposableAnalyzers"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> + </PackageReference> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> diff --git a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj index 71572bcf6a..09b1f8faa0 100644 --- a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj +++ b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj @@ -11,6 +11,10 @@ <!-- Code Analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="IDisposableAnalyzers"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> + </PackageReference> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> From f778073132f78b3a246a5a665a2c536d05a0278f Mon Sep 17 00:00:00 2001 From: Bond-009 <bond.009@outlook.com> Date: Sat, 23 Sep 2023 17:57:08 +0200 Subject: [PATCH 599/858] Downgrade SkiaSharp to prevent segfault (#10264) --- Directory.Packages.props | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 27851cdebd..6bf960bb71 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -19,6 +19,7 @@ <PackageVersion Include="DotNet.Glob" Version="3.1.3" /> <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.9.2" /> <PackageVersion Include="FsCheck.Xunit" Version="2.16.6" /> + <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0" /> <PackageVersion Include="IDisposableAnalyzers" Version="4.0.7" /> <PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" /> <PackageVersion Include="libse" Version="3.6.13" /> @@ -68,11 +69,10 @@ <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.0.2" /> <PackageVersion Include="SerilogAnalyzer" Version="0.15.0" /> <PackageVersion Include="SharpFuzz" Version="2.1.1" /> + <PackageVersion Include="SkiaSharp" Version="2.88.5" /> + <PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.5" /> <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.5" /> <PackageVersion Include="SkiaSharp.Svg" Version="1.60.0" /> - <PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.6" /> - <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0" /> - <PackageVersion Include="SkiaSharp" Version="2.88.6" /> <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.507" /> <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" /> From 493de3297a415061f8d6a69ff9f62261c3159a2a Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Fri, 22 Sep 2023 21:10:49 -0400 Subject: [PATCH 600/858] Use IHostLifetime to handle restarting and shutting down --- .../ApplicationHost.cs | 124 ++---------------- .../Plugins/PluginManager.cs | 22 ++-- .../Session/SessionManager.cs | 117 ++++++++--------- Jellyfin.Api/Controllers/SystemController.cs | 14 +- Jellyfin.Server/CoreAppHost.cs | 6 - Jellyfin.Server/Program.cs | 70 +--------- MediaBrowser.Common/IApplicationHost.cs | 21 +-- MediaBrowser.Common/Plugins/IPluginManager.cs | 5 - .../Session/ISessionManager.cs | 14 -- .../JellyfinApplicationFactory.cs | 3 +- 10 files changed, 98 insertions(+), 298 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 8b13ccadab..86721ace61 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -12,7 +12,6 @@ using System.Linq; using System.Net; using System.Reflection; using System.Security.Cryptography.X509Certificates; -using System.Threading; using System.Threading.Tasks; using Emby.Dlna; using Emby.Dlna.Main; @@ -112,7 +111,7 @@ namespace Emby.Server.Implementations /// <summary> /// Class CompositionRoot. /// </summary> - public abstract class ApplicationHost : IServerApplicationHost, IAsyncDisposable, IDisposable + public abstract class ApplicationHost : IServerApplicationHost, IDisposable { /// <summary> /// The disposable parts. @@ -127,7 +126,6 @@ namespace Emby.Server.Implementations private readonly IPluginManager _pluginManager; private List<Type> _creatingInstances; - private ISessionManager _sessionManager; /// <summary> /// Gets or sets all concrete types. @@ -172,6 +170,8 @@ namespace Emby.Server.Implementations ConfigurationManager.Configuration, ApplicationPaths.PluginsPath, ApplicationVersion); + + _disposableParts.TryAdd((PluginManager)_pluginManager, byte.MinValue); } /// <summary> @@ -202,7 +202,10 @@ namespace Emby.Server.Implementations public bool HasPendingRestart { get; private set; } /// <inheritdoc /> - public bool IsShuttingDown { get; private set; } + public bool IsShuttingDown { get; set; } + + /// <inheritdoc /> + public bool ShouldRestart { get; set; } /// <summary> /// Gets the logger. @@ -406,11 +409,9 @@ namespace Emby.Server.Implementations /// <summary> /// Runs the startup tasks. /// </summary> - /// <param name="cancellationToken">The cancellation token.</param> /// <returns><see cref="Task" />.</returns> - public async Task RunStartupTasksAsync(CancellationToken cancellationToken) + public async Task RunStartupTasksAsync() { - cancellationToken.ThrowIfCancellationRequested(); Logger.LogInformation("Running startup tasks"); Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false)); @@ -424,8 +425,6 @@ namespace Emby.Server.Implementations var entryPoints = GetExports<IServerEntryPoint>(); - cancellationToken.ThrowIfCancellationRequested(); - var stopWatch = new Stopwatch(); stopWatch.Start(); @@ -435,8 +434,6 @@ namespace Emby.Server.Implementations Logger.LogInformation("Core startup complete"); CoreStartupHasCompleted = true; - cancellationToken.ThrowIfCancellationRequested(); - stopWatch.Restart(); await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false); @@ -633,8 +630,6 @@ namespace Emby.Server.Implementations var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>(); await localizationManager.LoadAll().ConfigureAwait(false); - _sessionManager = Resolve<ISessionManager>(); - SetStaticProperties(); FindParts(); @@ -855,38 +850,6 @@ namespace Emby.Server.Implementations } } - /// <summary> - /// Restarts this instance. - /// </summary> - public void Restart() - { - if (IsShuttingDown) - { - return; - } - - IsShuttingDown = true; - _pluginManager.UnloadAssemblies(); - - Task.Run(async () => - { - try - { - await _sessionManager.SendServerRestartNotification(CancellationToken.None).ConfigureAwait(false); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error sending server restart notification"); - } - - Logger.LogInformation("Calling RestartInternal"); - - RestartInternal(); - }); - } - - protected abstract void RestartInternal(); - /// <summary> /// Gets the composable part assemblies. /// </summary> @@ -1065,30 +1028,6 @@ namespace Emby.Server.Implementations }.ToString().TrimEnd('/'); } - /// <inheritdoc /> - public async Task Shutdown() - { - if (IsShuttingDown) - { - return; - } - - IsShuttingDown = true; - - try - { - await _sessionManager.SendServerShutdownNotification(CancellationToken.None).ConfigureAwait(false); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error sending server shutdown notification"); - } - - ShutdownInternal(); - } - - protected abstract void ShutdownInternal(); - public IEnumerable<Assembly> GetApiPluginAssemblies() { var assemblies = _allConcreteTypes @@ -1152,52 +1091,5 @@ namespace Emby.Server.Implementations _disposed = true; } - - public async ValueTask DisposeAsync() - { - await DisposeAsyncCore().ConfigureAwait(false); - Dispose(false); - GC.SuppressFinalize(this); - } - - /// <summary> - /// Used to perform asynchronous cleanup of managed resources or for cascading calls to <see cref="DisposeAsync"/>. - /// </summary> - /// <returns>A ValueTask.</returns> - protected virtual async ValueTask DisposeAsyncCore() - { - var type = GetType(); - - Logger.LogInformation("Disposing {Type}", type.Name); - - foreach (var (part, _) in _disposableParts) - { - var partType = part.GetType(); - if (partType == type) - { - continue; - } - - Logger.LogInformation("Disposing {Type}", partType.Name); - - try - { - part.Dispose(); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error disposing {Type}", partType.Name); - } - } - - if (_sessionManager is not null) - { - // used for closing websockets - foreach (var session in _sessionManager.Sessions) - { - await session.DisposeAsync().ConfigureAwait(false); - } - } - } } } diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs index 1303012e1a..d7189ef0ca 100644 --- a/Emby.Server.Implementations/Plugins/PluginManager.cs +++ b/Emby.Server.Implementations/Plugins/PluginManager.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Data; using System.Globalization; using System.IO; using System.Linq; @@ -11,7 +10,6 @@ using System.Text; using System.Text.Json; using System.Threading.Tasks; using Emby.Server.Implementations.Library; -using Jellyfin.Extensions; using Jellyfin.Extensions.Json; using Jellyfin.Extensions.Json.Converters; using MediaBrowser.Common; @@ -30,7 +28,7 @@ namespace Emby.Server.Implementations.Plugins /// <summary> /// Defines the <see cref="PluginManager" />. /// </summary> - public class PluginManager : IPluginManager + public sealed class PluginManager : IPluginManager, IDisposable { private const string MetafileName = "meta.json"; @@ -191,15 +189,6 @@ namespace Emby.Server.Implementations.Plugins } } - /// <inheritdoc /> - public void UnloadAssemblies() - { - foreach (var assemblyLoadContext in _assemblyLoadContexts) - { - assemblyLoadContext.Unload(); - } - } - /// <summary> /// Creates all the plugin instances. /// </summary> @@ -441,6 +430,15 @@ namespace Emby.Server.Implementations.Plugins return SaveManifest(manifest, path); } + /// <inheritdoc /> + public void Dispose() + { + foreach (var assemblyLoadContext in _assemblyLoadContexts) + { + assemblyLoadContext.Unload(); + } + } + /// <summary> /// Reconciles the manifest against any properties that exist locally in a pre-packaged meta.json found at the path. /// If no file is found, no reconciliation occurs. diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index b4a622ccf4..902d46a906 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -36,6 +36,7 @@ using MediaBrowser.Model.Querying; using MediaBrowser.Model.Session; using MediaBrowser.Model.SyncPlay; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Episode = MediaBrowser.Controller.Entities.TV.Episode; @@ -44,7 +45,7 @@ namespace Emby.Server.Implementations.Session /// <summary> /// Class SessionManager. /// </summary> - public class SessionManager : ISessionManager, IDisposable + public sealed class SessionManager : ISessionManager, IAsyncDisposable { private readonly IUserDataManager _userDataManager; private readonly ILogger<SessionManager> _logger; @@ -57,11 +58,9 @@ namespace Emby.Server.Implementations.Session private readonly IMediaSourceManager _mediaSourceManager; private readonly IServerApplicationHost _appHost; private readonly IDeviceManager _deviceManager; - - /// <summary> - /// The active connections. - /// </summary> - private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections = new(StringComparer.OrdinalIgnoreCase); + private readonly CancellationTokenRegistration _shutdownCallback; + private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections + = new(StringComparer.OrdinalIgnoreCase); private Timer _idleTimer; @@ -79,7 +78,8 @@ namespace Emby.Server.Implementations.Session IImageProcessor imageProcessor, IServerApplicationHost appHost, IDeviceManager deviceManager, - IMediaSourceManager mediaSourceManager) + IMediaSourceManager mediaSourceManager, + IHostApplicationLifetime hostApplicationLifetime) { _logger = logger; _eventManager = eventManager; @@ -92,6 +92,7 @@ namespace Emby.Server.Implementations.Session _appHost = appHost; _deviceManager = deviceManager; _mediaSourceManager = mediaSourceManager; + _shutdownCallback = hostApplicationLifetime.ApplicationStopping.Register(OnApplicationStopping); _deviceManager.DeviceOptionsUpdated += OnDeviceManagerDeviceOptionsUpdated; } @@ -151,36 +152,6 @@ namespace Emby.Server.Implementations.Session } } - /// <inheritdoc /> - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// <summary> - /// Releases unmanaged and optionally managed resources. - /// </summary> - /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> - protected virtual void Dispose(bool disposing) - { - if (_disposed) - { - return; - } - - if (disposing) - { - _idleTimer?.Dispose(); - } - - _idleTimer = null; - - _deviceManager.DeviceOptionsUpdated -= OnDeviceManagerDeviceOptionsUpdated; - - _disposed = true; - } - private void CheckDisposed() { if (_disposed) @@ -1330,32 +1301,6 @@ namespace Emby.Server.Implementations.Session return SendMessageToSessions(Sessions, SessionMessageType.RestartRequired, string.Empty, cancellationToken); } - /// <summary> - /// Sends the server shutdown notification. - /// </summary> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - public Task SendServerShutdownNotification(CancellationToken cancellationToken) - { - CheckDisposed(); - - return SendMessageToSessions(Sessions, SessionMessageType.ServerShuttingDown, string.Empty, cancellationToken); - } - - /// <summary> - /// Sends the server restart notification. - /// </summary> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - public Task SendServerRestartNotification(CancellationToken cancellationToken) - { - CheckDisposed(); - - _logger.LogDebug("Beginning SendServerRestartNotification"); - - return SendMessageToSessions(Sessions, SessionMessageType.ServerRestarting, string.Empty, cancellationToken); - } - /// <summary> /// Adds the additional user. /// </summary> @@ -1833,5 +1778,51 @@ namespace Emby.Server.Implementations.Session return SendMessageToSessions(sessions, name, data, cancellationToken); } + + /// <inheritdoc /> + public async ValueTask DisposeAsync() + { + if (_disposed) + { + return; + } + + foreach (var session in _activeConnections.Values) + { + await session.DisposeAsync().ConfigureAwait(false); + } + + if (_idleTimer is not null) + { + await _idleTimer.DisposeAsync().ConfigureAwait(false); + _idleTimer = null; + } + + await _shutdownCallback.DisposeAsync().ConfigureAwait(false); + + _deviceManager.DeviceOptionsUpdated -= OnDeviceManagerDeviceOptionsUpdated; + _disposed = true; + } + + private async void OnApplicationStopping() + { + _logger.LogInformation("Sending shutdown notifications"); + try + { + var messageType = _appHost.ShouldRestart ? SessionMessageType.ServerRestarting : SessionMessageType.ServerShuttingDown; + + await SendMessageToSessions(Sessions, messageType, string.Empty, CancellationToken.None).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error sending server shutdown notifications"); + } + + // Close open websockets to allow Kestrel to shut down cleanly + foreach (var session in _activeConnections.Values) + { + await session.DisposeAsync().ConfigureAwait(false); + } + } } } diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index a29790961e..4cc0f0ced4 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -18,6 +18,7 @@ using MediaBrowser.Model.System; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace Jellyfin.Api.Controllers; @@ -32,6 +33,7 @@ public class SystemController : BaseJellyfinApiController private readonly IFileSystem _fileSystem; private readonly INetworkManager _network; private readonly ILogger<SystemController> _logger; + private readonly IHostApplicationLifetime _hostApplicationLifetime; /// <summary> /// Initializes a new instance of the <see cref="SystemController"/> class. @@ -41,18 +43,21 @@ public class SystemController : BaseJellyfinApiController /// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param> /// <param name="network">Instance of <see cref="INetworkManager"/> interface.</param> /// <param name="logger">Instance of <see cref="ILogger{SystemController}"/> interface.</param> + /// <param name="hostApplicationLifetime">Instance of <see cref="IHostApplicationLifetime"/> interface.</param> public SystemController( IServerConfigurationManager serverConfigurationManager, IServerApplicationHost appHost, IFileSystem fileSystem, INetworkManager network, - ILogger<SystemController> logger) + ILogger<SystemController> logger, + IHostApplicationLifetime hostApplicationLifetime) { _appPaths = serverConfigurationManager.ApplicationPaths; _appHost = appHost; _fileSystem = fileSystem; _network = network; _logger = logger; + _hostApplicationLifetime = hostApplicationLifetime; } /// <summary> @@ -110,7 +115,9 @@ public class SystemController : BaseJellyfinApiController Task.Run(async () => { await Task.Delay(100).ConfigureAwait(false); - _appHost.Restart(); + _appHost.ShouldRestart = true; + _appHost.IsShuttingDown = true; + _hostApplicationLifetime.StopApplication(); }); return NoContent(); } @@ -130,7 +137,8 @@ public class SystemController : BaseJellyfinApiController Task.Run(async () => { await Task.Delay(100).ConfigureAwait(false); - await _appHost.Shutdown().ConfigureAwait(false); + _appHost.IsShuttingDown = true; + _hostApplicationLifetime.StopApplication(); }); return NoContent(); } diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index 0c6315c667..4c116745b8 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -102,9 +102,6 @@ namespace Jellyfin.Server base.RegisterServices(serviceCollection); } - /// <inheritdoc /> - protected override void RestartInternal() => Program.Restart(); - /// <inheritdoc /> protected override IEnumerable<Assembly> GetAssembliesWithPartsInternal() { @@ -114,8 +111,5 @@ namespace Jellyfin.Server // Jellyfin.Server.Implementations yield return typeof(JellyfinDbContext).Assembly; } - - /// <inheritdoc /> - protected override void ShutdownInternal() => Program.Shutdown(); } } diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 3a3dd97bd3..f9259d0d92 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -4,7 +4,6 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; -using System.Threading; using System.Threading.Tasks; using CommandLine; using Emby.Server.Implementations; @@ -42,7 +41,6 @@ namespace Jellyfin.Server public const string LoggingConfigFileSystem = "logging.json"; private static readonly ILoggerFactory _loggerFactory = new SerilogLoggerFactory(); - private static CancellationTokenSource _tokenSource = new(); private static long _startTimestamp; private static ILogger _logger = NullLogger.Instance; private static bool _restartOnShutdown; @@ -65,27 +63,6 @@ namespace Jellyfin.Server .MapResult(StartApp, ErrorParsingArguments); } - /// <summary> - /// Shuts down the application. - /// </summary> - internal static void Shutdown() - { - if (!_tokenSource.IsCancellationRequested) - { - _tokenSource.Cancel(); - } - } - - /// <summary> - /// Restarts the application. - /// </summary> - internal static void Restart() - { - _restartOnShutdown = true; - - Shutdown(); - } - private static async Task StartApp(StartupOptions options) { _startTimestamp = Stopwatch.GetTimestamp(); @@ -110,33 +87,6 @@ namespace Jellyfin.Server AppDomain.CurrentDomain.UnhandledException += (_, e) => _logger.LogCritical((Exception)e.ExceptionObject, "Unhandled Exception"); - // Intercept Ctrl+C and Ctrl+Break - Console.CancelKeyPress += (_, e) => - { - if (_tokenSource.IsCancellationRequested) - { - return; // Already shutting down - } - - e.Cancel = true; - _logger.LogInformation("Ctrl+C, shutting down"); - Environment.ExitCode = 128 + 2; - Shutdown(); - }; - - // Register a SIGTERM handler - AppDomain.CurrentDomain.ProcessExit += (_, _) => - { - if (_tokenSource.IsCancellationRequested) - { - return; // Already shutting down - } - - _logger.LogInformation("Received a SIGTERM signal, shutting down"); - Environment.ExitCode = 128 + 15; - Shutdown(); - }; - _logger.LogInformation( "Jellyfin version: {Version}", Assembly.GetEntryAssembly()!.GetName().Version!.ToString(3)); @@ -166,12 +116,10 @@ namespace Jellyfin.Server do { - _restartOnShutdown = false; await StartServer(appPaths, options, startupConfig).ConfigureAwait(false); if (_restartOnShutdown) { - _tokenSource = new CancellationTokenSource(); _startTimestamp = Stopwatch.GetTimestamp(); } } while (_restartOnShutdown); @@ -179,7 +127,7 @@ namespace Jellyfin.Server private static async Task StartServer(IServerApplicationPaths appPaths, StartupOptions options, IConfiguration startupConfig) { - var appHost = new CoreAppHost( + using var appHost = new CoreAppHost( appPaths, _loggerFactory, options, @@ -189,6 +137,7 @@ namespace Jellyfin.Server try { host = Host.CreateDefaultBuilder() + .UseConsoleLifetime() .ConfigureServices(services => appHost.Init(services)) .ConfigureWebHostDefaults(webHostBuilder => webHostBuilder.ConfigureWebHostBuilder(appHost, startupConfig, appPaths, _logger)) .ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(options, appPaths, startupConfig)) @@ -203,7 +152,7 @@ namespace Jellyfin.Server try { - await host.StartAsync(_tokenSource.Token).ConfigureAwait(false); + await host.StartAsync().ConfigureAwait(false); if (!OperatingSystem.IsWindows() && startupConfig.UseUnixSocket()) { @@ -212,22 +161,18 @@ namespace Jellyfin.Server StartupHelpers.SetUnixSocketPermissions(startupConfig, socketPath, _logger); } } - catch (Exception ex) when (ex is not TaskCanceledException) + catch (Exception) { _logger.LogError("Kestrel failed to start! This is most likely due to an invalid address or port bind - correct your bind configuration in network.xml and try again"); throw; } - await appHost.RunStartupTasksAsync(_tokenSource.Token).ConfigureAwait(false); + await appHost.RunStartupTasksAsync().ConfigureAwait(false); _logger.LogInformation("Startup complete {Time:g}", Stopwatch.GetElapsedTime(_startTimestamp)); - // Block main thread until shutdown - await Task.Delay(-1, _tokenSource.Token).ConfigureAwait(false); - } - catch (TaskCanceledException) - { - // Don't throw on cancellation + await host.WaitForShutdownAsync().ConfigureAwait(false); + _restartOnShutdown = appHost.ShouldRestart; } catch (Exception ex) { @@ -250,7 +195,6 @@ namespace Jellyfin.Server } } - await appHost.DisposeAsync().ConfigureAwait(false); host?.Dispose(); } } diff --git a/MediaBrowser.Common/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs index 96ee701b38..c1ea6e87cc 100644 --- a/MediaBrowser.Common/IApplicationHost.cs +++ b/MediaBrowser.Common/IApplicationHost.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Reflection; -using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; namespace MediaBrowser.Common @@ -42,10 +41,15 @@ namespace MediaBrowser.Common bool HasPendingRestart { get; } /// <summary> - /// Gets a value indicating whether this instance is currently shutting down. + /// Gets or sets a value indicating whether this instance is currently shutting down. /// </summary> /// <value><c>true</c> if this instance is shutting down; otherwise, <c>false</c>.</value> - bool IsShuttingDown { get; } + bool IsShuttingDown { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether the application should restart. + /// </summary> + bool ShouldRestart { get; set; } /// <summary> /// Gets the application version. @@ -87,11 +91,6 @@ namespace MediaBrowser.Common /// </summary> void NotifyPendingRestart(); - /// <summary> - /// Restarts this instance. - /// </summary> - void Restart(); - /// <summary> /// Gets the exports. /// </summary> @@ -123,12 +122,6 @@ namespace MediaBrowser.Common /// <returns>``0.</returns> T Resolve<T>(); - /// <summary> - /// Shuts down. - /// </summary> - /// <returns>A task.</returns> - Task Shutdown(); - /// <summary> /// Initializes this instance. /// </summary> diff --git a/MediaBrowser.Common/Plugins/IPluginManager.cs b/MediaBrowser.Common/Plugins/IPluginManager.cs index 1d73de3c95..0ff9719e98 100644 --- a/MediaBrowser.Common/Plugins/IPluginManager.cs +++ b/MediaBrowser.Common/Plugins/IPluginManager.cs @@ -29,11 +29,6 @@ namespace MediaBrowser.Common.Plugins /// <returns>An IEnumerable{Assembly}.</returns> IEnumerable<Assembly> LoadAssemblies(); - /// <summary> - /// Unloads all of the assemblies. - /// </summary> - void UnloadAssemblies(); - /// <summary> /// Registers the plugin's services with the DI. /// Note: DI is not yet instantiated yet. diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index 0c4719a0e5..53df7133b5 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -232,20 +232,6 @@ namespace MediaBrowser.Controller.Session /// <returns>Task.</returns> Task SendRestartRequiredNotification(CancellationToken cancellationToken); - /// <summary> - /// Sends the server shutdown notification. - /// </summary> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - Task SendServerShutdownNotification(CancellationToken cancellationToken); - - /// <summary> - /// Sends the server restart notification. - /// </summary> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - Task SendServerRestartNotification(CancellationToken cancellationToken); - /// <summary> /// Adds the additional user. /// </summary> diff --git a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs index 55bc43455f..1c87d11f18 100644 --- a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs +++ b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Concurrent; using System.Globalization; using System.IO; -using System.Threading; using Emby.Server.Implementations; using Jellyfin.Server.Extensions; using Jellyfin.Server.Helpers; @@ -105,7 +104,7 @@ namespace Jellyfin.Server.Integration.Tests var appHost = (TestAppHost)testServer.Services.GetRequiredService<IApplicationHost>(); appHost.ServiceProvider = testServer.Services; appHost.InitializeServices().GetAwaiter().GetResult(); - appHost.RunStartupTasksAsync(CancellationToken.None).GetAwaiter().GetResult(); + appHost.RunStartupTasksAsync().GetAwaiter().GetResult(); return testServer; } From ba7e3bfd82aeee3c74a99fa1f4ca94deca8b4dbe Mon Sep 17 00:00:00 2001 From: Cody Robibero <cody@robibe.ro> Date: Sat, 23 Sep 2023 10:59:13 -0600 Subject: [PATCH 601/858] Migrate to customizable cast receiver config --- .config/dotnet-tools.json | 12 + .../ApplicationHost.cs | 3 +- Jellyfin.Data/Entities/User.cs | 6 + ...0230923170422_UserCastReceiver.Designer.cs | 654 ++++++++++++++++++ .../20230923170422_UserCastReceiver.cs | 29 + .../Migrations/JellyfinDbModelSnapshot.cs | 6 +- .../Users/UserManager.cs | 16 +- Jellyfin.Server/Migrations/MigrationRunner.cs | 3 +- .../Routines/AddDefaultCastReceivers.cs | 55 ++ .../Configuration/ServerConfiguration.cs | 509 +++++++------- .../Configuration/UserConfiguration.cs | 5 + .../System/CastReceiverApplication.cs | 17 + MediaBrowser.Model/System/SystemInfo.cs | 6 + 13 files changed, 1064 insertions(+), 257 deletions(-) create mode 100644 .config/dotnet-tools.json create mode 100644 Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.Designer.cs create mode 100644 Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.cs create mode 100644 Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs create mode 100644 MediaBrowser.Model/System/CastReceiverApplication.cs diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000000..96be437d5d --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "7.0.11", + "commands": [ + "dotnet-ef" + ] + } + } +} \ No newline at end of file diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index f8208af704..5f8d275b6c 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -968,7 +968,8 @@ namespace Emby.Server.Implementations ServerName = FriendlyName, LocalAddress = GetSmartApiUrl(request), SupportsLibraryMonitor = true, - PackageName = _startupOptions.PackageName + PackageName = _startupOptions.PackageName, + CastReceiverApplications = ConfigurationManager.Configuration.CastReceiverApplications }; } diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs index 58ddaaf83a..5c3e0338de 100644 --- a/Jellyfin.Data/Entities/User.cs +++ b/Jellyfin.Data/Entities/User.cs @@ -288,6 +288,12 @@ namespace Jellyfin.Data.Entities /// </summary> public SyncPlayUserAccessType SyncPlayAccess { get; set; } + /// <summary> + /// Gets or sets the cast receiver id. + /// </summary> + [StringLength(32)] + public string? CastReceiverId { get; set; } + /// <inheritdoc /> [ConcurrencyCheck] public uint RowVersion { get; private set; } diff --git a/Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.Designer.cs new file mode 100644 index 0000000000..2884d4256c --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.Designer.cs @@ -0,0 +1,654 @@ +// <auto-generated /> +using System; +using Jellyfin.Server.Implementations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + [DbContext(typeof(JellyfinDbContext))] + [Migration("20230923170422_UserCastReceiver")] + partial class UserCastReceiver + { + /// <inheritdoc /> + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.11"); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<int>("DayOfWeek") + .HasColumnType("INTEGER"); + + b.Property<double>("EndHour") + .HasColumnType("REAL"); + + b.Property<double>("StartHour") + .HasColumnType("REAL"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccessSchedules"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<DateTime>("DateCreated") + .HasColumnType("TEXT"); + + b.Property<string>("ItemId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property<int>("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property<string>("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property<string>("Overview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property<uint>("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property<string>("ShortOverview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property<string>("Type") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DateCreated"); + + b.ToTable("ActivityLogs"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<string>("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<Guid>("ItemId") + .HasColumnType("TEXT"); + + b.Property<string>("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.Property<string>("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client", "Key") + .IsUnique(); + + b.ToTable("CustomItemDisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<int>("ChromecastVersion") + .HasColumnType("INTEGER"); + + b.Property<string>("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<string>("DashboardTheme") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<bool>("EnableNextVideoInfoOverlay") + .HasColumnType("INTEGER"); + + b.Property<int?>("IndexBy") + .HasColumnType("INTEGER"); + + b.Property<Guid>("ItemId") + .HasColumnType("TEXT"); + + b.Property<int>("ScrollDirection") + .HasColumnType("INTEGER"); + + b.Property<bool>("ShowBackdrop") + .HasColumnType("INTEGER"); + + b.Property<bool>("ShowSidebar") + .HasColumnType("INTEGER"); + + b.Property<int>("SkipBackwardLength") + .HasColumnType("INTEGER"); + + b.Property<int>("SkipForwardLength") + .HasColumnType("INTEGER"); + + b.Property<string>("TvHome") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client") + .IsUnique(); + + b.ToTable("DisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<int>("DisplayPreferencesId") + .HasColumnType("INTEGER"); + + b.Property<int>("Order") + .HasColumnType("INTEGER"); + + b.Property<int>("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DisplayPreferencesId"); + + b.ToTable("HomeSection"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<DateTime>("LastModified") + .HasColumnType("TEXT"); + + b.Property<string>("Path") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property<Guid?>("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("ImageInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<string>("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<int?>("IndexBy") + .HasColumnType("INTEGER"); + + b.Property<Guid>("ItemId") + .HasColumnType("TEXT"); + + b.Property<bool>("RememberIndexing") + .HasColumnType("INTEGER"); + + b.Property<bool>("RememberSorting") + .HasColumnType("INTEGER"); + + b.Property<string>("SortBy") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property<int>("SortOrder") + .HasColumnType("INTEGER"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.Property<int>("ViewType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemDisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<int>("Kind") + .HasColumnType("INTEGER"); + + b.Property<Guid?>("Permission_Permissions_Guid") + .HasColumnType("TEXT"); + + b.Property<uint>("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property<Guid?>("UserId") + .HasColumnType("TEXT"); + + b.Property<bool>("Value") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<int>("Kind") + .HasColumnType("INTEGER"); + + b.Property<Guid?>("Preference_Preferences_Guid") + .HasColumnType("TEXT"); + + b.Property<uint>("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property<Guid?>("UserId") + .HasColumnType("TEXT"); + + b.Property<string>("Value") + .IsRequired() + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Preferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<string>("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property<DateTime>("DateCreated") + .HasColumnType("TEXT"); + + b.Property<DateTime>("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property<string>("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccessToken") + .IsUnique(); + + b.ToTable("ApiKeys"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<string>("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property<string>("AppName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property<string>("AppVersion") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<DateTime>("DateCreated") + .HasColumnType("TEXT"); + + b.Property<DateTime>("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property<DateTime>("DateModified") + .HasColumnType("TEXT"); + + b.Property<string>("DeviceId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property<string>("DeviceName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property<bool>("IsActive") + .HasColumnType("INTEGER"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("AccessToken", "DateLastActivity"); + + b.HasIndex("DeviceId", "DateLastActivity"); + + b.HasIndex("UserId", "DeviceId"); + + b.ToTable("Devices"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<string>("CustomName") + .HasColumnType("TEXT"); + + b.Property<string>("DeviceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.ToTable("DeviceOptions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property<string>("AudioLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property<string>("AuthenticationProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property<string>("CastReceiverId") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<bool>("DisplayCollectionsView") + .HasColumnType("INTEGER"); + + b.Property<bool>("DisplayMissingEpisodes") + .HasColumnType("INTEGER"); + + b.Property<bool>("EnableAutoLogin") + .HasColumnType("INTEGER"); + + b.Property<bool>("EnableLocalPassword") + .HasColumnType("INTEGER"); + + b.Property<bool>("EnableNextEpisodeAutoPlay") + .HasColumnType("INTEGER"); + + b.Property<bool>("EnableUserPreferenceAccess") + .HasColumnType("INTEGER"); + + b.Property<bool>("HidePlayedInLatest") + .HasColumnType("INTEGER"); + + b.Property<long>("InternalId") + .HasColumnType("INTEGER"); + + b.Property<int>("InvalidLoginAttemptCount") + .HasColumnType("INTEGER"); + + b.Property<DateTime?>("LastActivityDate") + .HasColumnType("TEXT"); + + b.Property<DateTime?>("LastLoginDate") + .HasColumnType("TEXT"); + + b.Property<int?>("LoginAttemptsBeforeLockout") + .HasColumnType("INTEGER"); + + b.Property<int>("MaxActiveSessions") + .HasColumnType("INTEGER"); + + b.Property<int?>("MaxParentalAgeRating") + .HasColumnType("INTEGER"); + + b.Property<bool>("MustUpdatePassword") + .HasColumnType("INTEGER"); + + b.Property<string>("Password") + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.Property<string>("PasswordResetProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property<bool>("PlayDefaultAudioTrack") + .HasColumnType("INTEGER"); + + b.Property<bool>("RememberAudioSelections") + .HasColumnType("INTEGER"); + + b.Property<bool>("RememberSubtitleSelections") + .HasColumnType("INTEGER"); + + b.Property<int?>("RemoteClientBitrateLimit") + .HasColumnType("INTEGER"); + + b.Property<uint>("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property<string>("SubtitleLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property<int>("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property<int>("SyncPlayAccess") + .HasColumnType("INTEGER"); + + b.Property<string>("Username") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("AccessSchedules") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("DisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null) + .WithMany("HomeSections") + .HasForeignKey("DisplayPreferencesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithOne("ProfileImage") + .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("ItemDisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Permissions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Preferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => + { + b.HasOne("Jellyfin.Data.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Navigation("HomeSections"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Navigation("AccessSchedules"); + + b.Navigation("DisplayPreferences"); + + b.Navigation("ItemDisplayPreferences"); + + b.Navigation("Permissions"); + + b.Navigation("Preferences"); + + b.Navigation("ProfileImage"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.cs b/Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.cs new file mode 100644 index 0000000000..f06410c15a --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + /// <inheritdoc /> + public partial class UserCastReceiver : Migration + { + /// <inheritdoc /> + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn<string>( + name: "CastReceiverId", + table: "Users", + type: "TEXT", + maxLength: 32, + nullable: true); + } + + /// <inheritdoc /> + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "CastReceiverId", + table: "Users"); + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs index d23508096f..6cd4594f69 100644 --- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs +++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs @@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "7.0.5"); + modelBuilder.HasAnnotation("ProductVersion", "7.0.11"); modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => { @@ -457,6 +457,10 @@ namespace Jellyfin.Server.Implementations.Migrations .HasMaxLength(255) .HasColumnType("TEXT"); + b.Property<string>("CastReceiverId") + .HasMaxLength(32) + .HasColumnType("TEXT"); + b.Property<bool>("DisplayCollectionsView") .HasColumnType("INTEGER"); diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 5010751ddb..2a38fcecc4 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -15,6 +15,7 @@ using MediaBrowser.Common; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Authentication; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Events; using MediaBrowser.Controller.Library; @@ -45,6 +46,7 @@ namespace Jellyfin.Server.Implementations.Users private readonly InvalidAuthProvider _invalidAuthProvider; private readonly DefaultAuthenticationProvider _defaultAuthenticationProvider; private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider; + private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IDictionary<Guid, User> _users; @@ -58,6 +60,7 @@ namespace Jellyfin.Server.Implementations.Users /// <param name="appHost">The application host.</param> /// <param name="imageProcessor">The image processor.</param> /// <param name="logger">The logger.</param> + /// <param name="serverConfigurationManager">The system config manager.</param> public UserManager( IDbContextFactory<JellyfinDbContext> dbProvider, IEventManager eventManager, @@ -65,7 +68,8 @@ namespace Jellyfin.Server.Implementations.Users INetworkManager networkManager, IApplicationHost appHost, IImageProcessor imageProcessor, - ILogger<UserManager> logger) + ILogger<UserManager> logger, + IServerConfigurationManager serverConfigurationManager) { _dbProvider = dbProvider; _eventManager = eventManager; @@ -74,6 +78,7 @@ namespace Jellyfin.Server.Implementations.Users _appHost = appHost; _imageProcessor = imageProcessor; _logger = logger; + _serverConfigurationManager = serverConfigurationManager; _passwordResetProviders = appHost.GetExports<IPasswordResetProvider>(); _authenticationProviders = appHost.GetExports<IAuthenticationProvider>(); @@ -320,7 +325,10 @@ namespace Jellyfin.Server.Implementations.Users OrderedViews = user.GetPreferenceValues<Guid>(PreferenceKind.OrderedViews), GroupedFolders = user.GetPreferenceValues<Guid>(PreferenceKind.GroupedFolders), MyMediaExcludes = user.GetPreferenceValues<Guid>(PreferenceKind.MyMediaExcludes), - LatestItemsExcludes = user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes) + LatestItemsExcludes = user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes), + CastReceiverId = string.IsNullOrEmpty(user.CastReceiverId) + ? _serverConfigurationManager.Configuration.CastReceiverApplications.FirstOrDefault()?.Id + : user.CastReceiverId }, Policy = new UserPolicy { @@ -608,6 +616,10 @@ namespace Jellyfin.Server.Implementations.Users user.EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay; user.RememberSubtitleSelections = config.RememberSubtitleSelections; user.SubtitleLanguagePreference = config.SubtitleLanguagePreference; + if (!string.IsNullOrEmpty(config.CastReceiverId)) + { + user.CastReceiverId = config.CastReceiverId; + } user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews); user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders); diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs index 2db0b77cd9..757b56a496 100644 --- a/Jellyfin.Server/Migrations/MigrationRunner.cs +++ b/Jellyfin.Server/Migrations/MigrationRunner.cs @@ -42,7 +42,8 @@ namespace Jellyfin.Server.Migrations typeof(Routines.RemoveDownloadImagesInAdvance), typeof(Routines.MigrateAuthenticationDb), typeof(Routines.FixPlaylistOwner), - typeof(Routines.MigrateRatingLevels) + typeof(Routines.MigrateRatingLevels), + typeof(Routines.AddDefaultCastReceivers) }; /// <summary> diff --git a/Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs b/Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs new file mode 100644 index 0000000000..75a6a6176c --- /dev/null +++ b/Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs @@ -0,0 +1,55 @@ +using System; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Model.System; + +namespace Jellyfin.Server.Migrations.Routines; + +/// <summary> +/// Migration to add the default cast receivers to the system config. +/// </summary> +public class AddDefaultCastReceivers : IMigrationRoutine +{ + private readonly IServerConfigurationManager _serverConfigurationManager; + + /// <summary> + /// Initializes a new instance of the <see cref="AddDefaultCastReceivers"/> class. + /// </summary> + /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> + public AddDefaultCastReceivers(IServerConfigurationManager serverConfigurationManager) + { + _serverConfigurationManager = serverConfigurationManager; + } + + /// <inheritdoc /> + public Guid Id => new("34A1A1C4-5572-418E-A2F8-32CDFE2668E8"); + + /// <inheritdoc /> + public string Name => "AddDefaultCastReceivers"; + + /// <inheritdoc /> + public bool PerformOnNewInstall => true; + + /// <inheritdoc /> + public void Perform() + { + // Only add if receiver list is empty. + if (_serverConfigurationManager.Configuration.CastReceiverApplications.Length == 0) + { + _serverConfigurationManager.Configuration.CastReceiverApplications = new CastReceiverApplication[] + { + new() + { + Id = "F007D354", + Name = "Stable" + }, + new() + { + Id = "6F511C87", + Name = "Unstable" + } + }; + + _serverConfigurationManager.SaveConfiguration(); + } + } +} diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 78a310f0b1..1342ffb48b 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -4,265 +4,270 @@ using System; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; +using MediaBrowser.Model.System; using MediaBrowser.Model.Updates; -namespace MediaBrowser.Model.Configuration +namespace MediaBrowser.Model.Configuration; + +/// <summary> +/// Represents the server configuration. +/// </summary> +public class ServerConfiguration : BaseApplicationConfiguration { /// <summary> - /// Represents the server configuration. + /// Initializes a new instance of the <see cref="ServerConfiguration" /> class. /// </summary> - public class ServerConfiguration : BaseApplicationConfiguration + public ServerConfiguration() { - /// <summary> - /// Initializes a new instance of the <see cref="ServerConfiguration" /> class. - /// </summary> - public ServerConfiguration() + MetadataOptions = new[] { - MetadataOptions = new[] + new MetadataOptions() { - new MetadataOptions() - { - ItemType = "Book" - }, - new MetadataOptions() - { - ItemType = "Movie" - }, - new MetadataOptions - { - ItemType = "MusicVideo", - DisabledMetadataFetchers = new[] { "The Open Movie Database" }, - DisabledImageFetchers = new[] { "The Open Movie Database" } - }, - new MetadataOptions - { - ItemType = "Series", - }, - new MetadataOptions - { - ItemType = "MusicAlbum", - DisabledMetadataFetchers = new[] { "TheAudioDB" } - }, - new MetadataOptions - { - ItemType = "MusicArtist", - DisabledMetadataFetchers = new[] { "TheAudioDB" } - }, - new MetadataOptions - { - ItemType = "BoxSet" - }, - new MetadataOptions - { - ItemType = "Season", - }, - new MetadataOptions - { - ItemType = "Episode", - } - }; - } - - /// <summary> - /// Gets or sets a value indicating whether to enable prometheus metrics exporting. - /// </summary> - public bool EnableMetrics { get; set; } = false; - - public bool EnableNormalizedItemByNameIds { get; set; } = true; - - /// <summary> - /// Gets or sets a value indicating whether this instance is port authorized. - /// </summary> - /// <value><c>true</c> if this instance is port authorized; otherwise, <c>false</c>.</value> - public bool IsPortAuthorized { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether quick connect is available for use on this server. - /// </summary> - public bool QuickConnectAvailable { get; set; } = true; - - /// <summary> - /// Gets or sets a value indicating whether [enable case sensitive item ids]. - /// </summary> - /// <value><c>true</c> if [enable case sensitive item ids]; otherwise, <c>false</c>.</value> - public bool EnableCaseSensitiveItemIds { get; set; } = true; - - public bool DisableLiveTvChannelUserDataName { get; set; } = true; - - /// <summary> - /// Gets or sets the metadata path. - /// </summary> - /// <value>The metadata path.</value> - public string MetadataPath { get; set; } = string.Empty; - - public string MetadataNetworkPath { get; set; } = string.Empty; - - /// <summary> - /// Gets or sets the preferred metadata language. - /// </summary> - /// <value>The preferred metadata language.</value> - public string PreferredMetadataLanguage { get; set; } = "en"; - - /// <summary> - /// Gets or sets the metadata country code. - /// </summary> - /// <value>The metadata country code.</value> - public string MetadataCountryCode { get; set; } = "US"; - - /// <summary> - /// Gets or sets characters to be replaced with a ' ' in strings to create a sort name. - /// </summary> - /// <value>The sort replace characters.</value> - public string[] SortReplaceCharacters { get; set; } = new[] { ".", "+", "%" }; - - /// <summary> - /// Gets or sets characters to be removed from strings to create a sort name. - /// </summary> - /// <value>The sort remove characters.</value> - public string[] SortRemoveCharacters { get; set; } = new[] { ",", "&", "-", "{", "}", "'" }; - - /// <summary> - /// Gets or sets words to be removed from strings to create a sort name. - /// </summary> - /// <value>The sort remove words.</value> - public string[] SortRemoveWords { get; set; } = new[] { "the", "a", "an" }; - - /// <summary> - /// Gets or sets the minimum percentage of an item that must be played in order for playstate to be updated. - /// </summary> - /// <value>The min resume PCT.</value> - public int MinResumePct { get; set; } = 5; - - /// <summary> - /// Gets or sets the maximum percentage of an item that can be played while still saving playstate. If this percentage is crossed playstate will be reset to the beginning and the item will be marked watched. - /// </summary> - /// <value>The max resume PCT.</value> - public int MaxResumePct { get; set; } = 90; - - /// <summary> - /// Gets or sets the minimum duration that an item must have in order to be eligible for playstate updates.. - /// </summary> - /// <value>The min resume duration seconds.</value> - public int MinResumeDurationSeconds { get; set; } = 300; - - /// <summary> - /// Gets or sets the minimum minutes of a book that must be played in order for playstate to be updated. - /// </summary> - /// <value>The min resume in minutes.</value> - public int MinAudiobookResume { get; set; } = 5; - - /// <summary> - /// Gets or sets the remaining minutes of a book that can be played while still saving playstate. If this percentage is crossed playstate will be reset to the beginning and the item will be marked watched. - /// </summary> - /// <value>The remaining time in minutes.</value> - public int MaxAudiobookResume { get; set; } = 5; - - /// <summary> - /// Gets or sets the delay in seconds that we will wait after a file system change to try and discover what has been added/removed - /// Some delay is necessary with some items because their creation is not atomic. It involves the creation of several - /// different directories and files. - /// </summary> - /// <value>The file watcher delay.</value> - public int LibraryMonitorDelay { get; set; } = 60; - - /// <summary> - /// Gets or sets the duration in seconds that we will wait after a library updated event before executing the library changed notification. - /// </summary> - /// <value>The library update duration.</value> - public int LibraryUpdateDuration { get; set; } = 30; - - /// <summary> - /// Gets or sets the image saving convention. - /// </summary> - /// <value>The image saving convention.</value> - public ImageSavingConvention ImageSavingConvention { get; set; } - - public MetadataOptions[] MetadataOptions { get; set; } - - public bool SkipDeserializationForBasicTypes { get; set; } = true; - - public string ServerName { get; set; } = string.Empty; - - public string UICulture { get; set; } = "en-US"; - - public bool SaveMetadataHidden { get; set; } = false; - - public NameValuePair[] ContentTypes { get; set; } = Array.Empty<NameValuePair>(); - - public int RemoteClientBitrateLimit { get; set; } - - public bool EnableFolderView { get; set; } = false; - - public bool EnableGroupingIntoCollections { get; set; } = false; - - public bool DisplaySpecialsWithinSeasons { get; set; } = true; - - public string[] CodecsUsed { get; set; } = Array.Empty<string>(); - - public RepositoryInfo[] PluginRepositories { get; set; } = Array.Empty<RepositoryInfo>(); - - public bool EnableExternalContentInSuggestions { get; set; } = true; - - public int ImageExtractionTimeoutMs { get; set; } - - public PathSubstitution[] PathSubstitutions { get; set; } = Array.Empty<PathSubstitution>(); - - /// <summary> - /// Gets or sets a value indicating whether slow server responses should be logged as a warning. - /// </summary> - public bool EnableSlowResponseWarning { get; set; } = true; - - /// <summary> - /// Gets or sets the threshold for the slow response time warning in ms. - /// </summary> - public long SlowResponseThresholdMs { get; set; } = 500; - - /// <summary> - /// Gets or sets the cors hosts. - /// </summary> - public string[] CorsHosts { get; set; } = new[] { "*" }; - - /// <summary> - /// Gets or sets the number of days we should retain activity logs. - /// </summary> - public int? ActivityLogRetentionDays { get; set; } = 30; - - /// <summary> - /// Gets or sets the how the library scan fans out. - /// </summary> - public int LibraryScanFanoutConcurrency { get; set; } - - /// <summary> - /// Gets or sets the how many metadata refreshes can run concurrently. - /// </summary> - public int LibraryMetadataRefreshConcurrency { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether older plugins should automatically be deleted from the plugin folder. - /// </summary> - public bool RemoveOldPlugins { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether clients should be allowed to upload logs. - /// </summary> - public bool AllowClientLogUpload { get; set; } = true; - - /// <summary> - /// Gets or sets the dummy chapter duration in seconds, use 0 (zero) or less to disable generation alltogether. - /// </summary> - /// <value>The dummy chapters duration.</value> - public int DummyChapterDuration { get; set; } - - /// <summary> - /// Gets or sets the chapter image resolution. - /// </summary> - /// <value>The chapter image resolution.</value> - public ImageResolution ChapterImageResolution { get; set; } = ImageResolution.MatchSource; - - /// <summary> - /// Gets or sets the limit for parallel image encoding. - /// </summary> - /// <value>The limit for parallel image encoding.</value> - public int ParallelImageEncodingLimit { get; set; } + ItemType = "Book" + }, + new MetadataOptions() + { + ItemType = "Movie" + }, + new MetadataOptions + { + ItemType = "MusicVideo", + DisabledMetadataFetchers = new[] { "The Open Movie Database" }, + DisabledImageFetchers = new[] { "The Open Movie Database" } + }, + new MetadataOptions + { + ItemType = "Series", + }, + new MetadataOptions + { + ItemType = "MusicAlbum", + DisabledMetadataFetchers = new[] { "TheAudioDB" } + }, + new MetadataOptions + { + ItemType = "MusicArtist", + DisabledMetadataFetchers = new[] { "TheAudioDB" } + }, + new MetadataOptions + { + ItemType = "BoxSet" + }, + new MetadataOptions + { + ItemType = "Season", + }, + new MetadataOptions + { + ItemType = "Episode", + } + }; } + + /// <summary> + /// Gets or sets a value indicating whether to enable prometheus metrics exporting. + /// </summary> + public bool EnableMetrics { get; set; } = false; + + public bool EnableNormalizedItemByNameIds { get; set; } = true; + + /// <summary> + /// Gets or sets a value indicating whether this instance is port authorized. + /// </summary> + /// <value><c>true</c> if this instance is port authorized; otherwise, <c>false</c>.</value> + public bool IsPortAuthorized { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether quick connect is available for use on this server. + /// </summary> + public bool QuickConnectAvailable { get; set; } = true; + + /// <summary> + /// Gets or sets a value indicating whether [enable case sensitive item ids]. + /// </summary> + /// <value><c>true</c> if [enable case sensitive item ids]; otherwise, <c>false</c>.</value> + public bool EnableCaseSensitiveItemIds { get; set; } = true; + + public bool DisableLiveTvChannelUserDataName { get; set; } = true; + + /// <summary> + /// Gets or sets the metadata path. + /// </summary> + /// <value>The metadata path.</value> + public string MetadataPath { get; set; } = string.Empty; + + public string MetadataNetworkPath { get; set; } = string.Empty; + + /// <summary> + /// Gets or sets the preferred metadata language. + /// </summary> + /// <value>The preferred metadata language.</value> + public string PreferredMetadataLanguage { get; set; } = "en"; + + /// <summary> + /// Gets or sets the metadata country code. + /// </summary> + /// <value>The metadata country code.</value> + public string MetadataCountryCode { get; set; } = "US"; + + /// <summary> + /// Gets or sets characters to be replaced with a ' ' in strings to create a sort name. + /// </summary> + /// <value>The sort replace characters.</value> + public string[] SortReplaceCharacters { get; set; } = new[] { ".", "+", "%" }; + + /// <summary> + /// Gets or sets characters to be removed from strings to create a sort name. + /// </summary> + /// <value>The sort remove characters.</value> + public string[] SortRemoveCharacters { get; set; } = new[] { ",", "&", "-", "{", "}", "'" }; + + /// <summary> + /// Gets or sets words to be removed from strings to create a sort name. + /// </summary> + /// <value>The sort remove words.</value> + public string[] SortRemoveWords { get; set; } = new[] { "the", "a", "an" }; + + /// <summary> + /// Gets or sets the minimum percentage of an item that must be played in order for playstate to be updated. + /// </summary> + /// <value>The min resume PCT.</value> + public int MinResumePct { get; set; } = 5; + + /// <summary> + /// Gets or sets the maximum percentage of an item that can be played while still saving playstate. If this percentage is crossed playstate will be reset to the beginning and the item will be marked watched. + /// </summary> + /// <value>The max resume PCT.</value> + public int MaxResumePct { get; set; } = 90; + + /// <summary> + /// Gets or sets the minimum duration that an item must have in order to be eligible for playstate updates.. + /// </summary> + /// <value>The min resume duration seconds.</value> + public int MinResumeDurationSeconds { get; set; } = 300; + + /// <summary> + /// Gets or sets the minimum minutes of a book that must be played in order for playstate to be updated. + /// </summary> + /// <value>The min resume in minutes.</value> + public int MinAudiobookResume { get; set; } = 5; + + /// <summary> + /// Gets or sets the remaining minutes of a book that can be played while still saving playstate. If this percentage is crossed playstate will be reset to the beginning and the item will be marked watched. + /// </summary> + /// <value>The remaining time in minutes.</value> + public int MaxAudiobookResume { get; set; } = 5; + + /// <summary> + /// Gets or sets the delay in seconds that we will wait after a file system change to try and discover what has been added/removed + /// Some delay is necessary with some items because their creation is not atomic. It involves the creation of several + /// different directories and files. + /// </summary> + /// <value>The file watcher delay.</value> + public int LibraryMonitorDelay { get; set; } = 60; + + /// <summary> + /// Gets or sets the duration in seconds that we will wait after a library updated event before executing the library changed notification. + /// </summary> + /// <value>The library update duration.</value> + public int LibraryUpdateDuration { get; set; } = 30; + + /// <summary> + /// Gets or sets the image saving convention. + /// </summary> + /// <value>The image saving convention.</value> + public ImageSavingConvention ImageSavingConvention { get; set; } + + public MetadataOptions[] MetadataOptions { get; set; } + + public bool SkipDeserializationForBasicTypes { get; set; } = true; + + public string ServerName { get; set; } = string.Empty; + + public string UICulture { get; set; } = "en-US"; + + public bool SaveMetadataHidden { get; set; } = false; + + public NameValuePair[] ContentTypes { get; set; } = Array.Empty<NameValuePair>(); + + public int RemoteClientBitrateLimit { get; set; } + + public bool EnableFolderView { get; set; } = false; + + public bool EnableGroupingIntoCollections { get; set; } = false; + + public bool DisplaySpecialsWithinSeasons { get; set; } = true; + + public string[] CodecsUsed { get; set; } = Array.Empty<string>(); + + public RepositoryInfo[] PluginRepositories { get; set; } = Array.Empty<RepositoryInfo>(); + + public bool EnableExternalContentInSuggestions { get; set; } = true; + + public int ImageExtractionTimeoutMs { get; set; } + + public PathSubstitution[] PathSubstitutions { get; set; } = Array.Empty<PathSubstitution>(); + + /// <summary> + /// Gets or sets a value indicating whether slow server responses should be logged as a warning. + /// </summary> + public bool EnableSlowResponseWarning { get; set; } = true; + + /// <summary> + /// Gets or sets the threshold for the slow response time warning in ms. + /// </summary> + public long SlowResponseThresholdMs { get; set; } = 500; + + /// <summary> + /// Gets or sets the cors hosts. + /// </summary> + public string[] CorsHosts { get; set; } = new[] { "*" }; + + /// <summary> + /// Gets or sets the number of days we should retain activity logs. + /// </summary> + public int? ActivityLogRetentionDays { get; set; } = 30; + + /// <summary> + /// Gets or sets the how the library scan fans out. + /// </summary> + public int LibraryScanFanoutConcurrency { get; set; } + + /// <summary> + /// Gets or sets the how many metadata refreshes can run concurrently. + /// </summary> + public int LibraryMetadataRefreshConcurrency { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether older plugins should automatically be deleted from the plugin folder. + /// </summary> + public bool RemoveOldPlugins { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether clients should be allowed to upload logs. + /// </summary> + public bool AllowClientLogUpload { get; set; } = true; + + /// <summary> + /// Gets or sets the dummy chapter duration in seconds, use 0 (zero) or less to disable generation alltogether. + /// </summary> + /// <value>The dummy chapters duration.</value> + public int DummyChapterDuration { get; set; } + + /// <summary> + /// Gets or sets the chapter image resolution. + /// </summary> + /// <value>The chapter image resolution.</value> + public ImageResolution ChapterImageResolution { get; set; } = ImageResolution.MatchSource; + + /// <summary> + /// Gets or sets the limit for parallel image encoding. + /// </summary> + /// <value>The limit for parallel image encoding.</value> + public int ParallelImageEncodingLimit { get; set; } + + /// <summary> + /// Gets or sets the list of cast receiver applications. + /// </summary> + public CastReceiverApplication[] CastReceiverApplications { get; set; } = Array.Empty<CastReceiverApplication>(); } diff --git a/MediaBrowser.Model/Configuration/UserConfiguration.cs b/MediaBrowser.Model/Configuration/UserConfiguration.cs index 94f3546608..b477f2593a 100644 --- a/MediaBrowser.Model/Configuration/UserConfiguration.cs +++ b/MediaBrowser.Model/Configuration/UserConfiguration.cs @@ -69,5 +69,10 @@ namespace MediaBrowser.Model.Configuration public bool RememberSubtitleSelections { get; set; } public bool EnableNextEpisodeAutoPlay { get; set; } + + /// <summary> + /// Gets or sets the id of the selected cast receiver. + /// </summary> + public string? CastReceiverId { get; set; } } } diff --git a/MediaBrowser.Model/System/CastReceiverApplication.cs b/MediaBrowser.Model/System/CastReceiverApplication.cs new file mode 100644 index 0000000000..6a49a5cac4 --- /dev/null +++ b/MediaBrowser.Model/System/CastReceiverApplication.cs @@ -0,0 +1,17 @@ +namespace MediaBrowser.Model.System; + +/// <summary> +/// The cast receiver application model. +/// </summary> +public class CastReceiverApplication +{ + /// <summary> + /// Gets or sets the cast receiver application id. + /// </summary> + public required string Id { get; set; } + + /// <summary> + /// Gets or sets the cast receiver application name. + /// </summary> + public required string Name { get; set; } +} diff --git a/MediaBrowser.Model/System/SystemInfo.cs b/MediaBrowser.Model/System/SystemInfo.cs index bd0099af70..d9544b40f0 100644 --- a/MediaBrowser.Model/System/SystemInfo.cs +++ b/MediaBrowser.Model/System/SystemInfo.cs @@ -2,6 +2,7 @@ #pragma warning disable CS1591 using System; +using System.Collections.Generic; using System.Runtime.InteropServices; using MediaBrowser.Model.Updates; @@ -128,6 +129,11 @@ namespace MediaBrowser.Model.System /// <value>The transcode path.</value> public string TranscodingTempPath { get; set; } + /// <summary> + /// Gets or sets the list of cast receiver applications. + /// </summary> + public IReadOnlyList<CastReceiverApplication> CastReceiverApplications { get; set; } + /// <summary> /// Gets or sets a value indicating whether this instance has update available. /// </summary> From effa303cb9804f48cac9a74c3c2370ee29fbd477 Mon Sep 17 00:00:00 2001 From: Cody Robibero <cody@robibe.ro> Date: Sat, 23 Sep 2023 15:58:03 -0600 Subject: [PATCH 602/858] Add missing LocalAccessOrRequiresElevationHandler (#10268) --- Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 3271e08e48..89dbbdd2fe 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -59,6 +59,7 @@ namespace Jellyfin.Server.Extensions serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeSetupHandler>(); serviceCollection.AddSingleton<IAuthorizationHandler, AnonymousLanAccessHandler>(); serviceCollection.AddSingleton<IAuthorizationHandler, SyncPlayAccessHandler>(); + serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessOrRequiresElevationHandler>(); return serviceCollection.AddAuthorizationCore(options => { From bc88c96cbe13301ee6e5f6a2d688a4c90338281d Mon Sep 17 00:00:00 2001 From: Cody Robibero <cody@robibe.ro> Date: Sat, 23 Sep 2023 16:14:03 -0600 Subject: [PATCH 603/858] Validate cast receiver id on get/set --- Jellyfin.Server.Implementations/Users/UserManager.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 2a38fcecc4..108d2fad4f 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -328,7 +328,8 @@ namespace Jellyfin.Server.Implementations.Users LatestItemsExcludes = user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes), CastReceiverId = string.IsNullOrEmpty(user.CastReceiverId) ? _serverConfigurationManager.Configuration.CastReceiverApplications.FirstOrDefault()?.Id - : user.CastReceiverId + : _serverConfigurationManager.Configuration.CastReceiverApplications.FirstOrDefault(c => string.Equals(c.Id, user.CastReceiverId, StringComparison.Ordinal))?.Id + ?? _serverConfigurationManager.Configuration.CastReceiverApplications.FirstOrDefault()?.Id }, Policy = new UserPolicy { @@ -616,7 +617,10 @@ namespace Jellyfin.Server.Implementations.Users user.EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay; user.RememberSubtitleSelections = config.RememberSubtitleSelections; user.SubtitleLanguagePreference = config.SubtitleLanguagePreference; - if (!string.IsNullOrEmpty(config.CastReceiverId)) + + // Only set cast receiver id if it is passed in and it exists in the server config. + if (!string.IsNullOrEmpty(config.CastReceiverId) + && _serverConfigurationManager.Configuration.CastReceiverApplications.Any(c => string.Equals(c.Id, config.CastReceiverId, StringComparison.Ordinal))) { user.CastReceiverId = config.CastReceiverId; } From 0eddc3e6adb65bea097115694469ecc1cd14c845 Mon Sep 17 00:00:00 2001 From: trailfullideal <book.plot.see@cloak.id> Date: Sun, 24 Sep 2023 11:18:28 -0400 Subject: [PATCH 604/858] Added translation using Weblate (Cherokee) --- Emby.Server.Implementations/Localization/Core/chr.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 Emby.Server.Implementations/Localization/Core/chr.json diff --git a/Emby.Server.Implementations/Localization/Core/chr.json b/Emby.Server.Implementations/Localization/Core/chr.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/chr.json @@ -0,0 +1 @@ +{} From 79976b6b48ccbc8ba828051a8751984a969f6fbc Mon Sep 17 00:00:00 2001 From: trailfullideal <book.plot.see@cloak.id> Date: Sun, 24 Sep 2023 15:14:19 +0000 Subject: [PATCH 605/858] Translated using Weblate (Zulu) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zu/ --- Emby.Server.Implementations/Localization/Core/zu.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/zu.json b/Emby.Server.Implementations/Localization/Core/zu.json index b5f4b920f3..aa056d4498 100644 --- a/Emby.Server.Implementations/Localization/Core/zu.json +++ b/Emby.Server.Implementations/Localization/Core/zu.json @@ -25,5 +25,14 @@ "Channels": "Amashaneli", "Books": "Izincwadi", "Artists": "Abadlali", - "Albums": "Ama-albhamu" + "Albums": "Ama-albhamu", + "CameraImageUploadedFrom": "Kulandelayo lwesithonjana sekhamera selithunyelwe kusuka ku {0}", + "HeaderFavoriteArtists": "Abasethi Abathandekayo", + "HeaderFavoriteEpisodes": "Izilimi Ezithandekayo", + "HeaderFavoriteShows": "Izisho Ezithandekayo", + "External": "Kwezifungo", + "FailedLoginAttemptWithUserName": "Ukushayiswa kwesithombe sokungena okungekho {0}", + "HeaderContinueWatching": "Buyela Ukubona", + "HeaderFavoriteAlbums": "Izimpahla Ezithandwayo", + "HeaderAlbumArtists": "Abasethi wenkulumo" } From 3229d3ba0236243fd2116b78376400a76d9d70dd Mon Sep 17 00:00:00 2001 From: trailfullideal <book.plot.see@cloak.id> Date: Sun, 24 Sep 2023 14:52:43 +0000 Subject: [PATCH 606/858] Translated using Weblate (Assamese) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/as/ --- .../Localization/Core/as.json | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/as.json b/Emby.Server.Implementations/Localization/Core/as.json index 0967ef424b..7c7dd26e92 100644 --- a/Emby.Server.Implementations/Localization/Core/as.json +++ b/Emby.Server.Implementations/Localization/Core/as.json @@ -1 +1,43 @@ -{} +{ + "Albums": "এলবাম", + "Application": "আবেদন", + "AppDeviceValues": "এপ্‌: {0}, ডিভাইচ: {1}", + "Artists": "শিল্পী", + "Channels": "চেনেলস", + "Default": "ডিফল্ট", + "AuthenticationSucceededWithUserName": "{0} সফলভাবে প্রমাণিত", + "Books": "পুস্তক", + "Movies": "চলচ্চিত্ৰ", + "CameraImageUploadedFrom": "একটি নতুন ক্যামেরা চিত্র আপলোড করা হয়েছে {0}", + "Collections": "সংগ্রহ", + "HeaderFavoriteShows": "প্রিয় শোসমূহ", + "Latest": "শেহতীয়া", + "MessageApplicationUpdated": "জেলিফিন চাইভাৰ আপডেট কৰা হৈছে", + "MixedContent": "মিশ্ৰিত সমগ্ৰতা", + "NewVersionIsAvailable": "ডাউনলোড কৰিবলৈ জেলিফিন চাইভাৰৰ এটা নতুন সংস্কৰণ উপলব্ধ আছে.", + "NotificationOptionCameraImageUploaded": "কেমেৰাৰ চিত্ৰ আপল'ড কৰা হ'ল", + "External": "বাহ্যিক", + "Favorites": "পছন্দসই", + "Folders": "ফোল্ডাৰ", + "Forced": "বলপূর্বক", + "Genres": "শ্রেণী", + "HeaderAlbumArtists": "অ্যালবাম শিল্পী", + "HeaderContinueWatching": "দেখা চালিয়ে যান", + "FailedLoginAttemptWithUserName": "লগইন ব্যর্থ চেষ্টা কৰা হৈছে থেকে {0}", + "HeaderFavoriteAlbums": "প্রিয় অ্যালবামসমূহ", + "HeaderFavoriteArtists": "প্রিয় শিল্পীসমূহ", + "HeaderFavoriteEpisodes": "প্রিয় পর্বসমূহ", + "HeaderFavoriteSongs": "প্ৰিয় গীত", + "HeaderLiveTV": "প্ৰতিবেদন টিভি", + "HeaderNextUp": "পৰৱৰ্তী অংশ", + "HeaderRecordingGroups": "অলংকৰণ গোষ্ঠীসমূহ", + "HearingImpaired": "শ্ৰবণ অক্ষম", + "HomeVideos": "ঘৰৰ ভিডিঅ'সমূহ", + "Inherit": "উত্তপ্ত কৰা", + "MessageServerConfigurationUpdated": "চাইভাৰ কনফিগাৰেশ্যন আপডেট কৰা হৈছে", + "NotificationOptionApplicationUpdateAvailable": "অ্যাপ্লিকেশ্যন আপডেট উপলব্ধ", + "NotificationOptionApplicationUpdateInstalled": "অ্যাপ্লিকেশ্যন আপডেট ইনষ্টল কৰা হ'ল", + "NotificationOptionAudioPlayback": "অডিঅ' প্লেবেক আৰম্ভ হ'ল", + "NotificationOptionAudioPlaybackStopped": "অডিঅ' প্লেবেক আঁতৰ হ'ল", + "NotificationOptionInstallationFailed": "ইনষ্টলেশ্যন ব্যৰ্থতা" +} From 99cc1ed13ac8ca3da4138500644515941f6390f6 Mon Sep 17 00:00:00 2001 From: Nyanmisaka <nst799610810@gmail.com> Date: Mon, 25 Sep 2023 22:56:59 +0800 Subject: [PATCH 607/858] Fix A53 CC SEI breaking H26x_VAAPI hardware encode Signed-off-by: nyanmisaka <nst799610810@gmail.com> --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index b6e680ab97..c311d3b8ab 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -48,6 +48,7 @@ namespace MediaBrowser.Controller.MediaEncoding private readonly Version _minFFmpegHwaUnsafeOutput = new Version(6, 0); private readonly Version _minFFmpegOclCuTonemapMode = new Version(5, 1, 3); private readonly Version _minFFmpegSvtAv1Params = new Version(5, 1); + private readonly Version _minFFmpegVaapiH26xEncA53CcSei = new Version(6, 0); private static readonly string[] _videoProfilesH264 = new[] { @@ -2006,6 +2007,14 @@ namespace MediaBrowser.Controller.MediaEncoding param += " -svtav1-params:0 rc=1:tune=0:film-grain=0:enable-overlays=1:enable-tf=0"; } + /* Access unit too large: 8192 < 20880 error */ + if ((string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) || + string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)) && + _mediaEncoder.EncoderVersion >= _minFFmpegVaapiH26xEncA53CcSei) + { + param += " -sei -a53_cc"; + } + return param; } From b17ee80652237102aa87e407648efaddbf032962 Mon Sep 17 00:00:00 2001 From: trailfullideal <book.plot.see@cloak.id> Date: Sun, 24 Sep 2023 15:22:17 +0000 Subject: [PATCH 608/858] Translated using Weblate (Cherokee) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/chr/ --- .../Localization/Core/chr.json | 53 ++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/chr.json b/Emby.Server.Implementations/Localization/Core/chr.json index 0967ef424b..85d1f4c881 100644 --- a/Emby.Server.Implementations/Localization/Core/chr.json +++ b/Emby.Server.Implementations/Localization/Core/chr.json @@ -1 +1,52 @@ -{} +{ + "ChapterNameValue": "Didanedi {0}", + "HeaderAlbumArtists": "Didanidanolisgisgi", + "HeaderFavoriteAlbums": "Dvganidi didanidisgisgi", + "HeaderLiveTV": "Anigadi didanidisgosgi", + "HeaderRecordingGroups": "Didanisquodiisgisgi", + "HomeVideos": "Diganadi dinagadisgisgi", + "Inherit": "Anigwe", + "MessageApplicationUpdatedTo": "Tsenigwidinonvhi Jellyfin Server tsadanidigwe anigadi {0}", + "MixedContent": "Ganinidi dininoladisgisgi", + "Movies": "Anidvnisgisgi", + "MusicVideos": "Danodisgisgi didanidisgosgi", + "NotificationOptionAudioPlayback": "Didanidigwe diganuyisgisgi anigadi", + "NotificationOptionInstallationFailed": "Diudvdi anadvnatisgisgi", + "NotificationOptionPluginUninstalled": "Ditsigvhnidv anawvdisgisgi", + "Albums": "Anigawidaniyv", + "Application": "Didanvyi", + "Artists": "Dinidaniyi", + "AuthenticationSucceededWithUserName": "{0} Sesoquonisdi nagadani", + "Books": "Didanedi", + "CameraImageUploadedFrom": "Anigawidaniyv nasgi didagwalanvyi {0}", + "Channels": "Diganadasgi", + "Collections": "Diganadisgi", + "Default": "Dinadi", + "DeviceOfflineWithName": "{0} Aniyvolehvi nasgi", + "External": "Amohdi", + "Favorites": "Nvdayelvdisgi", + "Folders": "Didanididisgi", + "Forced": "Ganedi", + "Genres": "Diganadisgi", + "HeaderContinueWatching": "Uwoditsu asdanidisgisgi", + "HeaderFavoriteArtists": "Dvganidi dinidanolisgisgi", + "HeaderFavoriteEpisodes": "Dvganidi didanidilisgadisgisgi", + "HeaderFavoriteShows": "Dvganidi didanididanolisgisgi)", + "HeaderFavoriteSongs": "Dvganidi danodisgisgi", + "HeaderNextUp": "Anidvli uwodoli", + "HearingImpaired": "Anitsunidi talunidisgisgi", + "ItemAddedWithName": "{0} Dinigwe anididanidisgi", + "Latest": "Uwodoli", + "MessageApplicationUpdated": "Tsenigwidinonvhi Jellyfin Server tsadanidigwe", + "MessageServerConfigurationUpdated": "Sedanidvdi anigadi diganidinonvhi", + "Music": "Danodisgisgi", + "NameSeasonUnknown": "Tsunita anidvdisgi", + "NewVersionIsAvailable": "Danodigwe anigadi Jellyfin Server tsadanidigwe adisdi uwodvdi diganidinonvhi.", + "NotificationOptionApplicationUpdateAvailable": "Disisdi tsadanidigwe udvdi", + "NotificationOptionApplicationUpdateInstalled": "Disisdi tsadanidigwe digawvdi", + "NotificationOptionAudioPlaybackStopped": "Didanidigwe diganuyisgisgi digawvdi", + "NotificationOptionCameraImageUploaded": "Asdayi adininisgisgi diganuyisgisgi", + "NotificationOptionNewLibraryContent": "Danodisgisgi anigadi digawvdi", + "NotificationOptionPluginError": "Ditsigvhnidv anadvnatisgisgi", + "NotificationOptionPluginInstalled": "Ditsigvhnidv digawvdi" +} From 526c918524468cab1044b93ac47cdf9b3fff59bd Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Mon, 25 Sep 2023 18:12:12 +0200 Subject: [PATCH 609/858] CollectionFolder: replace Dictionary + locks with ConcurrentDictionary This should be faster (and still safe I hope) --- .../Entities/CollectionFolder.cs | 42 ++++++------------- 1 file changed, 12 insertions(+), 30 deletions(-) diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs index 095b261c05..f51162f9d2 100644 --- a/MediaBrowser.Controller/Entities/CollectionFolder.cs +++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs @@ -3,6 +3,7 @@ #pragma warning disable CS1591 using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; @@ -29,7 +30,7 @@ namespace MediaBrowser.Controller.Entities public class CollectionFolder : Folder, ICollectionFolder { private static readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; - private static readonly Dictionary<string, LibraryOptions> _libraryOptions = new Dictionary<string, LibraryOptions>(); + private static readonly ConcurrentDictionary<string, LibraryOptions> _libraryOptions = new ConcurrentDictionary<string, LibraryOptions>(); private bool _requiresRefresh; /// <summary> @@ -139,45 +140,26 @@ namespace MediaBrowser.Controller.Entities } public static LibraryOptions GetLibraryOptions(string path) - { - lock (_libraryOptions) - { - if (!_libraryOptions.TryGetValue(path, out var options)) - { - options = LoadLibraryOptions(path); - _libraryOptions[path] = options; - } - - return options; - } - } + => _libraryOptions.GetOrAdd(path, LoadLibraryOptions); public static void SaveLibraryOptions(string path, LibraryOptions options) { - lock (_libraryOptions) + _libraryOptions[path] = options; + + var clone = JsonSerializer.Deserialize<LibraryOptions>(JsonSerializer.SerializeToUtf8Bytes(options, _jsonOptions), _jsonOptions); + foreach (var mediaPath in clone.PathInfos) { - _libraryOptions[path] = options; - - var clone = JsonSerializer.Deserialize<LibraryOptions>(JsonSerializer.SerializeToUtf8Bytes(options, _jsonOptions), _jsonOptions); - foreach (var mediaPath in clone.PathInfos) + if (!string.IsNullOrEmpty(mediaPath.Path)) { - if (!string.IsNullOrEmpty(mediaPath.Path)) - { - mediaPath.Path = ApplicationHost.ReverseVirtualPath(mediaPath.Path); - } + mediaPath.Path = ApplicationHost.ReverseVirtualPath(mediaPath.Path); } - - XmlSerializer.SerializeToFile(clone, GetLibraryOptionsPath(path)); } + + XmlSerializer.SerializeToFile(clone, GetLibraryOptionsPath(path)); } public static void OnCollectionFolderChange() - { - lock (_libraryOptions) - { - _libraryOptions.Clear(); - } - } + => _libraryOptions.Clear(); public override bool IsSaveLocalMetadataEnabled() { From 57891e763972705f2db88a46e03e4e9b74d4621b Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Wed, 13 Sep 2023 23:36:46 +0200 Subject: [PATCH 610/858] PhotoResolver: change how generated images are detected Backdrops/fanart are generated as (backdrop)|(fanart)[0-9]*.extension Fixes #7830 --- .../Library/Resolvers/PhotoResolver.cs | 31 +++++++++---------- .../Resolvers/IItemResolver.cs | 2 +- .../Resolvers/ItemResolver.cs | 6 ++-- 3 files changed, 18 insertions(+), 21 deletions(-) diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs index 9026160ff0..b77c6b204b 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Collections.Generic; using System.IO; @@ -25,7 +23,7 @@ namespace Emby.Server.Implementations.Library.Resolvers private readonly NamingOptions _namingOptions; private readonly IDirectoryService _directoryService; - private static readonly HashSet<string> _ignoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase) + private static readonly string[] _ignoreFiles = new[] { "folder", "thumb", @@ -56,7 +54,7 @@ namespace Emby.Server.Implementations.Library.Resolvers /// </summary> /// <param name="args">The args.</param> /// <returns>Trailer.</returns> - protected override Photo Resolve(ItemResolveArgs args) + protected override Photo? Resolve(ItemResolveArgs args) { if (!args.IsDirectory) { @@ -68,10 +66,11 @@ namespace Emby.Server.Implementations.Library.Resolvers { if (IsImageFile(args.Path, _imageProcessor)) { - var filename = Path.GetFileNameWithoutExtension(args.Path); + var filename = Path.GetFileNameWithoutExtension(args.Path.AsSpan()); // Make sure the image doesn't belong to a video file - var files = _directoryService.GetFiles(Path.GetDirectoryName(args.Path)); + var files = _directoryService.GetFiles(Path.GetDirectoryName(args.Path) + ?? throw new InvalidOperationException("Path can't be a root directory.")); foreach (var file in files) { @@ -92,32 +91,32 @@ namespace Emby.Server.Implementations.Library.Resolvers return null; } - internal static bool IsOwnedByMedia(NamingOptions namingOptions, string file, string imageFilename) + internal static bool IsOwnedByMedia(NamingOptions namingOptions, string file, ReadOnlySpan<char> imageFilename) { return VideoResolver.IsVideoFile(file, namingOptions) && IsOwnedByResolvedMedia(file, imageFilename); } - internal static bool IsOwnedByResolvedMedia(string file, string imageFilename) + internal static bool IsOwnedByResolvedMedia(ReadOnlySpan<char> file, ReadOnlySpan<char> imageFilename) => imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file), StringComparison.OrdinalIgnoreCase); internal static bool IsImageFile(string path, IImageProcessor imageProcessor) { ArgumentNullException.ThrowIfNull(path); + var extension = Path.GetExtension(path.AsSpan()).TrimStart('.'); + if (!imageProcessor.SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + var filename = Path.GetFileNameWithoutExtension(path); - if (_ignoreFiles.Contains(filename)) + if (_ignoreFiles.Any(i => filename.StartsWith(i, StringComparison.OrdinalIgnoreCase))) { return false; } - if (_ignoreFiles.Any(i => filename.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1)) - { - return false; - } - - string extension = Path.GetExtension(path).TrimStart('.'); - return imageProcessor.SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase); + return true; } } } diff --git a/MediaBrowser.Controller/Resolvers/IItemResolver.cs b/MediaBrowser.Controller/Resolvers/IItemResolver.cs index b95d00aa3c..282aa721e8 100644 --- a/MediaBrowser.Controller/Resolvers/IItemResolver.cs +++ b/MediaBrowser.Controller/Resolvers/IItemResolver.cs @@ -24,7 +24,7 @@ namespace MediaBrowser.Controller.Resolvers /// </summary> /// <param name="args">The args.</param> /// <returns>BaseItem.</returns> - BaseItem ResolvePath(ItemResolveArgs args); + BaseItem? ResolvePath(ItemResolveArgs args); } public interface IMultiItemResolver diff --git a/MediaBrowser.Controller/Resolvers/ItemResolver.cs b/MediaBrowser.Controller/Resolvers/ItemResolver.cs index a6da8384e8..5c9dd6f07c 100644 --- a/MediaBrowser.Controller/Resolvers/ItemResolver.cs +++ b/MediaBrowser.Controller/Resolvers/ItemResolver.cs @@ -1,5 +1,3 @@ -#nullable disable - using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -23,7 +21,7 @@ namespace MediaBrowser.Controller.Resolvers /// </summary> /// <param name="args">The args.</param> /// <returns>`0.</returns> - protected internal virtual T Resolve(ItemResolveArgs args) + protected internal virtual T? Resolve(ItemResolveArgs args) { return null; } @@ -42,7 +40,7 @@ namespace MediaBrowser.Controller.Resolvers /// </summary> /// <param name="args">The args.</param> /// <returns>BaseItem.</returns> - public BaseItem ResolvePath(ItemResolveArgs args) + public BaseItem? ResolvePath(ItemResolveArgs args) { var item = Resolve(args); From cc15ea7f65da7f32e52c909fbf1d6757c20e9e4e Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Wed, 13 Sep 2023 18:12:29 +0200 Subject: [PATCH 611/858] Ignore .zfs folder Maybe helps with #10215 ? --- Emby.Server.Implementations/Library/IgnorePatterns.cs | 4 ++++ .../Library/IgnorePatternsTests.cs | 1 + 2 files changed, 5 insertions(+) diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs index 5384c04b3b..cf6fc18456 100644 --- a/Emby.Server.Implementations/Library/IgnorePatterns.cs +++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs @@ -89,6 +89,10 @@ namespace Emby.Server.Implementations.Library // bts sync files "**/*.bts", "**/*.sync", + + // zfs + "**/.zfs/**", + "**/.zfs" }; private static readonly GlobOptions _globOptions = new GlobOptions diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs index 09eb223288..07061cfc77 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs @@ -31,6 +31,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library [InlineData("/media/music/Foo B.A.R./epic.flac", false)] [InlineData("/media/music/Foo B.A.R", false)] [InlineData("/media/music/Foo B.A.R.", false)] + [InlineData("/movies/.zfs/snapshot/AutoM-2023-09", true)] public void PathIgnored(string path, bool expected) { Assert.Equal(expected, IgnorePatterns.ShouldIgnore(path)); From 626f474faaa1f0511f6241fe95a9bb288e50343b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 27 Sep 2023 13:35:14 -0600 Subject: [PATCH 612/858] Update github/codeql-action action to v2.21.9 (#10290) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 7855ee4f79..5dc38e188d 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@6a28655e3dcb49cb0840ea372fd6d17733edd8a4 # v2.21.8 + uses: github/codeql-action/init@ddccb873888234080b77e9bc2d4764d5ccaaccf9 # v2.21.9 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@6a28655e3dcb49cb0840ea372fd6d17733edd8a4 # v2.21.8 + uses: github/codeql-action/autobuild@ddccb873888234080b77e9bc2d4764d5ccaaccf9 # v2.21.9 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@6a28655e3dcb49cb0840ea372fd6d17733edd8a4 # v2.21.8 + uses: github/codeql-action/analyze@ddccb873888234080b77e9bc2d4764d5ccaaccf9 # v2.21.9 From 3cc10d065ba37d1290c64ca49813ecfc1dab2e8f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 28 Sep 2023 12:30:06 +0000 Subject: [PATCH 613/858] Update dependency Serilog.Sinks.Graylog to v3.1.0 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 6bf960bb71..8cf3eae2a5 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -66,7 +66,7 @@ <PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" /> <PackageVersion Include="Serilog.Sinks.Console" Version="4.1.0" /> <PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" /> - <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.0.2" /> + <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.0" /> <PackageVersion Include="SerilogAnalyzer" Version="0.15.0" /> <PackageVersion Include="SharpFuzz" Version="2.1.1" /> <PackageVersion Include="SkiaSharp" Version="2.88.5" /> From ea56b3edb7739e4c12dd2734da854f724d0fdb1b Mon Sep 17 00:00:00 2001 From: Anand CU <anandcu3@gmail.com> Date: Wed, 27 Sep 2023 17:51:43 +0000 Subject: [PATCH 614/858] Translated using Weblate (Kannada) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/kn/ --- .../Localization/Core/kn.json | 122 +++++++++++++++++- 1 file changed, 121 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/kn.json b/Emby.Server.Implementations/Localization/Core/kn.json index 3c8c38ed4f..5e2b3756bb 100644 --- a/Emby.Server.Implementations/Localization/Core/kn.json +++ b/Emby.Server.Implementations/Localization/Core/kn.json @@ -3,5 +3,125 @@ "TaskOptimizeDatabase": "ಡೇಟಾಬೇಸ್ ಅನ್ನು ಆಪ್ಟಿಮೈಜ್ ಮಾಡಿ", "TaskOptimizeDatabaseDescription": "ಡೇಟಾಬೇಸ್ ಅನ್ನು ಕಾಂಪ್ಯಾಕ್ಟ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಮುಕ್ತ ಜಾಗವನ್ನು ಮೊಟಕುಗೊಳಿಸುತ್ತದೆ. ಲೈಬ್ರರಿಯನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡಿದ ನಂತರ ಈ ಕಾರ್ಯವನ್ನು ನಡೆಸುವುದು ಅಥವಾ ಡೇಟಾಬೇಸ್ ಮಾರ್ಪಾಡುಗಳನ್ನು ಸೂಚಿಸುವ ಇತರ ಬದಲಾವಣೆಗಳನ್ನು ಮಾಡುವುದರಿಂದ ಕಾರ್ಯಕ್ಷಮತೆಯನ್ನು ಸುಧಾರಿಸಬಹುದು.", "TaskKeyframeExtractor": "ಕೀಫ್ರೇಮ್ ಎಕ್ಸ್‌ಟ್ರಾಕ್ಟರ್", - "TaskKeyframeExtractorDescription": "ಹೆಚ್ಚು ನಿಖರವಾದ HLS ಪ್ಲೇಪಟ್ಟಿಗಳನ್ನು ರಚಿಸಲು ವೀಡಿಯೊ ಫೈಲ್‌ಗಳಿಂದ ಕೀಫ್ರೇಮ್‌ಗಳನ್ನು ಹೊರತೆಗೆಯುತ್ತದೆ. ಈ ಕಾರ್ಯವು ದೀರ್ಘಕಾಲದವರೆಗೆ ನಡೆಯಬಹುದು." + "TaskKeyframeExtractorDescription": "ಹೆಚ್ಚು ನಿಖರವಾದ HLS ಪ್ಲೇಪಟ್ಟಿಗಳನ್ನು ರಚಿಸಲು ವೀಡಿಯೊ ಫೈಲ್‌ಗಳಿಂದ ಕೀಫ್ರೇಮ್‌ಗಳನ್ನು ಹೊರತೆಗೆಯುತ್ತದೆ. ಈ ಕಾರ್ಯವು ದೀರ್ಘಕಾಲದವರೆಗೆ ನಡೆಯಬಹುದು.", + "ValueHasBeenAddedToLibrary": "{0} ಅನ್ನು ನಿಮ್ಮ ಮಾಧ್ಯಮ ಲೈಬ್ರರಿಗೆ ಸೇರಿಸಲಾಗಿದೆ", + "ValueSpecialEpisodeName": "ವಿಶೇಷ - {0}", + "TasksLibraryCategory": "ಸಮೊಹ", + "TasksApplicationCategory": "ಅಪ್ಲಿಕೇಶನ್", + "TasksChannelsCategory": "ಇಂಟರ್ನೆಟ್ ಚಾನೆಲ್ಗಳು", + "TaskCleanCache": "ಕ್ಲೀನ್ ಕ್ಯಾಶ ಡೈರೆಕ್ಟರಿ", + "TaskCleanCacheDescription": "ಸಿಸ್ಟಮ್‌ಗೆ ಇನ್ನು ಮುಂದೆ ಅಗತ್ಯವಿಲ್ಲದ ಸಂಗ್ರಹ ಫೈಲ್‌ಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.", + "TaskRefreshLibrary": "ಸ್ಕ್ಯಾನ್ ಮೀಡಿಯಾ ಲೈಬ್ರರಿ", + "UserOfflineFromDevice": "{1} ನಿಂದ {0} ಸಂಪರ್ಕ ಕಡಿತಗೊಂಡಿದೆ", + "Albums": "ಸಂಪುಟ", + "Application": "ಅಪ್ಲಿಕೇಶನ್", + "AppDeviceValues": "ಅಪ್ಲಿಕೇಶನ್: {0}, ಸಾಧನ: {1}", + "Artists": "ಕಲಾವಿದರು", + "AuthenticationSucceededWithUserName": "{0} ಯಶಸ್ವಿಯಾಗಿ ದೃಢೀಕರಿಸಲಾಗಿದೆ", + "Books": "ಪುಸ್ತಕಗಳು", + "ChapterNameValue": "ಅಧ್ಯಾಯ {0}", + "Collections": "ಸಂಗ್ರಹಣೆಗಳು", + "Default": "ಪೂರ್ವನಿಯೋಜಿತ", + "DeviceOfflineWithName": "{0} ಸಂಪರ್ಕ ಕಡಿತಗೊಂಡಿದೆ", + "DeviceOnlineWithName": "{0} ಸಂಪರ್ಕಗೊಂಡಿದೆ", + "External": "ಹೊರಗಿನ", + "FailedLoginAttemptWithUserName": "{0} ರಿಂದ ವಿಫಲ ಲಾಗಿನ್ ಪ್ರಯತ್ನ", + "Favorites": "ಮೆಚ್ಚಿನವುಗಳು", + "Folders": "ಫೋಲ್ಡರ್‌ಗಳು", + "Forced": "ಬಲವಂತವಾಗಿ", + "Genres": "ಪ್ರಕಾರಗಳು", + "HeaderContinueWatching": "ನೋಡುವುದನ್ನು ಮುಂದುವರಿಸಿ", + "HeaderFavoriteAlbums": "ಮೆಚ್ಚಿನ ಸಂಪುಟಗಳು", + "HeaderFavoriteArtists": "ಮೆಚ್ಚಿನ ಕಲಾವಿದರು", + "HeaderFavoriteShows": "ಮೆಚ್ಚಿನ ಪ್ರದರ್ಶನಗಳು", + "HeaderFavoriteSongs": "ಮೆಚ್ಚಿನ ಹಾಡುಗಳು", + "HeaderLiveTV": "ನೇರ ದೂರದರ್ಶನ", + "HeaderNextUp": "ಮುಂದೆ", + "HeaderRecordingGroups": "ರೆಕಾರ್ಡಿಂಗ್ ಗುಂಪುಗಳು", + "MessageApplicationUpdated": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ", + "CameraImageUploadedFrom": "ಹೊಸ ಕ್ಯಾಮರಾ ಚಿತ್ರವನ್ನು {0} ನಿಂದ ಅಪ್‌ಲೋಡ್ ಮಾಡಲಾಗಿದೆ", + "Channels": "ಮೂಲಗಳು", + "HeaderAlbumArtists": "ಸಂಪುಟ ಕಲಾವಿದರು", + "HeaderFavoriteEpisodes": "ಮೆಚ್ಚಿನ ಸಂಚಿಕೆಗಳು", + "HearingImpaired": "ಮೂಗ", + "ItemAddedWithName": "{0} ಅನ್ನು ಸಂಕಲನಕ್ಕೆ ಸೇರಿಸಲಾಗಿದೆ", + "MessageApplicationUpdatedTo": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ ಅನ್ನು {0} ಗೆ ನವೀಕರಿಸಲಾಗಿದೆ", + "MessageNamedServerConfigurationUpdatedWithValue": "ಸರ್ವರ್ ಕಾನ್ಫಿಗರೇಶನ್ ವಿಭಾಗ {0} ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ", + "NewVersionIsAvailable": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್‌ನ ಹೊಸ ಆವೃತ್ತಿಯು ಡೌನ್‌ಲೋಡ್‌ಗೆ ಲಭ್ಯವಿದೆ.", + "NotificationOptionAudioPlayback": "ಆಡಿಯೋ ಪ್ಲೇಬ್ಯಾಕ್ ಪ್ರಾರಂಭವಾಗಿದೆ", + "NotificationOptionCameraImageUploaded": "ಕ್ಯಾಮರಾ ಚಿತ್ರವನ್ನು ಅಪ್ಲೋಡ್ ಮಾಡಲಾಗಿದೆ", + "NotificationOptionPluginUninstalled": "ಪ್ಲಗಿನ್ ಅನ್‌ಇನ್‌ಸ್ಟಾಲ್ ಮಾಡಲಾಗಿದೆ", + "NotificationOptionUserLockedOut": "ಬಳಕೆದಾರರು ಲಾಕ್ ಔಟ್ ಆಗಿದ್ದಾರೆ", + "NotificationOptionVideoPlaybackStopped": "ವೀಡಿಯೊ ಪ್ಲೇಬ್ಯಾಕ್ ನಿಲ್ಲಿಸಲಾಗಿದೆ", + "PluginUninstalledWithName": "{0} ಅನ್ನು ಅನ್‌ಇನ್‌ಸ್ಟಾಲ್ ಮಾಡಲಾಗಿದೆ", + "ScheduledTaskFailedWithName": "{0} ವಿಫಲವಾಗಿದೆ", + "ScheduledTaskStartedWithName": "{0} ಪ್ರಾರಂಭವಾಯಿತು", + "ServerNameNeedsToBeRestarted": "{0} ಅನ್ನು ಮರುಪ್ರಾರಂಭಿಸಬೇಕಾಗಿದೆ", + "UserCreatedWithName": "ಬಳಕೆದಾರ {0} ಅನ್ನು ರಚಿಸಲಾಗಿದೆ", + "UserLockedOutWithName": "ಬಳಕೆದಾರ {0} ಅನ್ನು ಲಾಕ್ ಮಾಡಲಾಗಿದೆ", + "UserOnlineFromDevice": "{1} ನಿಂದ {0} ಆನ್‌ಲೈನ್‌ನಲ್ಲಿದೆ", + "UserPasswordChangedWithName": "{0} ಬಳಕೆದಾರರಿಗಾಗಿ ಪಾಸ್‌ವರ್ಡ್ ಅನ್ನು ಬದಲಾಯಿಸಲಾಗಿದೆ", + "UserPolicyUpdatedWithName": "ಬಳಕೆದಾರರ ನೀತಿಯನ್ನು {0} ಗೆ ನವೀಕರಿಸಲಾಗಿದೆ", + "UserStartedPlayingItemWithValues": "{2} ರಂದು {0} ಆಡುತ್ತಿದೆ {1}", + "UserStoppedPlayingItemWithValues": "{0} ಅವರು {1} ಅನ್ನು {2} ನಲ್ಲಿ ಆಡುವುದನ್ನು ಮುಗಿಸಿದ್ದಾರೆ", + "VersionNumber": "ಆವೃತ್ತಿ {0}", + "TasksMaintenanceCategory": "ನಿರ್ವಹಣೆ", + "TaskCleanActivityLog": "ಕ್ಲೀನ್ ಚಟುವಟಿಕೆ ಲಾಗ್", + "TaskCleanActivityLogDescription": "ಕಾನ್ಫಿಗರ್ ಮಾಡಿದ ವಯಸ್ಸಿಗಿಂತ ಹಳೆಯದಾದ ಚಟುವಟಿಕೆ ಲಾಗ್ ನಮೂದುಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.", + "TaskRefreshChapterImages": "ಅಧ್ಯಾಯ ಚಿತ್ರಗಳನ್ನು ಹೊರತೆಗೆಯಿರಿ", + "TaskRefreshChapterImagesDescription": "ಅಧ್ಯಾಯಗಳನ್ನು ಹೊಂದಿರುವ ವೀಡಿಯೊಗಳಿಗಾಗಿ ಥಂಬ್‌ನೇಲ್‌ಗಳನ್ನು ರಚಿಸುತ್ತದೆ.", + "TaskRefreshLibraryDescription": "ಹೊಸ ಫೈಲ್‌ಗಳಿಗಾಗಿ ನಿಮ್ಮ ಮೀಡಿಯಾ ಲೈಬ್ರರಿಯನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಮೆಟಾಡೇಟಾವನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡುತ್ತದೆ.", + "TaskCleanLogsDescription": "{0} ದಿನಗಳಿಗಿಂತ ಹಳೆಯದಾದ ಲಾಗ್ ಫೈಲ್‌ಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.", + "TaskUpdatePluginsDescription": "ಸ್ವಯಂಚಾಲಿತವಾಗಿ ನವೀಕರಿಸಲು ಕಾನ್ಫಿಗರ್ ಮಾಡಲಾದ ಪ್ಲಗಿನ್‌ಗಳಿಗಾಗಿ ನವೀಕರಣಗಳನ್ನು ಡೌನ್‌ಲೋಡ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಸ್ಥಾಪಿಸುತ್ತದೆ.", + "TaskCleanTranscodeDescription": "ಒಂದು ದಿನಕ್ಕಿಂತ ಹಳೆಯದಾದ ಟ್ರಾನ್ಸ್‌ಕೋಡ್ ಫೈಲ್‌ಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.", + "TaskDownloadMissingSubtitles": "ಕಾಣೆಯಾದ ಉಪಶೀರ್ಷಿಕೆಗಳನ್ನು ಡೌನ್‌ಲೋಡ್ ಮಾಡಿ", + "Shows": "ಧಾರವಾಹಿಗಳು", + "Songs": "ಹಾಡುಗಳು", + "StartupEmbyServerIsLoading": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ ಲೋಡ್ ಆಗುತ್ತಿದೆ. ದಯವಿಟ್ಟು ಸ್ವಲ್ಪ ಸಮಯದ ನಂತರ ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ.", + "UserDeletedWithName": "ಬಳಕೆದಾರ {0} ಅನ್ನು ಅಳಿಸಲಾಗಿದೆ", + "UserDownloadingItemWithValues": "{0} ಡೌನ್‌ಲೋಡ್ ಆಗುತ್ತಿದೆ {1}", + "SubtitleDownloadFailureFromForItem": "ಉಪಶೀರ್ಷಿಕೆಗಳು {0} ನಿಂದ {1} ಗಾಗಿ ಡೌನ್‌ಲೋಡ್ ಮಾಡಲು ವಿಫಲವಾಗಿವೆ", + "Sync": "ಹೊಂದಿಕೆ", + "System": "ವ್ಯವಸ್ಥೆ", + "TvShows": "ದೂರದರ್ಶನ ಕಾರ್ಯಕ್ರಮಗಳು", + "Undefined": "ವ್ಯಾಖ್ಯಾನಿಸಲಾಗಿಲ್ಲ", + "User": "ಬಳಕೆದಾರ", + "HomeVideos": "ಮುಖಪುಟ ವೀಡಿಯೊಗಳು", + "Inherit": "ಪಾರಂಪರ್ಯವಾಗಿ", + "ItemRemovedWithName": "{0} ಅನ್ನು ಸಂಕಲನದಿಂದ ತೆಗೆದುಹಾಕಲಾಗಿದೆ", + "LabelIpAddressValue": "IP ವಿಳಾಸ: {0}", + "LabelRunningTimeValue": "ಅವಧಿ: {0}", + "Latest": "ಹೊಸದಾದ", + "MessageServerConfigurationUpdated": "ಸರ್ವರ್ ಕಾನ್ಫಿಗರೇಶನ್ ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ", + "MixedContent": "ಮಿಶ್ರ ವಿಷಯ", + "Movies": "ಚಲನಚಿತ್ರಗಳು", + "Music": "ಸಂಗೀತ", + "MusicVideos": "ಸಂಗೀತ ವೀಡಿಯೊಗಳು", + "NameInstallFailed": "{0} ಸ್ಥಾಪನೆ ವಿಫಲವಾಗಿದೆ", + "NameSeasonNumber": "ಸೀಸನ್ {0}", + "NameSeasonUnknown": "ಸೀಸನ್ ತಿಳಿದಿಲ್ಲ", + "NotificationOptionApplicationUpdateAvailable": "ಅಪ್ಲಿಕೇಶನ್ ನವೀಕರಣ ಲಭ್ಯವಿದೆ", + "NotificationOptionApplicationUpdateInstalled": "ಅಪ್ಲಿಕೇಶನ್ ನವೀಕರಣವನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿದೆ", + "NotificationOptionAudioPlaybackStopped": "ಆಡಿಯೋ ಪ್ಲೇಬ್ಯಾಕ್ ನಿಲ್ಲಿಸಲಾಗಿದೆ", + "NotificationOptionInstallationFailed": "ಸ್ಥಾಪನ ವೈಫಲ್ಯ", + "NotificationOptionNewLibraryContent": "ಹೊಸ ವಿಷಯವನ್ನು ಒಳಗೊಂಡಿದೆ", + "NotificationOptionPluginError": "ಪ್ಲಗಿನ್ ವೈಫಲ್ಯ", + "NotificationOptionPluginInstalled": "ಪ್ಲಗಿನ್ ವೈಫಲ್ಯ", + "NotificationOptionPluginUpdateInstalled": "ಪ್ಲಗಿನ್ ನವೀಕರಣವನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿದೆ", + "NotificationOptionServerRestartRequired": "ಸರ್ವರ್ ಮರುಪ್ರಾರಂಭದ ಅಗತ್ಯವಿದೆ", + "NotificationOptionTaskFailed": "ನಿಗದಿತ ಕಾರ್ಯ ವೈಫಲ್ಯ", + "NotificationOptionVideoPlayback": "ವೀಡಿಯೊ ಪ್ಲೇಬ್ಯಾಕ್ ಪ್ರಾರಂಭವಾಗಿದೆ", + "Photos": "ಚಿತ್ರಗಳು", + "Playlists": "ಪ್ಲೇಪಟ್ಟಿಗಳು", + "Plugin": "ಪ್ಲಗಿನ್", + "PluginInstalledWithName": "{0} ಅನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿದೆ", + "PluginUpdatedWithName": "{0} ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ", + "ProviderValue": "ಒದಗಿಸುವವರು: {0}", + "TaskCleanLogs": "ಕ್ಲೀನ್ ಲಾಗ್ ಡೈರೆಕ್ಟರಿ", + "TaskRefreshPeople": "ಜನರನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡಿ", + "TaskRefreshPeopleDescription": "ನಿಮ್ಮ ಮಾಧ್ಯಮ ಲೈಬ್ರರಿಯಲ್ಲಿ ನಟರು ಮತ್ತು ನಿರ್ದೇಶಕರಿಗಾಗಿ ಮೆಟಾಡೇಟಾವನ್ನು ನವೀಕರಿಸಿ.", + "TaskUpdatePlugins": "ಪ್ಲಗಿನ್‌ಗಳನ್ನು ನವೀಕರಿಸಿ", + "TaskCleanTranscode": "ಟ್ರಾನ್ಸ್‌ಕೋಡ್ ಡೈರೆಕ್ಟರಿಯನ್ನು ಸ್ವಚ್ಛಗೊಳಿಸಿ", + "TaskRefreshChannels": "ಚಾನಲ್‌ಗಳನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡಿ", + "TaskRefreshChannelsDescription": "ಇಂಟರ್ನೆಟ್ ಚಾನಲ್ ಮಾಹಿತಿಯನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡುತ್ತದೆ." } From d0dc080c93006b3cec0e111aa9c2a79c8d68872a Mon Sep 17 00:00:00 2001 From: Thomas Johansen <thomas@unitmakers.dk> Date: Fri, 29 Sep 2023 14:41:35 +0200 Subject: [PATCH 615/858] I think this is better --- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 346e97ae12..90ef937203 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -419,6 +419,8 @@ namespace MediaBrowser.MediaEncoding.Encoder var extractChapters = request.MediaType == DlnaProfileType.Video && request.ExtractChapters; var analyzeDuration = string.Empty; var ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty; + var ffmpegProbeSize = _config.GetFFmpegProbeSize() ?? string.Empty; + var extraArgs = string.Empty; if (request.MediaSource.AnalyzeDurationMs > 0) { @@ -429,12 +431,22 @@ namespace MediaBrowser.MediaEncoding.Encoder analyzeDuration = "-analyzeduration " + ffmpegAnalyzeDuration; } + if (!string.IsNullOrEmpty(analyzeDuration)) + { + extraArgs = analyzeDuration; + } + + if (!string.IsNullOrEmpty(ffmpegProbeSize)) + { + extraArgs += " -probesize " + ffmpegProbeSize; + } + return GetMediaInfoInternal( GetInputArgument(request.MediaSource.Path, request.MediaSource), request.MediaSource.Path, request.MediaSource.Protocol, extractChapters, - analyzeDuration, + extraArgs, request.MediaType == DlnaProfileType.Audio, request.MediaSource.VideoType, cancellationToken); From 3e10ace985f70fdb92dc655d792cfbc34f2c4bd8 Mon Sep 17 00:00:00 2001 From: Bill Thornton <billt2006@gmail.com> Date: Fri, 29 Sep 2023 10:11:01 -0400 Subject: [PATCH 616/858] Update node versions --- Dockerfile | 2 +- Dockerfile.arm | 2 +- Dockerfile.arm64 | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index e51d285e12..9be319311e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ # https://github.com/multiarch/qemu-user-static#binfmt_misc-register ARG DOTNET_VERSION=7.0 -FROM node:lts-alpine as web-builder +FROM node:20-alpine as web-builder ARG JELLYFIN_WEB_VERSION=master RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ diff --git a/Dockerfile.arm b/Dockerfile.arm index 46a3e9b998..e8ec6398e6 100644 --- a/Dockerfile.arm +++ b/Dockerfile.arm @@ -5,7 +5,7 @@ ARG DOTNET_VERSION=7.0 -FROM node:lts-alpine as web-builder +FROM node:20-alpine as web-builder ARG JELLYFIN_WEB_VERSION=master RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 index 4f9d5e1fdc..83137ee895 100644 --- a/Dockerfile.arm64 +++ b/Dockerfile.arm64 @@ -5,7 +5,7 @@ ARG DOTNET_VERSION=7.0 -FROM node:lts-alpine as web-builder +FROM node:20-alpine as web-builder ARG JELLYFIN_WEB_VERSION=master RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ From 59ec06c35c3b958a1778a56334bdf91a2f0ccf3f Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Fri, 29 Sep 2023 12:43:49 -0400 Subject: [PATCH 617/858] Clear active sessions on application stopping --- Emby.Server.Implementations/Session/SessionManager.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 902d46a906..50d3e8e46d 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1823,6 +1823,8 @@ namespace Emby.Server.Implementations.Session { await session.DisposeAsync().ConfigureAwait(false); } + + _activeConnections.Clear(); } } } From 7f8d9ae7c5821c2ba67812eff176eba19823226b Mon Sep 17 00:00:00 2001 From: Claus Vium <cvium@users.noreply.github.com> Date: Sun, 1 Oct 2023 05:10:42 +0200 Subject: [PATCH 618/858] fix: use TryGetString to avoid crashing, fixes #10306 (#10308) --- Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs index 996f3fe8a6..e1a43bb489 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs @@ -73,8 +73,7 @@ namespace Jellyfin.Server.Migrations.Routines var queryResult = connection.Query("SELECT DISTINCT OfficialRating FROM TypedBaseItems"); foreach (var entry in queryResult) { - var ratingString = entry.GetString(0); - if (string.IsNullOrEmpty(ratingString)) + if (!entry.TryGetString(0, out var ratingString) || string.IsNullOrEmpty(ratingString)) { connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = NULL WHERE OfficialRating IS NULL OR OfficialRating='';"); } From 35d6c1465301689de098748a43fe12a282fcaec3 Mon Sep 17 00:00:00 2001 From: DavidFair <DavidFair@users.noreply.github.com> Date: Sun, 1 Oct 2023 04:11:20 +0100 Subject: [PATCH 619/858] Fix sed failing on Docker builds for CentOS/Fedora (#10285) --- fedora/jellyfin.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fedora/jellyfin.spec b/fedora/jellyfin.spec index 1f7262e660..e783689069 100644 --- a/fedora/jellyfin.spec +++ b/fedora/jellyfin.spec @@ -75,7 +75,7 @@ dotnet publish --configuration Release --self-contained --runtime %{dotnet_runti %{__mkdir} -p %{buildroot}%{_libdir}/jellyfin %{buildroot}%{_bindir} %{__cp} -r Jellyfin.Server/bin/Release/net7.0/%{dotnet_runtime}/publish/* %{buildroot}%{_libdir}/jellyfin %{__install} -D %{SOURCE10} %{buildroot}%{_bindir}/jellyfin -sed -i -e 's/\/usr\/lib64/%{_libdir}/g' %{buildroot}%{_bindir}/jellyfin +sed -i -e 's|/usr/lib64|%{_libdir}|g' %{buildroot}%{_bindir}/jellyfin # Jellyfin config %{__install} -D Jellyfin.Server/Resources/Configuration/logging.json %{buildroot}%{_sysconfdir}/jellyfin/logging.json From 916a40d4ec0abad90d0e26dcbac72d2da34c650f Mon Sep 17 00:00:00 2001 From: MartinWilkerson <drwilko+github@gmail.com> Date: Sun, 1 Oct 2023 09:51:19 +0100 Subject: [PATCH 620/858] Add shebang to jellyfin.init --- debian/jellyfin.init | 1 + 1 file changed, 1 insertion(+) diff --git a/debian/jellyfin.init b/debian/jellyfin.init index 7f5642bac1..784536d874 100644 --- a/debian/jellyfin.init +++ b/debian/jellyfin.init @@ -1,3 +1,4 @@ +#!/bin/sh ### BEGIN INIT INFO # Provides: Jellyfin Media Server # Required-Start: $local_fs $network From b83217d1d74deaafe3dc1e605c5d4ec572278d46 Mon Sep 17 00:00:00 2001 From: YuLong Yao <feilongphone@gmail.com> Date: Mon, 2 Oct 2023 08:55:55 +0800 Subject: [PATCH 621/858] use pcm as ext name when codec is pcm --- Jellyfin.Api/Helpers/StreamingHelpers.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index 782cd65685..e55420d116 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -191,6 +191,11 @@ public static class StreamingHelpers state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, streamingRequest.AudioCodec, state.AudioStream, state.OutputAudioChannels) ?? 0; } + if (outputAudioCodec.StartsWith("pcm_", StringComparison.Ordinal)) + { + containerInternal = ".pcm"; + } + state.OutputAudioCodec = outputAudioCodec; state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.'); state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec); From 808e59fdda3f08cdb2baaac8485f575f0c77ecff Mon Sep 17 00:00:00 2001 From: YuLong Yao <feilongphone@gmail.com> Date: Mon, 2 Oct 2023 09:03:00 +0800 Subject: [PATCH 622/858] add pcm format when codec is pcm_* --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index c311d3b8ab..4a71f25a78 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -6242,6 +6242,12 @@ namespace MediaBrowser.Controller.MediaEncoding audioTranscodeParams.Add("-acodec " + GetAudioEncoder(state)); } + if (GetAudioEncoder(state).StartsWith("pcm_", StringComparison.Ordinal)) + { + audioTranscodeParams.Add(string.Concat("-f ", GetAudioEncoder(state).Substring(4))); + audioTranscodeParams.Add("-ar " + state.BaseRequest.AudioBitRate); + } + if (!string.Equals(outputCodec, "opus", StringComparison.OrdinalIgnoreCase)) { // opus only supports specific sampling rates From 34c4c9f034b316030245d86e23502bca5488f88a Mon Sep 17 00:00:00 2001 From: Hagay Goshen <hagay.goshen@gmail.com> Date: Mon, 2 Oct 2023 13:48:32 +0300 Subject: [PATCH 623/858] allow repeated same tv guide m3u channels , issue 6527 --- .../LiveTv/TunerHosts/M3uParser.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs index df9101f48c..341782d9d3 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs @@ -94,14 +94,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts else if (!string.IsNullOrWhiteSpace(extInf) && !trimmedLine.StartsWith('#')) { var channel = GetChannelnfo(extInf, tunerHostId, trimmedLine); - if (string.IsNullOrWhiteSpace(channel.Id)) - { - channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture); - } - else - { - channel.Id = channelIdPrefix + channel.Id.GetMD5().ToString("N", CultureInfo.InvariantCulture); - } + channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture); channel.Path = trimmedLine; channels.Add(channel); From 6f464a3ac6253a5280d89f1f51d2aee93ad9a016 Mon Sep 17 00:00:00 2001 From: Oliver Bastholm <olbastholm@gmail.com> Date: Sun, 1 Oct 2023 23:57:26 +0000 Subject: [PATCH 624/858] Translated using Weblate (Danish) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/da/ --- .../Localization/Core/da.json | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json index 1b6eecdcfe..837172a5b9 100644 --- a/Emby.Server.Implementations/Localization/Core/da.json +++ b/Emby.Server.Implementations/Localization/Core/da.json @@ -15,13 +15,13 @@ "Favorites": "Favoritter", "Folders": "Mapper", "Genres": "Genrer", - "HeaderAlbumArtists": "Albums kunstnere", + "HeaderAlbumArtists": "Albumkunstnere", "HeaderContinueWatching": "Fortsæt afspilning", - "HeaderFavoriteAlbums": "Favorit albummer", - "HeaderFavoriteArtists": "Favorit kunstnere", - "HeaderFavoriteEpisodes": "Favorit afsnit", - "HeaderFavoriteShows": "Favorit serier", - "HeaderFavoriteSongs": "Favorit sange", + "HeaderFavoriteAlbums": "Favoritalbummer", + "HeaderFavoriteArtists": "Favoritkunstnere", + "HeaderFavoriteEpisodes": "Yndlingsafsnit", + "HeaderFavoriteShows": "Yndlingsserier", + "HeaderFavoriteSongs": "Yndlingssange", "HeaderLiveTV": "Live-TV", "HeaderNextUp": "Næste", "HeaderRecordingGroups": "Optagelsesgrupper", @@ -34,8 +34,8 @@ "Latest": "Seneste", "MessageApplicationUpdated": "Jellyfin Server er blevet opdateret", "MessageApplicationUpdatedTo": "Jellyfin Server er blevet opdateret til {0}", - "MessageNamedServerConfigurationUpdatedWithValue": "Server konfiguration sektion {0} er blevet opdateret", - "MessageServerConfigurationUpdated": "Server konfigurationen er blevet opdateret", + "MessageNamedServerConfigurationUpdatedWithValue": "Serverkonfiguration sektion {0} er blevet opdateret", + "MessageServerConfigurationUpdated": "Serverkonfigurationen er blevet opdateret", "MixedContent": "Blandet indhold", "Movies": "Film", "Music": "Musik", @@ -51,7 +51,7 @@ "NotificationOptionCameraImageUploaded": "Kamerabillede uploadet", "NotificationOptionInstallationFailed": "Installationen mislykkedes", "NotificationOptionNewLibraryContent": "Nyt indhold tilføjet", - "NotificationOptionPluginError": "Plugin fejl", + "NotificationOptionPluginError": "Plugin-fejl", "NotificationOptionPluginInstalled": "Plugin blev installeret", "NotificationOptionPluginUninstalled": "Plugin blev afinstalleret", "NotificationOptionPluginUpdateInstalled": "Opdatering til plugin blev installeret", @@ -92,26 +92,26 @@ "ValueHasBeenAddedToLibrary": "{0} er blevet tilføjet til dit mediebibliotek", "ValueSpecialEpisodeName": "Special - {0}", "VersionNumber": "Version {0}", - "TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata konfigurationen.", + "TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata-konfigurationen.", "TaskDownloadMissingSubtitles": "Hent manglende undertekster", "TaskUpdatePluginsDescription": "Henter og installerer opdateringer for plugins, som er indstillet til at blive opdateret automatisk.", "TaskUpdatePlugins": "Opdater Plugins", - "TaskCleanLogsDescription": "Sletter log filer som er mere end {0} dage gamle.", - "TaskCleanLogs": "Ryd Log mappe", - "TaskRefreshLibraryDescription": "Scanner dit medie bibliotek for nye filer og opdateret metadata.", - "TaskRefreshLibrary": "Scan Medie Bibliotek", - "TaskCleanCacheDescription": "Sletter cache filer som systemet ikke længere bruger.", - "TaskCleanCache": "Ryd Cache mappe", - "TasksChannelsCategory": "Internet Kanaler", + "TaskCleanLogsDescription": "Sletter log-filer som er mere end {0} dage gamle.", + "TaskCleanLogs": "Ryd Log-mappe", + "TaskRefreshLibraryDescription": "Scanner dit mediebibliotek for nye filer og opdateret metadata.", + "TaskRefreshLibrary": "Scan Mediebibliotek", + "TaskCleanCacheDescription": "Sletter cache-filer som systemet ikke længere bruger.", + "TaskCleanCache": "Ryd Cache-mappe", + "TasksChannelsCategory": "Internetkanaler", "TasksApplicationCategory": "Applikation", "TasksLibraryCategory": "Bibliotek", "TasksMaintenanceCategory": "Vedligeholdelse", - "TaskRefreshChapterImages": "Udtræk kapitel billeder", - "TaskRefreshChapterImagesDescription": "Lav miniaturebilleder for videoer der har kapitler.", - "TaskRefreshChannelsDescription": "Opdater internet kanal information.", + "TaskRefreshChapterImages": "Udtræk kapitelbilleder", + "TaskRefreshChapterImagesDescription": "Laver miniaturebilleder for videoer, der har kapitler.", + "TaskRefreshChannelsDescription": "Opdaterer information for internetkanal.", "TaskRefreshChannels": "Opdater Kanaler", - "TaskCleanTranscodeDescription": "Fjern transcode filer som er mere end 1 dag gammel.", - "TaskCleanTranscode": "Tøm Transcode mappen", + "TaskCleanTranscodeDescription": "Fjerner transcode-filer, som er mere end 1 dag gammel.", + "TaskCleanTranscode": "Tøm Transcode-mappen", "TaskRefreshPeople": "Opdater Personer", "TaskRefreshPeopleDescription": "Opdaterer metadata for skuespillere og instruktører i dit mediebibliotek.", "TaskCleanActivityLogDescription": "Sletter linjer i aktivitetsloggen ældre end den konfigurerede alder.", @@ -121,8 +121,8 @@ "Default": "Standard", "TaskOptimizeDatabaseDescription": "Komprimerer databasen og frigør plads. Denne handling køres efter at have scannet mediebiblioteket, eller efter at have lavet ændringer til databasen, for at højne ydeevnen.", "TaskOptimizeDatabase": "Optimér database", - "TaskKeyframeExtractorDescription": "Udtrækker billeder fra videofiler for at lave mere præcise HLS playlister. Denne opgave kan tage lang tid.", - "TaskKeyframeExtractor": "Nøglebillede udtræk", + "TaskKeyframeExtractorDescription": "Udtrækker billeder fra videofiler for at lave mere præcise HLS-playlister. Denne opgave kan tage lang tid.", + "TaskKeyframeExtractor": "Udtræk af nøglebillede", "External": "Ekstern", "HearingImpaired": "Hørehæmmet" } From 5153e9259b9695a2619c1dbd34f11ce21b4068ca Mon Sep 17 00:00:00 2001 From: officialdanielamani <official.danielamani@gmail.com> Date: Sun, 1 Oct 2023 23:19:21 +0000 Subject: [PATCH 625/858] Translated using Weblate (Malay) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ms/ --- Emby.Server.Implementations/Localization/Core/ms.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/ms.json b/Emby.Server.Implementations/Localization/Core/ms.json index 904443bea1..a07222975b 100644 --- a/Emby.Server.Implementations/Localization/Core/ms.json +++ b/Emby.Server.Implementations/Localization/Core/ms.json @@ -1,5 +1,5 @@ { - "Albums": "Albums", + "Albums": "Album", "AppDeviceValues": "Apl: {0}, Peranti: {1}", "Application": "Aplikasi", "Artists": "Artis-artis", From f746db9a54caef36bb3b32cf22812a78d97c865d Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Mon, 2 Oct 2023 15:55:26 -0400 Subject: [PATCH 626/858] Re-add shutdown/restart methods --- .../ApplicationHost.cs | 19 +++++++++++++++ Jellyfin.Api/Controllers/SystemController.cs | 23 +++---------------- MediaBrowser.Common/IApplicationHost.cs | 18 +++++++++++---- 3 files changed, 36 insertions(+), 24 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 86721ace61..e2d2719e5d 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -101,6 +101,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Prometheus.DotNetRuntime; using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; @@ -850,6 +851,24 @@ namespace Emby.Server.Implementations } } + /// <inheritdoc /> + public void Restart() + { + ShouldRestart = true; + Shutdown(); + } + + /// <inheritdoc /> + public void Shutdown() + { + Task.Run(async () => + { + await Task.Delay(100).ConfigureAwait(false); + IsShuttingDown = true; + Resolve<IHostApplicationLifetime>().StopApplication(); + }); + } + /// <summary> /// Gets the composable part assemblies. /// </summary> diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index 4cc0f0ced4..42ac4a9b4b 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -4,7 +4,6 @@ using System.ComponentModel.DataAnnotations; using System.IO; using System.Linq; using System.Net.Mime; -using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using MediaBrowser.Common.Configuration; @@ -18,7 +17,6 @@ using MediaBrowser.Model.System; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace Jellyfin.Api.Controllers; @@ -33,7 +31,6 @@ public class SystemController : BaseJellyfinApiController private readonly IFileSystem _fileSystem; private readonly INetworkManager _network; private readonly ILogger<SystemController> _logger; - private readonly IHostApplicationLifetime _hostApplicationLifetime; /// <summary> /// Initializes a new instance of the <see cref="SystemController"/> class. @@ -43,21 +40,18 @@ public class SystemController : BaseJellyfinApiController /// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param> /// <param name="network">Instance of <see cref="INetworkManager"/> interface.</param> /// <param name="logger">Instance of <see cref="ILogger{SystemController}"/> interface.</param> - /// <param name="hostApplicationLifetime">Instance of <see cref="IHostApplicationLifetime"/> interface.</param> public SystemController( IServerConfigurationManager serverConfigurationManager, IServerApplicationHost appHost, IFileSystem fileSystem, INetworkManager network, - ILogger<SystemController> logger, - IHostApplicationLifetime hostApplicationLifetime) + ILogger<SystemController> logger) { _appPaths = serverConfigurationManager.ApplicationPaths; _appHost = appHost; _fileSystem = fileSystem; _network = network; _logger = logger; - _hostApplicationLifetime = hostApplicationLifetime; } /// <summary> @@ -112,13 +106,7 @@ public class SystemController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status403Forbidden)] public ActionResult RestartApplication() { - Task.Run(async () => - { - await Task.Delay(100).ConfigureAwait(false); - _appHost.ShouldRestart = true; - _appHost.IsShuttingDown = true; - _hostApplicationLifetime.StopApplication(); - }); + _appHost.Restart(); return NoContent(); } @@ -134,12 +122,7 @@ public class SystemController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status403Forbidden)] public ActionResult ShutdownApplication() { - Task.Run(async () => - { - await Task.Delay(100).ConfigureAwait(false); - _appHost.IsShuttingDown = true; - _hostApplicationLifetime.StopApplication(); - }); + _appHost.Shutdown(); return NoContent(); } diff --git a/MediaBrowser.Common/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs index c1ea6e87cc..5985d3dd82 100644 --- a/MediaBrowser.Common/IApplicationHost.cs +++ b/MediaBrowser.Common/IApplicationHost.cs @@ -41,15 +41,15 @@ namespace MediaBrowser.Common bool HasPendingRestart { get; } /// <summary> - /// Gets or sets a value indicating whether this instance is currently shutting down. + /// Gets a value indicating whether this instance is currently shutting down. /// </summary> /// <value><c>true</c> if this instance is shutting down; otherwise, <c>false</c>.</value> - bool IsShuttingDown { get; set; } + bool IsShuttingDown { get; } /// <summary> - /// Gets or sets a value indicating whether the application should restart. + /// Gets a value indicating whether the application should restart. /// </summary> - bool ShouldRestart { get; set; } + bool ShouldRestart { get; } /// <summary> /// Gets the application version. @@ -91,6 +91,11 @@ namespace MediaBrowser.Common /// </summary> void NotifyPendingRestart(); + /// <summary> + /// Restarts this instance. + /// </summary> + void Restart(); + /// <summary> /// Gets the exports. /// </summary> @@ -122,6 +127,11 @@ namespace MediaBrowser.Common /// <returns>``0.</returns> T Resolve<T>(); + /// <summary> + /// Shuts down. + /// </summary> + void Shutdown(); + /// <summary> /// Initializes this instance. /// </summary> From ab0790271aac8846430c9689dff647eb4ce55078 Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Mon, 2 Oct 2023 16:54:19 -0400 Subject: [PATCH 627/858] Apply suggestions from code review Co-authored-by: Claus Vium <cvium@users.noreply.github.com> --- Emby.Server.Implementations/ApplicationHost.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index e2d2719e5d..a8b8e5fb94 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -203,10 +203,10 @@ namespace Emby.Server.Implementations public bool HasPendingRestart { get; private set; } /// <inheritdoc /> - public bool IsShuttingDown { get; set; } + public bool IsShuttingDown { get; private set; } /// <inheritdoc /> - public bool ShouldRestart { get; set; } + public bool ShouldRestart { get; private set; } /// <summary> /// Gets the logger. From b95040bc5ed7b53ad3e2a92363d50dc3f6ee82c5 Mon Sep 17 00:00:00 2001 From: Joe Rogers <1337joe@gmail.com> Date: Mon, 2 Oct 2023 23:00:51 -0400 Subject: [PATCH 628/858] Add We;Na to split whitelist --- MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 3055e6cde3..441a3abd46 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -78,6 +78,7 @@ namespace MediaBrowser.MediaEncoding.Probing "She/Her/Hers", "5/8erl in Ehr'n", "Smith/Kotzen", + "We;Na", }; /// <summary> From 2c31ed80314350a1445f9b55426c6744475beff2 Mon Sep 17 00:00:00 2001 From: Nyanmisaka <nst799610810@gmail.com> Date: Tue, 3 Oct 2023 15:33:59 +0800 Subject: [PATCH 629/858] Fix JELLYFIN_FFMPEG_OPT is not enabled in fedora ExecStart --- fedora/jellyfin.service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fedora/jellyfin.service b/fedora/jellyfin.service index 1b3f8032c6..01accdc0c9 100644 --- a/fedora/jellyfin.service +++ b/fedora/jellyfin.service @@ -8,7 +8,7 @@ EnvironmentFile = /etc/sysconfig/jellyfin User = jellyfin Group = jellyfin WorkingDirectory = /var/lib/jellyfin -ExecStart = /usr/bin/jellyfin $JELLYFIN_WEB_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT $JELLYFIN_ADDITIONAL_OPTS +ExecStart = /usr/bin/jellyfin $JELLYFIN_WEB_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT $JELLYFIN_ADDITIONAL_OPTS Restart = on-failure TimeoutSec = 15 SuccessExitStatus=0 143 From 1ca9f8b04ba93f2c9d33f278beb4e591d84a2f6a Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Tue, 3 Oct 2023 09:26:20 -0400 Subject: [PATCH 630/858] Remove unused fields and parameters --- Jellyfin.Api/Controllers/LiveTvController.cs | 7 +------ Jellyfin.Server.Implementations/Users/UserManager.cs | 10 ++-------- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 267ba4afb4..649397d68d 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -23,7 +23,6 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.LiveTv; @@ -48,7 +47,6 @@ public class LiveTvController : BaseJellyfinApiController private readonly IMediaSourceManager _mediaSourceManager; private readonly IConfigurationManager _configurationManager; private readonly TranscodingJobHelper _transcodingJobHelper; - private readonly ISessionManager _sessionManager; /// <summary> /// Initializes a new instance of the <see cref="LiveTvController"/> class. @@ -61,7 +59,6 @@ public class LiveTvController : BaseJellyfinApiController /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param> /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param> /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param> - /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param> public LiveTvController( ILiveTvManager liveTvManager, IUserManager userManager, @@ -70,8 +67,7 @@ public class LiveTvController : BaseJellyfinApiController IDtoService dtoService, IMediaSourceManager mediaSourceManager, IConfigurationManager configurationManager, - TranscodingJobHelper transcodingJobHelper, - ISessionManager sessionManager) + TranscodingJobHelper transcodingJobHelper) { _liveTvManager = liveTvManager; _userManager = userManager; @@ -81,7 +77,6 @@ public class LiveTvController : BaseJellyfinApiController _mediaSourceManager = mediaSourceManager; _configurationManager = configurationManager; _transcodingJobHelper = transcodingJobHelper; - _sessionManager = sessionManager; } /// <summary> diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 5010751ddb..94ac4798ca 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -20,7 +20,6 @@ using MediaBrowser.Controller.Events; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Users; using Microsoft.EntityFrameworkCore; @@ -35,7 +34,6 @@ namespace Jellyfin.Server.Implementations.Users { private readonly IDbContextFactory<JellyfinDbContext> _dbProvider; private readonly IEventManager _eventManager; - private readonly ICryptoProvider _cryptoProvider; private readonly INetworkManager _networkManager; private readonly IApplicationHost _appHost; private readonly IImageProcessor _imageProcessor; @@ -53,7 +51,6 @@ namespace Jellyfin.Server.Implementations.Users /// </summary> /// <param name="dbProvider">The database provider.</param> /// <param name="eventManager">The event manager.</param> - /// <param name="cryptoProvider">The cryptography provider.</param> /// <param name="networkManager">The network manager.</param> /// <param name="appHost">The application host.</param> /// <param name="imageProcessor">The image processor.</param> @@ -61,7 +58,6 @@ namespace Jellyfin.Server.Implementations.Users public UserManager( IDbContextFactory<JellyfinDbContext> dbProvider, IEventManager eventManager, - ICryptoProvider cryptoProvider, INetworkManager networkManager, IApplicationHost appHost, IImageProcessor imageProcessor, @@ -69,7 +65,6 @@ namespace Jellyfin.Server.Implementations.Users { _dbProvider = dbProvider; _eventManager = eventManager; - _cryptoProvider = cryptoProvider; _networkManager = networkManager; _appHost = appHost; _imageProcessor = imageProcessor; @@ -384,7 +379,7 @@ namespace Jellyfin.Server.Implementations.Users } var user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase)); - var authResult = await AuthenticateLocalUser(username, password, user, remoteEndPoint) + var authResult = await AuthenticateLocalUser(username, password, user) .ConfigureAwait(false); var authenticationProvider = authResult.AuthenticationProvider; var success = authResult.Success; @@ -787,8 +782,7 @@ namespace Jellyfin.Server.Implementations.Users private async Task<(IAuthenticationProvider? AuthenticationProvider, string Username, bool Success)> AuthenticateLocalUser( string username, string password, - User? user, - string remoteEndPoint) + User? user) { bool success = false; IAuthenticationProvider? authenticationProvider = null; From fa26bcde3a7b6c288ae5a528a45f08e64a6a2011 Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Tue, 3 Oct 2023 09:29:06 -0400 Subject: [PATCH 631/858] Remove unnecessary ToString in RobotsRedirectionMiddleware --- Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs b/Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs index 8bf626035d..acf3645fdc 100644 --- a/Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs +++ b/Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs @@ -33,8 +33,7 @@ public class RobotsRedirectionMiddleware /// <returns>The async task.</returns> public async Task Invoke(HttpContext httpContext) { - var localPath = httpContext.Request.Path.ToString(); - if (string.Equals(localPath, "/robots.txt", StringComparison.OrdinalIgnoreCase)) + if (httpContext.Request.Path.Equals("/robots.txt", StringComparison.OrdinalIgnoreCase)) { _logger.LogDebug("Redirecting robots.txt request to web/robots.txt"); httpContext.Response.Redirect("web/robots.txt"); From 78e00578c20d0127b540bc69290972104f12ac84 Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Tue, 3 Oct 2023 10:25:14 -0400 Subject: [PATCH 632/858] Use DI for IFileSystem --- .../AppBase/BaseConfigurationManager.cs | 20 +++++++------------ .../ApplicationHost.cs | 11 +++++----- .../ServerConfigurationManager.cs | 11 +++++----- .../IO/ManagedFileSystem.cs | 19 ++++++++---------- MediaBrowser.Model/IO/IFileSystem.cs | 2 -- 5 files changed, 26 insertions(+), 37 deletions(-) diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs index a4deeddb78..a2f38c8c2d 100644 --- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs +++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs @@ -8,7 +8,6 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Events; using MediaBrowser.Common.Extensions; using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; @@ -19,14 +18,8 @@ namespace Emby.Server.Implementations.AppBase /// </summary> public abstract class BaseConfigurationManager : IConfigurationManager { - private readonly IFileSystem _fileSystem; - - private readonly ConcurrentDictionary<string, object> _configurations = new ConcurrentDictionary<string, object>(); - - /// <summary> - /// The _configuration sync lock. - /// </summary> - private readonly object _configurationSyncLock = new object(); + private readonly ConcurrentDictionary<string, object> _configurations = new(); + private readonly object _configurationSyncLock = new(); private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>(); private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>(); @@ -42,12 +35,13 @@ namespace Emby.Server.Implementations.AppBase /// <param name="applicationPaths">The application paths.</param> /// <param name="loggerFactory">The logger factory.</param> /// <param name="xmlSerializer">The XML serializer.</param> - /// <param name="fileSystem">The file system.</param> - protected BaseConfigurationManager(IApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IXmlSerializer xmlSerializer, IFileSystem fileSystem) + protected BaseConfigurationManager( + IApplicationPaths applicationPaths, + ILoggerFactory loggerFactory, + IXmlSerializer xmlSerializer) { CommonApplicationPaths = applicationPaths; XmlSerializer = xmlSerializer; - _fileSystem = fileSystem; Logger = loggerFactory.CreateLogger<BaseConfigurationManager>(); UpdateCachePath(); @@ -272,7 +266,7 @@ namespace Emby.Server.Implementations.AppBase { var file = Path.Combine(path, Guid.NewGuid().ToString()); File.WriteAllText(file, string.Empty); - _fileSystem.DeleteFile(file); + File.Delete(file); } private string GetConfigurationFile(string key) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 985ab0db5e..8518b13521 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -120,7 +120,6 @@ namespace Emby.Server.Implementations private readonly ConcurrentDictionary<IDisposable, byte> _disposableParts = new(); private readonly DeviceId _deviceId; - private readonly IFileSystem _fileSystemManager; private readonly IConfiguration _startupConfig; private readonly IXmlSerializer _xmlSerializer; private readonly IStartupOptions _startupOptions; @@ -153,10 +152,8 @@ namespace Emby.Server.Implementations LoggerFactory = loggerFactory; _startupOptions = options; _startupConfig = startupConfig; - _fileSystemManager = new ManagedFileSystem(LoggerFactory.CreateLogger<ManagedFileSystem>(), applicationPaths); Logger = LoggerFactory.CreateLogger<ApplicationHost>(); - _fileSystemManager.AddShortcutHandler(new MbLinkShortcutHandler()); _deviceId = new DeviceId(ApplicationPaths, LoggerFactory); ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version; @@ -164,7 +161,7 @@ namespace Emby.Server.Implementations ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString; _xmlSerializer = new MyXmlSerializer(); - ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager); + ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer); _pluginManager = new PluginManager( LoggerFactory.CreateLogger<PluginManager>(), this, @@ -507,7 +504,9 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(_pluginManager); serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths); - serviceCollection.AddSingleton(_fileSystemManager); + serviceCollection.AddSingleton<IFileSystem, ManagedFileSystem>(); + serviceCollection.AddSingleton<IShortcutHandler, MbLinkShortcutHandler>(); + serviceCollection.AddSingleton<TmdbClientManager>(); serviceCollection.AddSingleton(NetManager); @@ -681,7 +680,7 @@ namespace Emby.Server.Implementations BaseItem.ProviderManager = Resolve<IProviderManager>(); BaseItem.LocalizationManager = Resolve<ILocalizationManager>(); BaseItem.ItemRepository = Resolve<IItemRepository>(); - BaseItem.FileSystem = _fileSystemManager; + BaseItem.FileSystem = Resolve<IFileSystem>(); BaseItem.UserDataManager = Resolve<IUserDataManager>(); BaseItem.ChannelManager = Resolve<IChannelManager>(); Video.LiveTvManager = Resolve<ILiveTvManager>(); diff --git a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs index 6b8b1a620f..0ee43ce0a3 100644 --- a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs +++ b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs @@ -7,7 +7,6 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; @@ -22,11 +21,13 @@ namespace Emby.Server.Implementations.Configuration /// Initializes a new instance of the <see cref="ServerConfigurationManager" /> class. /// </summary> /// <param name="applicationPaths">The application paths.</param> - /// <param name="loggerFactory">The paramref name="loggerFactory" factory.</param> + /// <param name="loggerFactory">The logger factory.</param> /// <param name="xmlSerializer">The XML serializer.</param> - /// <param name="fileSystem">The file system.</param> - public ServerConfigurationManager(IApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IXmlSerializer xmlSerializer, IFileSystem fileSystem) - : base(applicationPaths, loggerFactory, xmlSerializer, fileSystem) + public ServerConfigurationManager( + IApplicationPaths applicationPaths, + ILoggerFactory loggerFactory, + IXmlSerializer xmlSerializer) + : base(applicationPaths, loggerFactory, xmlSerializer) { UpdateMetadataPath(); } diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 3aa5233ed1..18b00ce0b2 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -15,10 +15,6 @@ namespace Emby.Server.Implementations.IO /// </summary> public class ManagedFileSystem : IFileSystem { - private readonly ILogger<ManagedFileSystem> _logger; - - private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>(); - private readonly string _tempPath; private static readonly bool _isEnvironmentCaseInsensitive = OperatingSystem.IsWindows(); private static readonly char[] _invalidPathCharacters = { @@ -29,23 +25,24 @@ namespace Emby.Server.Implementations.IO (char)31, ':', '*', '?', '\\', '/' }; + private readonly ILogger<ManagedFileSystem> _logger; + private readonly List<IShortcutHandler> _shortcutHandlers; + private readonly string _tempPath; + /// <summary> /// Initializes a new instance of the <see cref="ManagedFileSystem"/> class. /// </summary> /// <param name="logger">The <see cref="ILogger"/> instance to use.</param> /// <param name="applicationPaths">The <see cref="IApplicationPaths"/> instance to use.</param> + /// <param name="shortcutHandlers">the <see cref="IShortcutHandler"/>'s to use.</param> public ManagedFileSystem( ILogger<ManagedFileSystem> logger, - IApplicationPaths applicationPaths) + IApplicationPaths applicationPaths, + IEnumerable<IShortcutHandler> shortcutHandlers) { _logger = logger; _tempPath = applicationPaths.TempDirectory; - } - - /// <inheritdoc /> - public virtual void AddShortcutHandler(IShortcutHandler handler) - { - _shortcutHandlers.Add(handler); + _shortcutHandlers = shortcutHandlers.ToList(); } /// <summary> diff --git a/MediaBrowser.Model/IO/IFileSystem.cs b/MediaBrowser.Model/IO/IFileSystem.cs index 5bdda2b240..ec381d4231 100644 --- a/MediaBrowser.Model/IO/IFileSystem.cs +++ b/MediaBrowser.Model/IO/IFileSystem.cs @@ -10,8 +10,6 @@ namespace MediaBrowser.Model.IO /// </summary> public interface IFileSystem { - void AddShortcutHandler(IShortcutHandler handler); - /// <summary> /// Determines whether the specified filename is shortcut. /// </summary> From 12b51cf2ba537a6f56882277dc856ab39ace94a0 Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Tue, 3 Oct 2023 10:31:55 -0400 Subject: [PATCH 633/858] Reduce nesting in SessionManager.OnPlaybackStopped --- .../Session/SessionManager.cs | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 50d3e8e46d..e935f7e5e5 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -951,28 +951,28 @@ namespace Emby.Server.Implementations.Session private bool OnPlaybackStopped(User user, BaseItem item, long? positionTicks, bool playbackFailed) { - bool playedToCompletion = false; - - if (!playbackFailed) + if (playbackFailed) { - var data = _userDataManager.GetUserData(user, item); - - if (positionTicks.HasValue) - { - playedToCompletion = _userDataManager.UpdatePlayState(item, data, positionTicks.Value); - } - else - { - // If the client isn't able to report this, then we'll just have to make an assumption - data.PlayCount++; - data.Played = item.SupportsPlayedStatus; - data.PlaybackPositionTicks = 0; - playedToCompletion = true; - } - - _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackFinished, CancellationToken.None); + return false; } + var data = _userDataManager.GetUserData(user, item); + bool playedToCompletion; + if (positionTicks.HasValue) + { + playedToCompletion = _userDataManager.UpdatePlayState(item, data, positionTicks.Value); + } + else + { + // If the client isn't able to report this, then we'll just have to make an assumption + data.PlayCount++; + data.Played = item.SupportsPlayedStatus; + data.PlaybackPositionTicks = 0; + playedToCompletion = true; + } + + _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackFinished, CancellationToken.None); + return playedToCompletion; } From 487d79f8acd5449c8c01d68b0df2caeb979213cc Mon Sep 17 00:00:00 2001 From: Pithaya <19533412+Pithaya@users.noreply.github.com> Date: Wed, 4 Oct 2023 05:16:16 +0200 Subject: [PATCH 634/858] Add book related values to PersonKind (#10325) --- Jellyfin.Data/Enums/PersonKind.cs | 36 +++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/Jellyfin.Data/Enums/PersonKind.cs b/Jellyfin.Data/Enums/PersonKind.cs index 10a8056669..29308789a0 100644 --- a/Jellyfin.Data/Enums/PersonKind.cs +++ b/Jellyfin.Data/Enums/PersonKind.cs @@ -94,4 +94,40 @@ public enum PersonKind /// A person who was the illustrator. /// </summary> Illustrator, + + /// <summary> + /// A person responsible for drawing the art. + /// </summary> + Penciller, + + /// <summary> + /// A person responsible for inking the pencil art. + /// </summary> + Inker, + + /// <summary> + /// A person responsible for applying color to drawings. + /// </summary> + Colorist, + + /// <summary> + /// A person responsible for drawing text and speech bubbles. + /// </summary> + Letterer, + + /// <summary> + /// A person responsible for drawing the cover art. + /// </summary> + CoverArtist, + + /// <summary> + /// A person contributing to a resource by revising or elucidating the content, e.g., adding an introduction, notes, or other critical matter. + /// An editor may also prepare a resource for production, publication, or distribution. + /// </summary> + Editor, + + /// <summary> + /// A person who renders a text from one language into another. + /// </summary> + Translator } From 6f2c165cc3d0000bda5722200971dd619833c349 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Wed, 4 Oct 2023 16:06:26 +0200 Subject: [PATCH 635/858] Use Authorization header in integration tests instead of X-Emby-Authorization And ensure the response has a successful status code --- .../Jellyfin.Server.Integration.Tests/AuthHelper.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs b/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs index 3dc62afaf8..5ddbd30d1e 100644 --- a/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs +++ b/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs @@ -15,8 +15,8 @@ namespace Jellyfin.Server.Integration.Tests { public static class AuthHelper { - public const string AuthHeaderName = "X-Emby-Authorization"; - public const string DummyAuthHeader = "MediaBrowser Client=\"Jellyfin.Server Integration Tests\", DeviceId=\"69420\", Device=\"Apple II\", Version=\"10.8.0\""; + public const string AuthHeaderName = "Authorization"; + public const string DummyAuthHeader = "MediaBrowser Client=\"Jellyfin.Server%20Integration%20Tests\", DeviceId=\"69420\", Device=\"Apple%20II\", Version=\"10.8.0\""; public static async Task<string> CompleteStartupAsync(HttpClient client) { @@ -27,16 +27,19 @@ namespace Jellyfin.Server.Integration.Tests using var completeResponse = await client.PostAsync("/Startup/Complete", new ByteArrayContent(Array.Empty<byte>())); Assert.Equal(HttpStatusCode.NoContent, completeResponse.StatusCode); - using var content = JsonContent.Create( + using var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/Users/AuthenticateByName"); + httpRequest.Headers.TryAddWithoutValidation(AuthHeaderName, DummyAuthHeader); + httpRequest.Content = JsonContent.Create( new AuthenticateUserByName() { Username = user!.Name, Pw = user.Password, }, options: jsonOptions); - content.Headers.Add("X-Emby-Authorization", DummyAuthHeader); - using var authResponse = await client.PostAsync("/Users/AuthenticateByName", content); + using var authResponse = await client.SendAsync(httpRequest); + authResponse.EnsureSuccessStatusCode(); + var auth = await JsonSerializer.DeserializeAsync<AuthenticationResultDto>( await authResponse.Content.ReadAsStreamAsync(), jsonOptions); From 76c64516a78ca583c13739cedd63dd2b2cc5e05e Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Wed, 4 Oct 2023 16:18:14 +0200 Subject: [PATCH 636/858] Simplify some stuff in AuthorizationContext --- .../Security/AuthorizationContext.cs | 30 ++++--------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs index 700e639700..f415d01115 100644 --- a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs +++ b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs @@ -49,14 +49,13 @@ namespace Jellyfin.Server.Implementations.Security /// <summary> /// Gets the authorization. /// </summary> - /// <param name="httpReq">The HTTP req.</param> + /// <param name="httpContext">The HTTP req.</param> /// <returns>Dictionary{System.StringSystem.String}.</returns> - private async Task<AuthorizationInfo> GetAuthorization(HttpContext httpReq) + private async Task<AuthorizationInfo> GetAuthorization(HttpContext httpContext) { - var auth = GetAuthorizationDictionary(httpReq); - var authInfo = await GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query).ConfigureAwait(false); + var authInfo = await GetAuthorizationInfo(httpContext.Request).ConfigureAwait(false); - httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo; + httpContext.Request.HttpContext.Items["AuthorizationInfo"] = authInfo; return authInfo; } @@ -80,7 +79,6 @@ namespace Jellyfin.Server.Implementations.Security auth.TryGetValue("Token", out token); } -#pragma warning disable CA1508 // string.IsNullOrEmpty(token) is always false. if (string.IsNullOrEmpty(token)) { token = headers["X-Emby-Token"]; @@ -118,7 +116,6 @@ namespace Jellyfin.Server.Implementations.Security // Request doesn't contain a token. return authInfo; } -#pragma warning restore CA1508 authInfo.HasToken = true; var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().ConfigureAwait(false); @@ -219,24 +216,7 @@ namespace Jellyfin.Server.Implementations.Security /// <summary> /// Gets the auth. /// </summary> - /// <param name="httpReq">The HTTP req.</param> - /// <returns>Dictionary{System.StringSystem.String}.</returns> - private static Dictionary<string, string>? GetAuthorizationDictionary(HttpContext httpReq) - { - var auth = httpReq.Request.Headers["X-Emby-Authorization"]; - - if (string.IsNullOrEmpty(auth)) - { - auth = httpReq.Request.Headers[HeaderNames.Authorization]; - } - - return auth.Count > 0 ? GetAuthorization(auth[0]) : null; - } - - /// <summary> - /// Gets the auth. - /// </summary> - /// <param name="httpReq">The HTTP req.</param> + /// <param name="httpReq">The HTTP request.</param> /// <returns>Dictionary{System.StringSystem.String}.</returns> private static Dictionary<string, string>? GetAuthorizationDictionary(HttpRequest httpReq) { From 35e09cf203550ca101b85be92e7aa4014902ffd3 Mon Sep 17 00:00:00 2001 From: Piranha <danielmperagallo@gmail.com> Date: Tue, 3 Oct 2023 14:30:03 +0000 Subject: [PATCH 637/858] Translated using Weblate (Spanish) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/es/ --- Emby.Server.Implementations/Localization/Core/es.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json index f5636a0af0..4c56f789d3 100644 --- a/Emby.Server.Implementations/Localization/Core/es.json +++ b/Emby.Server.Implementations/Localization/Core/es.json @@ -3,9 +3,9 @@ "AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}", "Application": "Aplicación", "Artists": "Artistas", - "AuthenticationSucceededWithUserName": "{0} identificado correctamente", + "AuthenticationSucceededWithUserName": "{0} autenticado correctamente", "Books": "Libros", - "CameraImageUploadedFrom": "Se ha subido una nueva imagen de cámara desde {0}", + "CameraImageUploadedFrom": "Se ha subido una nueva imagen por cámara desde {0}", "Channels": "Canales", "ChapterNameValue": "Capítulo {0}", "Collections": "Colecciones", From c9ce246c173339c00b4131f3e8b2c9240584d6ae Mon Sep 17 00:00:00 2001 From: Artnal <shopping@marquet.ch> Date: Tue, 3 Oct 2023 11:29:38 +0000 Subject: [PATCH 638/858] Translated using Weblate (French) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fr/ --- Emby.Server.Implementations/Localization/Core/fr.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json index 4877bcd7a7..a2b429dcdd 100644 --- a/Emby.Server.Implementations/Localization/Core/fr.json +++ b/Emby.Server.Implementations/Localization/Core/fr.json @@ -105,8 +105,8 @@ "TaskRefreshPeople": "Actualiser les acteurs", "TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.", "TaskCleanLogs": "Nettoyer le répertoire des journaux", - "TaskRefreshLibraryDescription": "Scanne votre médiathèque pour trouver les nouveaux fichiers et actualise les métadonnées.", - "TaskRefreshLibrary": "Scanner la médiathèque", + "TaskRefreshLibraryDescription": "Analyser sa médiathèque pour trouver les nouveaux fichiers et actualiser les métadonnées.", + "TaskRefreshLibrary": "Analyser la médiathèque", "TaskRefreshChapterImagesDescription": "Crée des vignettes pour les vidéos ayant des chapitres.", "TaskRefreshChapterImages": "Extraire les images de chapitre", "TaskCleanCacheDescription": "Supprime les fichiers de cache dont le système n'a plus besoin.", From 6f7413812fb4654d1b1ca065a177a1bb19408386 Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Wed, 4 Oct 2023 14:34:53 -0400 Subject: [PATCH 639/858] Add SystemManager service --- .../ApplicationHost.cs | 80 +------------ Emby.Server.Implementations/SystemManager.cs | 108 ++++++++++++++++++ Jellyfin.Api/Controllers/SystemController.cs | 49 ++++---- MediaBrowser.Common/IApplicationHost.cs | 24 +--- .../IServerApplicationHost.cs | 12 -- MediaBrowser.Controller/ISystemManager.cs | 34 ++++++ 6 files changed, 174 insertions(+), 133 deletions(-) create mode 100644 Emby.Server.Implementations/SystemManager.cs create mode 100644 MediaBrowser.Controller/ISystemManager.cs diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 8518b13521..3253ea0267 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -101,7 +101,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Prometheus.DotNetRuntime; using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; @@ -133,7 +132,7 @@ namespace Emby.Server.Implementations /// <value>All concrete types.</value> private Type[] _allConcreteTypes; - private bool _disposed = false; + private bool _disposed; /// <summary> /// Initializes a new instance of the <see cref="ApplicationHost"/> class. @@ -184,26 +183,16 @@ namespace Emby.Server.Implementations public bool CoreStartupHasCompleted { get; private set; } - public virtual bool CanLaunchWebBrowser => Environment.UserInteractive - && !_startupOptions.IsService - && (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS()); - /// <summary> /// Gets the <see cref="INetworkManager"/> singleton instance. /// </summary> public INetworkManager NetManager { get; private set; } - /// <summary> - /// Gets a value indicating whether this instance has changes that require the entire application to restart. - /// </summary> - /// <value><c>true</c> if this instance has pending application restart; otherwise, <c>false</c>.</value> + /// <inheritdoc /> public bool HasPendingRestart { get; private set; } /// <inheritdoc /> - public bool IsShuttingDown { get; private set; } - - /// <inheritdoc /> - public bool ShouldRestart { get; private set; } + public bool ShouldRestart { get; set; } /// <summary> /// Gets the logger. @@ -507,6 +496,8 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton<IFileSystem, ManagedFileSystem>(); serviceCollection.AddSingleton<IShortcutHandler, MbLinkShortcutHandler>(); + serviceCollection.AddScoped<ISystemManager, SystemManager>(); + serviceCollection.AddSingleton<TmdbClientManager>(); serviceCollection.AddSingleton(NetManager); @@ -850,24 +841,6 @@ namespace Emby.Server.Implementations } } - /// <inheritdoc /> - public void Restart() - { - ShouldRestart = true; - Shutdown(); - } - - /// <inheritdoc /> - public void Shutdown() - { - Task.Run(async () => - { - await Task.Delay(100).ConfigureAwait(false); - IsShuttingDown = true; - Resolve<IHostApplicationLifetime>().StopApplication(); - }); - } - /// <summary> /// Gets the composable part assemblies. /// </summary> @@ -923,49 +896,6 @@ namespace Emby.Server.Implementations protected abstract IEnumerable<Assembly> GetAssembliesWithPartsInternal(); - /// <summary> - /// Gets the system status. - /// </summary> - /// <param name="request">Where this request originated.</param> - /// <returns>SystemInfo.</returns> - public SystemInfo GetSystemInfo(HttpRequest request) - { - return new SystemInfo - { - HasPendingRestart = HasPendingRestart, - IsShuttingDown = IsShuttingDown, - Version = ApplicationVersionString, - WebSocketPortNumber = HttpPort, - CompletedInstallations = Resolve<IInstallationManager>().CompletedInstallations.ToArray(), - Id = SystemId, - ProgramDataPath = ApplicationPaths.ProgramDataPath, - WebPath = ApplicationPaths.WebPath, - LogPath = ApplicationPaths.LogDirectoryPath, - ItemsByNamePath = ApplicationPaths.InternalMetadataPath, - InternalMetadataPath = ApplicationPaths.InternalMetadataPath, - CachePath = ApplicationPaths.CachePath, - CanLaunchWebBrowser = CanLaunchWebBrowser, - TranscodingTempPath = ConfigurationManager.GetTranscodePath(), - ServerName = FriendlyName, - LocalAddress = GetSmartApiUrl(request), - SupportsLibraryMonitor = true, - PackageName = _startupOptions.PackageName - }; - } - - public PublicSystemInfo GetPublicSystemInfo(HttpRequest request) - { - return new PublicSystemInfo - { - Version = ApplicationVersionString, - ProductName = ApplicationProductName, - Id = SystemId, - ServerName = FriendlyName, - LocalAddress = GetSmartApiUrl(request), - StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted - }; - } - /// <inheritdoc/> public string GetSmartApiUrl(IPAddress remoteAddr) { diff --git a/Emby.Server.Implementations/SystemManager.cs b/Emby.Server.Implementations/SystemManager.cs new file mode 100644 index 0000000000..5e9c424e9a --- /dev/null +++ b/Emby.Server.Implementations/SystemManager.cs @@ -0,0 +1,108 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Updates; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Model.System; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Hosting; + +namespace Emby.Server.Implementations; + +/// <inheritdoc /> +public class SystemManager : ISystemManager +{ + private readonly IHostApplicationLifetime _applicationLifetime; + private readonly IServerApplicationHost _applicationHost; + private readonly IServerApplicationPaths _applicationPaths; + private readonly IServerConfigurationManager _configurationManager; + private readonly IStartupOptions _startupOptions; + private readonly IInstallationManager _installationManager; + + /// <summary> + /// Initializes a new instance of the <see cref="SystemManager"/> class. + /// </summary> + /// <param name="applicationLifetime">Instance of <see cref="IHostApplicationLifetime"/>.</param> + /// <param name="applicationHost">Instance of <see cref="IServerApplicationHost"/>.</param> + /// <param name="applicationPaths">Instance of <see cref="IServerApplicationPaths"/>.</param> + /// <param name="configurationManager">Instance of <see cref="IServerConfigurationManager"/>.</param> + /// <param name="startupOptions">Instance of <see cref="IStartupOptions"/>.</param> + /// <param name="installationManager">Instance of <see cref="IInstallationManager"/>.</param> + public SystemManager( + IHostApplicationLifetime applicationLifetime, + IServerApplicationHost applicationHost, + IServerApplicationPaths applicationPaths, + IServerConfigurationManager configurationManager, + IStartupOptions startupOptions, + IInstallationManager installationManager) + { + _applicationLifetime = applicationLifetime; + _applicationHost = applicationHost; + _applicationPaths = applicationPaths; + _configurationManager = configurationManager; + _startupOptions = startupOptions; + _installationManager = installationManager; + } + + private bool CanLaunchWebBrowser => Environment.UserInteractive + && !_startupOptions.IsService + && (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS()); + + /// <inheritdoc /> + public SystemInfo GetSystemInfo(HttpRequest request) + { + return new SystemInfo + { + HasPendingRestart = _applicationHost.HasPendingRestart, + IsShuttingDown = _applicationLifetime.ApplicationStopping.IsCancellationRequested, + Version = _applicationHost.ApplicationVersionString, + WebSocketPortNumber = _applicationHost.HttpPort, + CompletedInstallations = _installationManager.CompletedInstallations.ToArray(), + Id = _applicationHost.SystemId, + ProgramDataPath = _applicationPaths.ProgramDataPath, + WebPath = _applicationPaths.WebPath, + LogPath = _applicationPaths.LogDirectoryPath, + ItemsByNamePath = _applicationPaths.InternalMetadataPath, + InternalMetadataPath = _applicationPaths.InternalMetadataPath, + CachePath = _applicationPaths.CachePath, + CanLaunchWebBrowser = CanLaunchWebBrowser, + TranscodingTempPath = _configurationManager.GetTranscodePath(), + ServerName = _applicationHost.FriendlyName, + LocalAddress = _applicationHost.GetSmartApiUrl(request), + SupportsLibraryMonitor = true, + PackageName = _startupOptions.PackageName + }; + } + + /// <inheritdoc /> + public PublicSystemInfo GetPublicSystemInfo(HttpRequest request) + { + return new PublicSystemInfo + { + Version = _applicationHost.ApplicationVersionString, + ProductName = _applicationHost.Name, + Id = _applicationHost.SystemId, + ServerName = _applicationHost.FriendlyName, + LocalAddress = _applicationHost.GetSmartApiUrl(request), + StartupWizardCompleted = _configurationManager.CommonConfiguration.IsStartupWizardCompleted + }; + } + + /// <inheritdoc /> + public void Restart() => ShutdownInternal(true); + + /// <inheritdoc /> + public void Shutdown() => ShutdownInternal(false); + + private void ShutdownInternal(bool restart) + { + Task.Run(async () => + { + await Task.Delay(100).ConfigureAwait(false); + _applicationHost.ShouldRestart = restart; + _applicationLifetime.StopApplication(); + }); + } +} diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index 42ac4a9b4b..11095a97f0 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -10,7 +10,6 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; using MediaBrowser.Model.System; @@ -26,32 +25,36 @@ namespace Jellyfin.Api.Controllers; /// </summary> public class SystemController : BaseJellyfinApiController { + private readonly ILogger<SystemController> _logger; private readonly IServerApplicationHost _appHost; private readonly IApplicationPaths _appPaths; private readonly IFileSystem _fileSystem; - private readonly INetworkManager _network; - private readonly ILogger<SystemController> _logger; + private readonly INetworkManager _networkManager; + private readonly ISystemManager _systemManager; /// <summary> /// Initializes a new instance of the <see cref="SystemController"/> class. /// </summary> - /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param> + /// <param name="logger">Instance of <see cref="ILogger{SystemController}"/> interface.</param> + /// <param name="appPaths">Instance of <see cref="IServerApplicationPaths"/> interface.</param> /// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param> /// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param> - /// <param name="network">Instance of <see cref="INetworkManager"/> interface.</param> - /// <param name="logger">Instance of <see cref="ILogger{SystemController}"/> interface.</param> + /// <param name="networkManager">Instance of <see cref="INetworkManager"/> interface.</param> + /// <param name="systemManager">Instance of <see cref="ISystemManager"/> interface.</param> public SystemController( - IServerConfigurationManager serverConfigurationManager, + ILogger<SystemController> logger, IServerApplicationHost appHost, + IServerApplicationPaths appPaths, IFileSystem fileSystem, - INetworkManager network, - ILogger<SystemController> logger) + INetworkManager networkManager, + ISystemManager systemManager) { - _appPaths = serverConfigurationManager.ApplicationPaths; - _appHost = appHost; - _fileSystem = fileSystem; - _network = network; _logger = logger; + _appHost = appHost; + _appPaths = appPaths; + _fileSystem = fileSystem; + _networkManager = networkManager; + _systemManager = systemManager; } /// <summary> @@ -65,9 +68,7 @@ public class SystemController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public ActionResult<SystemInfo> GetSystemInfo() - { - return _appHost.GetSystemInfo(Request); - } + => _systemManager.GetSystemInfo(Request); /// <summary> /// Gets public information about the server. @@ -77,9 +78,7 @@ public class SystemController : BaseJellyfinApiController [HttpGet("Info/Public")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<PublicSystemInfo> GetPublicSystemInfo() - { - return _appHost.GetPublicSystemInfo(Request); - } + => _systemManager.GetPublicSystemInfo(Request); /// <summary> /// Pings the system. @@ -90,9 +89,7 @@ public class SystemController : BaseJellyfinApiController [HttpPost("Ping", Name = "PostPingSystem")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<string> PingSystem() - { - return _appHost.Name; - } + => _appHost.Name; /// <summary> /// Restarts the application. @@ -106,7 +103,7 @@ public class SystemController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status403Forbidden)] public ActionResult RestartApplication() { - _appHost.Restart(); + _systemManager.Restart(); return NoContent(); } @@ -122,7 +119,7 @@ public class SystemController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status403Forbidden)] public ActionResult ShutdownApplication() { - _appHost.Shutdown(); + _systemManager.Shutdown(); return NoContent(); } @@ -180,7 +177,7 @@ public class SystemController : BaseJellyfinApiController return new EndPointInfo { IsLocal = HttpContext.IsLocal(), - IsInNetwork = _network.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIP()) + IsInNetwork = _networkManager.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIP()) }; } @@ -218,7 +215,7 @@ public class SystemController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<IEnumerable<WakeOnLanInfo>> GetWakeOnLanInfo() { - var result = _network.GetMacAddresses() + var result = _networkManager.GetMacAddresses() .Select(i => new WakeOnLanInfo(i)); return Ok(result); } diff --git a/MediaBrowser.Common/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs index 5985d3dd82..23795c6be8 100644 --- a/MediaBrowser.Common/IApplicationHost.cs +++ b/MediaBrowser.Common/IApplicationHost.cs @@ -35,21 +35,15 @@ namespace MediaBrowser.Common string SystemId { get; } /// <summary> - /// Gets a value indicating whether this instance has pending kernel reload. + /// Gets a value indicating whether this instance has pending changes requiring a restart. /// </summary> - /// <value><c>true</c> if this instance has pending kernel reload; otherwise, <c>false</c>.</value> + /// <value><c>true</c> if this instance has a pending restart; otherwise, <c>false</c>.</value> bool HasPendingRestart { get; } /// <summary> - /// Gets a value indicating whether this instance is currently shutting down. + /// Gets or sets a value indicating whether the application should restart. /// </summary> - /// <value><c>true</c> if this instance is shutting down; otherwise, <c>false</c>.</value> - bool IsShuttingDown { get; } - - /// <summary> - /// Gets a value indicating whether the application should restart. - /// </summary> - bool ShouldRestart { get; } + bool ShouldRestart { get; set; } /// <summary> /// Gets the application version. @@ -91,11 +85,6 @@ namespace MediaBrowser.Common /// </summary> void NotifyPendingRestart(); - /// <summary> - /// Restarts this instance. - /// </summary> - void Restart(); - /// <summary> /// Gets the exports. /// </summary> @@ -127,11 +116,6 @@ namespace MediaBrowser.Common /// <returns>``0.</returns> T Resolve<T>(); - /// <summary> - /// Shuts down. - /// </summary> - void Shutdown(); - /// <summary> /// Initializes this instance. /// </summary> diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index 45ac5c3a85..e9c4d9e19a 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -4,7 +4,6 @@ using System.Net; using MediaBrowser.Common; -using MediaBrowser.Model.System; using Microsoft.AspNetCore.Http; namespace MediaBrowser.Controller @@ -16,8 +15,6 @@ namespace MediaBrowser.Controller { bool CoreStartupHasCompleted { get; } - bool CanLaunchWebBrowser { get; } - /// <summary> /// Gets the HTTP server port. /// </summary> @@ -41,15 +38,6 @@ namespace MediaBrowser.Controller /// <value>The name of the friendly.</value> string FriendlyName { get; } - /// <summary> - /// Gets the system info. - /// </summary> - /// <param name="request">The HTTP request.</param> - /// <returns>SystemInfo.</returns> - SystemInfo GetSystemInfo(HttpRequest request); - - PublicSystemInfo GetPublicSystemInfo(HttpRequest request); - /// <summary> /// Gets a URL specific for the request. /// </summary> diff --git a/MediaBrowser.Controller/ISystemManager.cs b/MediaBrowser.Controller/ISystemManager.cs new file mode 100644 index 0000000000..ef3034d2f5 --- /dev/null +++ b/MediaBrowser.Controller/ISystemManager.cs @@ -0,0 +1,34 @@ +using MediaBrowser.Model.System; +using Microsoft.AspNetCore.Http; + +namespace MediaBrowser.Controller; + +/// <summary> +/// A service for managing the application instance. +/// </summary> +public interface ISystemManager +{ + /// <summary> + /// Gets the system info. + /// </summary> + /// <param name="request">The HTTP request.</param> + /// <returns>The <see cref="SystemInfo"/>.</returns> + SystemInfo GetSystemInfo(HttpRequest request); + + /// <summary> + /// Gets the public system info. + /// </summary> + /// <param name="request">The HTTP request.</param> + /// <returns>The <see cref="PublicSystemInfo"/>.</returns> + PublicSystemInfo GetPublicSystemInfo(HttpRequest request); + + /// <summary> + /// Starts the application restart process. + /// </summary> + void Restart(); + + /// <summary> + /// Starts the application shutdown process. + /// </summary> + void Shutdown(); +} From cb8a8c3ef441dd048536a4bcd81d4f5ddf8485b2 Mon Sep 17 00:00:00 2001 From: Leo <leo-van@hotmail.com> Date: Thu, 5 Oct 2023 11:19:52 +0800 Subject: [PATCH 640/858] fix: use movie.nfo first when <filename>.nfo also exists (jellyfin/jellyfin#1558) --- MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs index 82e1dc860d..8fa22fad94 100644 --- a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs @@ -60,13 +60,13 @@ namespace MediaBrowser.XbmcMetadata.Savers } else { - yield return Path.ChangeExtension(item.Path, ".nfo"); - // only allow movie object to read movie.nfo, not owned videos (which will be itemtype video, not movie) if (!item.IsInMixedFolder && item.ItemType == typeof(Movie)) { yield return Path.Combine(item.ContainingFolderPath, "movie.nfo"); } + + yield return Path.ChangeExtension(item.Path, ".nfo"); } } From 130c035d4fe7e9c195d0f3f9bd8a216cc36e896e Mon Sep 17 00:00:00 2001 From: Jacob Slusser <jslusser.mobile@gmail.com> Date: Thu, 5 Oct 2023 05:02:10 -0700 Subject: [PATCH 641/858] #10333 Updates issue stale action to fix issues with not running (#10334) --- .github/workflows/repo-stale.yaml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/repo-stale.yaml b/.github/workflows/repo-stale.yaml index c753c1600a..b2e8a572b2 100644 --- a/.github/workflows/repo-stale.yaml +++ b/.github/workflows/repo-stale.yaml @@ -2,20 +2,21 @@ name: Stale Check on: schedule: - - cron: '30 1 * * *' + - cron: '30 */12 * * *' workflow_dispatch: permissions: issues: write pull-requests: write + actions: write jobs: issues: - name: Check issues + name: Check for stale issues runs-on: ubuntu-latest if: ${{ contains(github.repository, 'jellyfin/') }} steps: - - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0 + - uses: actions/stale@v8 with: repo-token: ${{ secrets.JF_BOT_TOKEN }} days-before-stale: 120 @@ -26,11 +27,11 @@ jobs: exempt-issue-labels: regression,security,roadmap,future,feature,enhancement,confirmed stale-issue-label: stale stale-issue-message: |- - This issue has gone 120 days without comment. To avoid abandoned issues, it will be closed in 21 days if there are no new comments. + This issue has gone 120 days without an update and will be closed within 21 days if there is no new activity. To prevent this issue from being closed, please confirm the issue has not already been fixed by providing updated examples or logs. - If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or master branch, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label. - - This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://docs.jellyfin.org/general/getting-help.html). + If you have any questions you can use one of several ways to [contact us](https://jellyfin.org/contact). + close-issue-message: |- + This issue was closed due to inactivity. prs-conflicts: name: Check PRs with merge conflicts From 7dd4c1223105edd926605bd59c20ba7591b085e2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Oct 2023 12:02:38 +0000 Subject: [PATCH 642/858] Pin actions/stale action to 1160a22 --- .github/workflows/repo-stale.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/repo-stale.yaml b/.github/workflows/repo-stale.yaml index b2e8a572b2..4eb0cf0996 100644 --- a/.github/workflows/repo-stale.yaml +++ b/.github/workflows/repo-stale.yaml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest if: ${{ contains(github.repository, 'jellyfin/') }} steps: - - uses: actions/stale@v8 + - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8 with: repo-token: ${{ secrets.JF_BOT_TOKEN }} days-before-stale: 120 From b87765bacec36aba3ee37ebc034458f36c637ffe Mon Sep 17 00:00:00 2001 From: Bond-009 <bond.009@outlook.com> Date: Thu, 5 Oct 2023 18:21:43 +0200 Subject: [PATCH 643/858] Update Jellyfin.Server.Implementations/Security/AuthorizationContext.cs Co-authored-by: Patrick Barron <barronpm@gmail.com> --- .../Security/AuthorizationContext.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs index f415d01115..77f8f7071b 100644 --- a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs +++ b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs @@ -49,7 +49,7 @@ namespace Jellyfin.Server.Implementations.Security /// <summary> /// Gets the authorization. /// </summary> - /// <param name="httpContext">The HTTP req.</param> + /// <param name="httpContext">The HTTP context.</param> /// <returns>Dictionary{System.StringSystem.String}.</returns> private async Task<AuthorizationInfo> GetAuthorization(HttpContext httpContext) { From 852f1dc0c1e3178955e2d1b2791b1e45f3f956b3 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Thu, 5 Oct 2023 23:16:17 +0200 Subject: [PATCH 644/858] Don't create non existent persons in LibraryManager.GetPerson return null instead. GetStudio, GetGenre, GetMusicGenre, GetYear, GetArtist still create a new one when the requested one doesn't exist Fixes #3901 --- .../Library/LibraryManager.cs | 27 ++++++++++--------- .../Controllers/PersonsControllerTests.cs | 26 ++++++++++++++++++ 2 files changed, 40 insertions(+), 13 deletions(-) create mode 100644 tests/Jellyfin.Server.Integration.Tests/Controllers/PersonsControllerTests.cs diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index b0a4a4151a..bdbf5f703c 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -46,7 +46,6 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.Library; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Tasks; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Episode = MediaBrowser.Controller.Entities.TV.Episode; using EpisodeInfo = Emby.Naming.TV.EpisodeInfo; @@ -839,19 +838,12 @@ namespace Emby.Server.Implementations.Library { var path = Person.GetPath(name); var id = GetItemByNameId<Person>(path); - if (GetItemById(id) is not Person item) + if (GetItemById(id) is Person item) { - item = new Person - { - Name = name, - Id = id, - DateCreated = DateTime.UtcNow, - DateModified = DateTime.UtcNow, - Path = path - }; + return item; } - return item; + return null; } /// <summary> @@ -2900,9 +2892,18 @@ namespace Emby.Server.Implementations.Library var saveEntity = false; var personEntity = GetPerson(person.Name); - // if PresentationUniqueKey is empty it's likely a new item. - if (string.IsNullOrEmpty(personEntity.PresentationUniqueKey)) + if (personEntity is null) { + var path = Person.GetPath(person.Name); + personEntity = new Person() + { + Name = person.Name, + Id = GetItemByNameId<Person>(path), + DateCreated = DateTime.UtcNow, + DateModified = DateTime.UtcNow, + Path = path + }; + personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey(); saveEntity = true; } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/PersonsControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/PersonsControllerTests.cs new file mode 100644 index 0000000000..38c64547c2 --- /dev/null +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/PersonsControllerTests.cs @@ -0,0 +1,26 @@ +using System.Net; +using System.Threading.Tasks; +using Xunit; + +namespace Jellyfin.Server.Integration.Tests.Controllers; + +public class PersonsControllerTests : IClassFixture<JellyfinApplicationFactory> +{ + private readonly JellyfinApplicationFactory _factory; + private static string? _accessToken; + + public PersonsControllerTests(JellyfinApplicationFactory factory) + { + _factory = factory; + } + + [Fact] + public async Task GetPerson_DoesntExist_NotFound() + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); + + using var response = await client.GetAsync($"Persons/DoesntExist"); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } +} From efc4c305a912eb92904289fa4a176db120047fba Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Thu, 5 Oct 2023 23:29:31 +0200 Subject: [PATCH 645/858] Use CryptoStream to convert stream from base64 Should be way more efficient --- Jellyfin.Api/Controllers/ImageController.cs | 43 ++++++++----------- .../Controllers/SubtitleController.cs | 8 ++-- MediaBrowser.Providers/Manager/ImageSaver.cs | 6 ++- 3 files changed, 28 insertions(+), 29 deletions(-) diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index 3c5f18af55..7b10ea170f 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -7,6 +7,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Net.Mime; +using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Attributes; @@ -78,6 +79,9 @@ public class ImageController : BaseJellyfinApiController _appPaths = appPaths; } + private static Stream GetFromBase64Stream(Stream inputStream) + => new CryptoStream(inputStream, new FromBase64Transform(), CryptoStreamMode.Read); + /// <summary> /// Sets the user image. /// </summary> @@ -116,8 +120,8 @@ public class ImageController : BaseJellyfinApiController return BadRequest("Incorrect ContentType."); } - var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); - await using (memoryStream.ConfigureAwait(false)) + var stream = GetFromBase64Stream(Request.Body); + await using (stream.ConfigureAwait(false)) { // Handle image/png; charset=utf-8 var mimeType = Request.ContentType?.Split(';').FirstOrDefault(); @@ -130,7 +134,7 @@ public class ImageController : BaseJellyfinApiController user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension)); await _providerManager - .SaveImage(memoryStream, mimeType, user.ProfileImage.Path) + .SaveImage(stream, mimeType, user.ProfileImage.Path) .ConfigureAwait(false); await _userManager.UpdateUserAsync(user).ConfigureAwait(false); @@ -176,8 +180,8 @@ public class ImageController : BaseJellyfinApiController return BadRequest("Incorrect ContentType."); } - var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); - await using (memoryStream.ConfigureAwait(false)) + var stream = GetFromBase64Stream(Request.Body); + await using (stream.ConfigureAwait(false)) { // Handle image/png; charset=utf-8 var mimeType = Request.ContentType?.Split(';').FirstOrDefault(); @@ -190,7 +194,7 @@ public class ImageController : BaseJellyfinApiController user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension)); await _providerManager - .SaveImage(memoryStream, mimeType, user.ProfileImage.Path) + .SaveImage(stream, mimeType, user.ProfileImage.Path) .ConfigureAwait(false); await _userManager.UpdateUserAsync(user).ConfigureAwait(false); @@ -372,12 +376,12 @@ public class ImageController : BaseJellyfinApiController return BadRequest("Incorrect ContentType."); } - var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); - await using (memoryStream.ConfigureAwait(false)) + var stream = GetFromBase64Stream(Request.Body); + await using (stream.ConfigureAwait(false)) { // Handle image/png; charset=utf-8 var mimeType = Request.ContentType?.Split(';').FirstOrDefault(); - await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false); + await _providerManager.SaveImage(item, stream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false); await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false); return NoContent(); @@ -416,12 +420,12 @@ public class ImageController : BaseJellyfinApiController return BadRequest("Incorrect ContentType."); } - var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); - await using (memoryStream.ConfigureAwait(false)) + var stream = GetFromBase64Stream(Request.Body); + await using (stream.ConfigureAwait(false)) { // Handle image/png; charset=utf-8 var mimeType = Request.ContentType?.Split(';').FirstOrDefault(); - await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false); + await _providerManager.SaveImage(item, stream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false); await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false); return NoContent(); @@ -1792,8 +1796,8 @@ public class ImageController : BaseJellyfinApiController return BadRequest("Incorrect ContentType."); } - var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); - await using (memoryStream.ConfigureAwait(false)) + var stream = GetFromBase64Stream(Request.Body); + await using (stream.ConfigureAwait(false)) { var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension); var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding"); @@ -1803,7 +1807,7 @@ public class ImageController : BaseJellyfinApiController var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); await using (fs.ConfigureAwait(false)) { - await memoryStream.CopyToAsync(fs, CancellationToken.None).ConfigureAwait(false); + await stream.CopyToAsync(fs, CancellationToken.None).ConfigureAwait(false); } return NoContent(); @@ -1833,15 +1837,6 @@ public class ImageController : BaseJellyfinApiController return NoContent(); } - private static async Task<MemoryStream> GetMemoryStream(Stream inputStream) - { - using var reader = new StreamReader(inputStream); - var text = await reader.ReadToEndAsync().ConfigureAwait(false); - - var bytes = Convert.FromBase64String(text); - return new MemoryStream(bytes, 0, bytes.Length, false, true); - } - private ImageInfo? GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex) { int? width = null; diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index 7d02550b68..fb89e96108 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -6,6 +6,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Net.Mime; +using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -405,9 +406,8 @@ public class SubtitleController : BaseJellyfinApiController [FromBody, Required] UploadSubtitleDto body) { var video = (Video)_libraryManager.GetItemById(itemId); - var data = Convert.FromBase64String(body.Data); - var memoryStream = new MemoryStream(data, 0, data.Length, false, true); - await using (memoryStream.ConfigureAwait(false)) + var stream = new CryptoStream(Request.Body, new FromBase64Transform(), CryptoStreamMode.Read); + await using (stream.ConfigureAwait(false)) { await _subtitleManager.UploadSubtitle( video, @@ -417,7 +417,7 @@ public class SubtitleController : BaseJellyfinApiController Language = body.Language, IsForced = body.IsForced, IsHearingImpaired = body.IsHearingImpaired, - Stream = memoryStream + Stream = stream }).ConfigureAwait(false); _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High); diff --git a/MediaBrowser.Providers/Manager/ImageSaver.cs b/MediaBrowser.Providers/Manager/ImageSaver.cs index e7c2cd2558..d827168314 100644 --- a/MediaBrowser.Providers/Manager/ImageSaver.cs +++ b/MediaBrowser.Providers/Manager/ImageSaver.cs @@ -263,7 +263,11 @@ namespace MediaBrowser.Providers.Manager var fileStreamOptions = AsyncFile.WriteOptions; fileStreamOptions.Mode = FileMode.Create; - fileStreamOptions.PreallocationSize = source.Length; + if (source.CanSeek) + { + fileStreamOptions.PreallocationSize = source.Length; + } + var fs = new FileStream(path, fileStreamOptions); await using (fs.ConfigureAwait(false)) { From 33b3331c72a811acb1caa03cfa5c3e492df56c50 Mon Sep 17 00:00:00 2001 From: fei long <feilongphone@gmail.com> Date: Fri, 6 Oct 2023 06:26:52 +0800 Subject: [PATCH 646/858] change Substring to AsSpan Co-authored-by: Bond-009 <bond.009@outlook.com> --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 4a71f25a78..72511ec4da 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -6244,7 +6244,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (GetAudioEncoder(state).StartsWith("pcm_", StringComparison.Ordinal)) { - audioTranscodeParams.Add(string.Concat("-f ", GetAudioEncoder(state).Substring(4))); + audioTranscodeParams.Add(string.Concat("-f ", GetAudioEncoder(state).AsSpan(4))); audioTranscodeParams.Add("-ar " + state.BaseRequest.AudioBitRate); } From b176beb88e22a36cc056439ac2a4df4fbe68f2c1 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Fri, 6 Oct 2023 00:40:09 +0200 Subject: [PATCH 647/858] Reduce string allocations Some simple changes to reduce the number of allocated strings --- Emby.Dlna/DlnaManager.cs | 2 +- .../ExternalFiles/ExternalPathParser.cs | 2 +- Emby.Naming/Video/StubResolver.cs | 7 ++-- Emby.Photos/PhotoProvider.cs | 2 +- .../IO/ManagedFileSystem.cs | 6 ++-- .../Library/LibraryManager.cs | 4 +-- .../Library/Resolvers/Audio/AudioResolver.cs | 6 ++-- .../Library/Resolvers/BaseVideoResolver.cs | 2 +- .../Library/Resolvers/Books/BookResolver.cs | 9 +++-- .../MediaEncoder/EncodingManager.cs | 2 +- .../Playlists/PlaylistManager.cs | 16 ++++----- .../Updates/InstallationManager.cs | 3 +- .../Controllers/DynamicHlsController.cs | 4 +-- .../Controllers/HlsSegmentController.cs | 9 ++--- Jellyfin.Api/Helpers/StreamingHelpers.cs | 10 +++--- Jellyfin.Api/Helpers/TranscodingJobHelper.cs | 2 +- .../MediaEncoding/EncodingHelper.cs | 18 +++++----- .../Attachments/AttachmentExtractor.cs | 36 +++---------------- .../Encoder/MediaEncoder.cs | 6 ++-- src/Jellyfin.Drawing.Skia/SkiaEncoder.cs | 2 +- .../StripCollageBuilder.cs | 12 +++---- 21 files changed, 63 insertions(+), 97 deletions(-) diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs index 99b3e6e7ef..d67cb67b54 100644 --- a/Emby.Dlna/DlnaManager.cs +++ b/Emby.Dlna/DlnaManager.cs @@ -228,7 +228,7 @@ namespace Emby.Dlna try { return _fileSystem.GetFilePaths(path) - .Where(i => string.Equals(Path.GetExtension(i), ".xml", StringComparison.OrdinalIgnoreCase)) + .Where(i => Path.GetExtension(i.AsSpan()).Equals(".xml", StringComparison.OrdinalIgnoreCase)) .Select(i => ParseProfileFile(i, type)) .Where(i => i is not null) .ToList()!; // We just filtered out all the nulls diff --git a/Emby.Naming/ExternalFiles/ExternalPathParser.cs b/Emby.Naming/ExternalFiles/ExternalPathParser.cs index 9531296711..4080ba10d3 100644 --- a/Emby.Naming/ExternalFiles/ExternalPathParser.cs +++ b/Emby.Naming/ExternalFiles/ExternalPathParser.cs @@ -43,7 +43,7 @@ namespace Emby.Naming.ExternalFiles return null; } - var extension = Path.GetExtension(path); + var extension = Path.GetExtension(path.AsSpan()); if (!(_type == DlnaProfileType.Subtitle && _namingOptions.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) && !(_type == DlnaProfileType.Audio && _namingOptions.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))) { diff --git a/Emby.Naming/Video/StubResolver.cs b/Emby.Naming/Video/StubResolver.cs index f7ba606e3e..4b9df19b08 100644 --- a/Emby.Naming/Video/StubResolver.cs +++ b/Emby.Naming/Video/StubResolver.cs @@ -26,19 +26,18 @@ namespace Emby.Naming.Video return false; } - var extension = Path.GetExtension(path); + var extension = Path.GetExtension(path.AsSpan()); if (!options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) { return false; } - path = Path.GetFileNameWithoutExtension(path); - var token = Path.GetExtension(path).TrimStart('.'); + var token = Path.GetExtension(Path.GetFileNameWithoutExtension(path.AsSpan())).TrimStart('.'); foreach (var rule in options.StubTypes) { - if (string.Equals(rule.Token, token, StringComparison.OrdinalIgnoreCase)) + if (token.Equals(rule.Token, StringComparison.OrdinalIgnoreCase)) { stubType = rule.StubType; return true; diff --git a/Emby.Photos/PhotoProvider.cs b/Emby.Photos/PhotoProvider.cs index f54066c57f..27329a7f2f 100644 --- a/Emby.Photos/PhotoProvider.cs +++ b/Emby.Photos/PhotoProvider.cs @@ -61,7 +61,7 @@ namespace Emby.Photos item.SetImagePath(ImageType.Primary, item.Path); // Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs - if (_includeExtensions.Contains(Path.GetExtension(item.Path), StringComparison.OrdinalIgnoreCase)) + if (_includeExtensions.Contains(Path.GetExtension(item.Path.AsSpan()), StringComparison.OrdinalIgnoreCase)) { try { diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 18b00ce0b2..4178936ce2 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -103,15 +103,17 @@ namespace Emby.Server.Implementations.IO return filePath; } + var filePathSpan = filePath.AsSpan(); + // relative path if (firstChar == '\\') { - filePath = filePath.Substring(1); + filePathSpan = filePathSpan.Slice(1); } try { - return Path.GetFullPath(Path.Combine(folderPath, filePath)); + return Path.GetFullPath(Path.Join(folderPath, filePathSpan)); } catch (ArgumentException) { diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index b0a4a4151a..845e7e3512 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -1162,7 +1162,7 @@ namespace Emby.Server.Implementations.Library Name = Path.GetFileName(dir), Locations = _fileSystem.GetFilePaths(dir, false) - .Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase)) + .Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCase)) .Select(i => { try @@ -3135,7 +3135,7 @@ namespace Emby.Server.Implementations.Library } var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true) - .Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase)) + .Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCase)) .FirstOrDefault(f => _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(f)).Equals(mediaPath, StringComparison.OrdinalIgnoreCase)); if (!string.IsNullOrEmpty(shortcut)) diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs index a74f824752..862f144e68 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs @@ -94,9 +94,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio if (AudioFileParser.IsAudioFile(args.Path, _namingOptions)) { - var extension = Path.GetExtension(args.Path); + var extension = Path.GetExtension(args.Path.AsSpan()); - if (string.Equals(extension, ".cue", StringComparison.OrdinalIgnoreCase)) + if (extension.Equals(".cue", StringComparison.OrdinalIgnoreCase)) { // if audio file exists of same name, return null return null; @@ -128,7 +128,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio if (item is not null) { - item.IsShortcut = string.Equals(extension, ".strm", StringComparison.OrdinalIgnoreCase); + item.IsShortcut = extension.Equals(".strm", StringComparison.OrdinalIgnoreCase); item.IsInMixedFolder = true; } diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs index 381796d0e3..779cfd5be4 100644 --- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs @@ -263,7 +263,7 @@ namespace Emby.Server.Implementations.Library.Resolvers return false; } - return directoryService.GetFilePaths(fullPath).Any(i => string.Equals(Path.GetExtension(i), ".vob", StringComparison.OrdinalIgnoreCase)); + return directoryService.GetFilePaths(fullPath).Any(i => Path.GetExtension(i.AsSpan()).Equals(".vob", StringComparison.OrdinalIgnoreCase)); } /// <summary> diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs index 042422c6f4..73861ff599 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs @@ -32,9 +32,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books return GetBook(args); } - var extension = Path.GetExtension(args.Path); + var extension = Path.GetExtension(args.Path.AsSpan()); - if (extension is not null && _validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) + if (_validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) { // It's a book return new Book @@ -51,12 +51,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books { var bookFiles = args.FileSystemChildren.Where(f => { - var fileExtension = Path.GetExtension(f.FullName) - ?? string.Empty; + var fileExtension = Path.GetExtension(f.FullName.AsSpan()); return _validExtensions.Contains( fileExtension, - StringComparer.OrdinalIgnoreCase); + StringComparison.OrdinalIgnoreCase); }).ToList(); // Don't return a Book if there is more (or less) than one document in the directory diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs index 7732e32d0a..896f47923f 100644 --- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs +++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs @@ -222,7 +222,7 @@ namespace Emby.Server.Implementations.MediaEncoder { var deadImages = images .Except(chapters.Select(i => i.ImagePath).Where(i => !string.IsNullOrEmpty(i)), StringComparer.OrdinalIgnoreCase) - .Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i), StringComparison.OrdinalIgnoreCase)) + .Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i.AsSpan()), StringComparison.OrdinalIgnoreCase)) .ToList(); foreach (var image in deadImages) diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 0cb450cdf3..649c499247 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -327,9 +327,9 @@ namespace Emby.Server.Implementations.Playlists // this is probably best done as a metadata provider // saving a file over itself will require some work to prevent this from happening when not needed var playlistPath = item.Path; - var extension = Path.GetExtension(playlistPath); + var extension = Path.GetExtension(playlistPath.AsSpan()); - if (string.Equals(".wpl", extension, StringComparison.OrdinalIgnoreCase)) + if (extension.Equals(".wpl", StringComparison.OrdinalIgnoreCase)) { var playlist = new WplPlaylist(); foreach (var child in item.GetLinkedChildren()) @@ -362,8 +362,7 @@ namespace Emby.Server.Implementations.Playlists string text = new WplContent().ToText(playlist); File.WriteAllText(playlistPath, text); } - - if (string.Equals(".zpl", extension, StringComparison.OrdinalIgnoreCase)) + else if (extension.Equals(".zpl", StringComparison.OrdinalIgnoreCase)) { var playlist = new ZplPlaylist(); foreach (var child in item.GetLinkedChildren()) @@ -396,8 +395,7 @@ namespace Emby.Server.Implementations.Playlists string text = new ZplContent().ToText(playlist); File.WriteAllText(playlistPath, text); } - - if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase)) + else if (extension.Equals(".m3u", StringComparison.OrdinalIgnoreCase)) { var playlist = new M3uPlaylist { @@ -428,8 +426,7 @@ namespace Emby.Server.Implementations.Playlists string text = new M3uContent().ToText(playlist); File.WriteAllText(playlistPath, text); } - - if (string.Equals(".m3u8", extension, StringComparison.OrdinalIgnoreCase)) + else if (extension.Equals(".m3u8", StringComparison.OrdinalIgnoreCase)) { var playlist = new M3uPlaylist(); playlist.IsExtended = true; @@ -458,8 +455,7 @@ namespace Emby.Server.Implementations.Playlists string text = new M3uContent().ToText(playlist); File.WriteAllText(playlistPath, text); } - - if (string.Equals(".pls", extension, StringComparison.OrdinalIgnoreCase)) + else if (extension.Equals(".pls", StringComparison.OrdinalIgnoreCase)) { var playlist = new PlsPlaylist(); foreach (var child in item.GetLinkedChildren()) diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index 6c198b6f99..77d385ba1c 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -504,8 +504,7 @@ namespace Emby.Server.Implementations.Updates private async Task PerformPackageInstallation(InstallationInfo package, PluginStatus status, CancellationToken cancellationToken) { - var extension = Path.GetExtension(package.SourceUrl); - if (!string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase)) + if (!Path.GetExtension(package.SourceUrl.AsSpan()).Equals(".zip", StringComparison.OrdinalIgnoreCase)) { _logger.LogError("Only zip packages are supported. {SourceUrl} is not a zip archive.", package.SourceUrl); return; diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 065a4ce5c6..4026a38f8d 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -2041,9 +2041,9 @@ public class DynamicHlsController : BaseJellyfinApiController return null; } - var playlistFilename = Path.GetFileNameWithoutExtension(playlist); + var playlistFilename = Path.GetFileNameWithoutExtension(playlist.AsSpan()); - var indexString = Path.GetFileNameWithoutExtension(file.Name).Substring(playlistFilename.Length); + var indexString = Path.GetFileNameWithoutExtension(file.Name.AsSpan()).Slice(playlistFilename.Length); return int.Parse(indexString, NumberStyles.Integer, CultureInfo.InvariantCulture); } diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs index d7cec865e1..6eedfd8c7f 100644 --- a/Jellyfin.Api/Controllers/HlsSegmentController.cs +++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs @@ -59,7 +59,7 @@ public class HlsSegmentController : BaseJellyfinApiController public ActionResult GetHlsAudioSegmentLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string segmentId) { // TODO: Deprecate with new iOS app - var file = segmentId + Path.GetExtension(Request.Path); + var file = string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan())); var transcodePath = _serverConfigurationManager.GetTranscodePath(); file = Path.GetFullPath(Path.Combine(transcodePath, file)); var fileDir = Path.GetDirectoryName(file); @@ -85,11 +85,12 @@ public class HlsSegmentController : BaseJellyfinApiController [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")] public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId) { - var file = playlistId + Path.GetExtension(Request.Path); + var file = string.Concat(playlistId, Path.GetExtension(Request.Path.Value.AsSpan())); var transcodePath = _serverConfigurationManager.GetTranscodePath(); file = Path.GetFullPath(Path.Combine(transcodePath, file)); var fileDir = Path.GetDirectoryName(file); - if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture) || Path.GetExtension(file) != ".m3u8") + if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture) + || Path.GetExtension(file.AsSpan()).Equals(".m3u8", StringComparison.OrdinalIgnoreCase)) { return BadRequest("Invalid segment."); } @@ -138,7 +139,7 @@ public class HlsSegmentController : BaseJellyfinApiController [FromRoute, Required] string segmentId, [FromRoute, Required] string segmentContainer) { - var file = segmentId + Path.GetExtension(Request.Path); + var file = string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan())); var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath(); file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file)); diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index 782cd65685..d15fe4fab2 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -243,7 +243,7 @@ public static class StreamingHelpers ? GetOutputFileExtension(state, mediaSource) : ("." + state.OutputContainer); - state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId); + state.OutputFilePath = GetOutputFilePath(state, ext, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId); return state; } @@ -418,11 +418,11 @@ public static class StreamingHelpers /// <returns>System.String.</returns> private static string? GetOutputFileExtension(StreamState state, MediaSourceInfo? mediaSource) { - var ext = Path.GetExtension(state.RequestedUrl); + var ext = Path.GetExtension(state.RequestedUrl.AsSpan()); - if (!string.IsNullOrEmpty(ext)) + if (ext.IsEmpty) { - return ext; + return null; } // Try to infer based on the desired video codec @@ -504,7 +504,7 @@ public static class StreamingHelpers /// <param name="deviceId">The device id.</param> /// <param name="playSessionId">The play session id.</param> /// <returns>The complete file path, including the folder, for the transcoding file.</returns> - private static string GetOutputFilePath(StreamState state, string outputFileExtension, IServerConfigurationManager serverConfigurationManager, string? deviceId, string? playSessionId) + private static string GetOutputFilePath(StreamState state, string? outputFileExtension, IServerConfigurationManager serverConfigurationManager, string? deviceId, string? playSessionId) { var data = $"{state.MediaPath}-{state.UserAgent}-{deviceId!}-{playSessionId!}"; diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index 73ebb396d0..c16a586d60 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -538,7 +538,7 @@ public class TranscodingJobHelper : IDisposable await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false); } - if (state.SubtitleStream.IsExternal && string.Equals(Path.GetExtension(state.SubtitleStream.Path), ".mks", StringComparison.OrdinalIgnoreCase)) + if (state.SubtitleStream.IsExternal && Path.GetExtension(state.SubtitleStream.Path.AsSpan()).Equals(".mks", StringComparison.OrdinalIgnoreCase)) { string subtitlePath = state.SubtitleStream.Path; string subtitlePathArgument = string.Format(CultureInfo.InvariantCulture, "file:\"{0}\"", subtitlePath.Replace("\"", "\\\"", StringComparison.Ordinal)); diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index c311d3b8ab..3be7a4252b 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -548,25 +548,25 @@ namespace MediaBrowser.Controller.MediaEncoding /// <returns>System.Nullable{VideoCodecs}.</returns> public string InferVideoCodec(string url) { - var ext = Path.GetExtension(url); + var ext = Path.GetExtension(url.AsSpan()); - if (string.Equals(ext, ".asf", StringComparison.OrdinalIgnoreCase)) + if (ext.Equals(".asf", StringComparison.OrdinalIgnoreCase)) { return "wmv"; } - if (string.Equals(ext, ".webm", StringComparison.OrdinalIgnoreCase)) + if (ext.Equals(".webm", StringComparison.OrdinalIgnoreCase)) { // TODO: this may not always mean VP8, as the codec ages return "vp8"; } - if (string.Equals(ext, ".ogg", StringComparison.OrdinalIgnoreCase) || string.Equals(ext, ".ogv", StringComparison.OrdinalIgnoreCase)) + if (ext.Equals(".ogg", StringComparison.OrdinalIgnoreCase) || ext.Equals(".ogv", StringComparison.OrdinalIgnoreCase)) { return "theora"; } - if (string.Equals(ext, ".m3u8", StringComparison.OrdinalIgnoreCase) || string.Equals(ext, ".ts", StringComparison.OrdinalIgnoreCase)) + if (ext.Equals(".m3u8", StringComparison.OrdinalIgnoreCase) || ext.Equals(".ts", StringComparison.OrdinalIgnoreCase)) { return "h264"; } @@ -1080,10 +1080,10 @@ namespace MediaBrowser.Controller.MediaEncoding && state.SubtitleStream.IsExternal) { var subtitlePath = state.SubtitleStream.Path; - var subtitleExtension = Path.GetExtension(subtitlePath); + var subtitleExtension = Path.GetExtension(subtitlePath.AsSpan()); - if (string.Equals(subtitleExtension, ".sub", StringComparison.OrdinalIgnoreCase) - || string.Equals(subtitleExtension, ".sup", StringComparison.OrdinalIgnoreCase)) + if (subtitleExtension.Equals(".sub", StringComparison.OrdinalIgnoreCase) + || subtitleExtension.Equals(".sup", StringComparison.OrdinalIgnoreCase)) { var idxFile = Path.ChangeExtension(subtitlePath, ".idx"); if (File.Exists(idxFile)) @@ -6033,7 +6033,7 @@ namespace MediaBrowser.Controller.MediaEncoding var format = string.Empty; var keyFrame = string.Empty; - if (string.Equals(Path.GetExtension(outputPath), ".mp4", StringComparison.OrdinalIgnoreCase) + if (Path.GetExtension(outputPath.AsSpan()).Equals(".mp4", StringComparison.OrdinalIgnoreCase) && state.BaseRequest.Context == EncodingContext.Streaming) { // Comparison: https://github.com/jansmolders86/mediacenterjs/blob/master/lib/transcoding/desktop.js diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs index 989e386a51..0ec0c84d41 100644 --- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs +++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs @@ -1,4 +1,3 @@ -#nullable disable #pragma warning disable CS1591 using System; @@ -23,7 +22,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.MediaEncoding.Attachments { - public class AttachmentExtractor : IAttachmentExtractor, IDisposable + public sealed class AttachmentExtractor : IAttachmentExtractor { private readonly ILogger<AttachmentExtractor> _logger; private readonly IApplicationPaths _appPaths; @@ -34,8 +33,6 @@ namespace MediaBrowser.MediaEncoding.Attachments private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks = new ConcurrentDictionary<string, SemaphoreSlim>(); - private bool _disposed = false; - public AttachmentExtractor( ILogger<AttachmentExtractor> logger, IApplicationPaths appPaths, @@ -296,7 +293,7 @@ namespace MediaBrowser.MediaEncoding.Attachments ArgumentException.ThrowIfNullOrEmpty(outputPath); - Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); + Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new ArgumentException("Path can't be a root directory.", nameof(outputPath))); var processArgs = string.Format( CultureInfo.InvariantCulture, @@ -391,33 +388,8 @@ namespace MediaBrowser.MediaEncoding.Attachments filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D", CultureInfo.InvariantCulture); } - var prefix = filename.Substring(0, 1); - return Path.Combine(_appPaths.DataPath, "attachments", prefix, filename); - } - - /// <inheritdoc /> - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// <summary> - /// Releases unmanaged and - optionally - managed resources. - /// </summary> - /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> - protected virtual void Dispose(bool disposing) - { - if (_disposed) - { - return; - } - - if (disposing) - { - } - - _disposed = true; + var prefix = filename.AsSpan(0, 1); + return Path.Join(_appPaths.DataPath, "attachments", prefix, filename); } } } diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 346e97ae12..59c3505616 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -316,10 +316,8 @@ namespace MediaBrowser.MediaEncoding.Encoder { var files = _fileSystem.GetFilePaths(path, recursive); - var excludeExtensions = new[] { ".c" }; - - return files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), filename, StringComparison.OrdinalIgnoreCase) - && !excludeExtensions.Contains(Path.GetExtension(i) ?? string.Empty)); + return files.FirstOrDefault(i => Path.GetFileNameWithoutExtension(i.AsSpan()).Equals(filename, StringComparison.OrdinalIgnoreCase) + && !Path.GetExtension(i.AsSpan()).Equals(".c", StringComparison.OrdinalIgnoreCase)); } catch (Exception) { diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 126c0503e0..416f602179 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -188,7 +188,7 @@ public class SkiaEncoder : IImageEncoder return path; } - var tempPath = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + Path.GetExtension(path)); + var tempPath = Path.Combine(_appPaths.TempDirectory, string.Concat(Guid.NewGuid().ToString(), Path.GetExtension(path.AsSpan()))); var directory = Path.GetDirectoryName(tempPath) ?? throw new ResourceNotFoundException($"Provided path ({tempPath}) is not valid."); Directory.CreateDirectory(directory); File.Copy(path, tempPath, true); diff --git a/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs index 6dff7aa9b4..4aff26c16b 100644 --- a/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs +++ b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs @@ -38,25 +38,25 @@ public partial class StripCollageBuilder { ArgumentNullException.ThrowIfNull(outputPath); - var ext = Path.GetExtension(outputPath); + var ext = Path.GetExtension(outputPath.AsSpan()); - if (string.Equals(ext, ".jpg", StringComparison.OrdinalIgnoreCase) - || string.Equals(ext, ".jpeg", StringComparison.OrdinalIgnoreCase)) + if (ext.Equals(".jpg", StringComparison.OrdinalIgnoreCase) + || ext.Equals(".jpeg", StringComparison.OrdinalIgnoreCase)) { return SKEncodedImageFormat.Jpeg; } - if (string.Equals(ext, ".webp", StringComparison.OrdinalIgnoreCase)) + if (ext.Equals(".webp", StringComparison.OrdinalIgnoreCase)) { return SKEncodedImageFormat.Webp; } - if (string.Equals(ext, ".gif", StringComparison.OrdinalIgnoreCase)) + if (ext.Equals(".gif", StringComparison.OrdinalIgnoreCase)) { return SKEncodedImageFormat.Gif; } - if (string.Equals(ext, ".bmp", StringComparison.OrdinalIgnoreCase)) + if (ext.Equals(".bmp", StringComparison.OrdinalIgnoreCase)) { return SKEncodedImageFormat.Bmp; } From e9e1fd214b608007735b2a2a3ed722d0e4937da8 Mon Sep 17 00:00:00 2001 From: "[ ]" <christiangrece45@gmail.com> Date: Thu, 5 Oct 2023 07:08:18 +0000 Subject: [PATCH 648/858] Translated using Weblate (Pirate (pr)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pr/ --- Emby.Server.Implementations/Localization/Core/pr.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/pr.json b/Emby.Server.Implementations/Localization/Core/pr.json index d2446a7fdc..26dc5ce82f 100644 --- a/Emby.Server.Implementations/Localization/Core/pr.json +++ b/Emby.Server.Implementations/Localization/Core/pr.json @@ -29,5 +29,8 @@ "Forced": "Pressed", "External": "Outboard", "HeaderFavoriteEpisodes": "Treasured Tales", - "HeaderFavoriteShows": "Treasured Tales" + "HeaderFavoriteShows": "Treasured Tales", + "ChapterNameValue": "Piece {0}", + "HeaderFavoriteSongs": "Treasured Chimes", + "HeaderNextUp": "Incoming" } From a18b3fbe70429f55852f0aebeda4e02e2abdef77 Mon Sep 17 00:00:00 2001 From: Claus Vium <cvium@users.noreply.github.com> Date: Fri, 6 Oct 2023 10:49:20 +0200 Subject: [PATCH 649/858] simplify the if --- .../MediaEncoding/EncodingHelper.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index f8d2dd40fd..449ea64899 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -5697,21 +5697,13 @@ namespace MediaBrowser.Controller.MediaEncoding inputModifier = inputModifier.Trim(); // Apply -probesize if configured - var probeSizeArgument = string.Empty; var ffmpegProbeSize = _config.GetFFmpegProbeSize(); if (!string.IsNullOrEmpty(ffmpegProbeSize)) { - probeSizeArgument = $"-probesize {probeSizeArgument}"; + inputModifier += $" -probesize {ffmpegProbeSize}"; } - if (!string.IsNullOrEmpty(probeSizeArgument)) - { - inputModifier += $" {probeSizeArgument}"; - } - - inputModifier = inputModifier.Trim(); - var userAgentParam = GetUserAgentParam(state); if (!string.IsNullOrEmpty(userAgentParam)) From ffb3df9e7725a067934e1ce22483007209f17010 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Oct 2023 08:50:50 -0600 Subject: [PATCH 650/858] Update github/codeql-action action to v2.22.0 (#10351) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 5dc38e188d..8e764b0194 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@ddccb873888234080b77e9bc2d4764d5ccaaccf9 # v2.21.9 + uses: github/codeql-action/init@2cb752a87e96af96708ab57187ab6372ee1973ab # v2.22.0 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@ddccb873888234080b77e9bc2d4764d5ccaaccf9 # v2.21.9 + uses: github/codeql-action/autobuild@2cb752a87e96af96708ab57187ab6372ee1973ab # v2.22.0 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@ddccb873888234080b77e9bc2d4764d5ccaaccf9 # v2.21.9 + uses: github/codeql-action/analyze@2cb752a87e96af96708ab57187ab6372ee1973ab # v2.22.0 From bdca4ed322a6eaa1fa1810e3187aa7948a574c60 Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Fri, 6 Oct 2023 12:46:35 -0400 Subject: [PATCH 651/858] Add XmlReader.GetPersonFromXmlNode --- .../Extensions/XmlReaderExtensions.cs | 107 ++++++++++++++++ .../Parsers/BaseItemXmlParser.cs | 100 +-------------- .../Parsers/BaseNfoParser.cs | 119 +----------------- 3 files changed, 116 insertions(+), 210 deletions(-) create mode 100644 MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs diff --git a/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs b/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs new file mode 100644 index 0000000000..cd7db91dd4 --- /dev/null +++ b/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs @@ -0,0 +1,107 @@ +using System; +using System.Globalization; +using System.Xml; +using Jellyfin.Data.Enums; +using MediaBrowser.Controller.Entities; + +namespace MediaBrowser.Controller.Extensions; + +/// <summary> +/// Provides extension methods for <see cref="XmlReader"/> to parse <see cref="BaseItem"/>'s. +/// </summary> +public static class XmlReaderExtensions +{ + /// <summary> + /// Parses a <see cref="PersonInfo"/> from the xml node. + /// </summary> + /// <param name="reader">The <see cref="XmlReader"/>.</param> + /// <returns>A <see cref="PersonInfo"/>, or <c>null</c> if none is found.</returns> + public static PersonInfo? GetPersonFromXmlNode(this XmlReader reader) + { + ArgumentNullException.ThrowIfNull(reader); + + if (reader.IsEmptyElement) + { + reader.Read(); + return null; + } + + var name = string.Empty; + var type = PersonKind.Actor; // If type is not specified assume actor + var role = string.Empty; + int? sortOrder = null; + string? imageUrl = null; + + using var subtree = reader.ReadSubtree(); + subtree.MoveToContent(); + subtree.Read(); + + while (subtree is { EOF: false, ReadState: ReadState.Interactive }) + { + if (subtree.NodeType != XmlNodeType.Element) + { + subtree.Read(); + continue; + } + + switch (subtree.Name) + { + case "name": + case "Name": + name = subtree.ReadElementContentAsString(); + break; + case "role": + case "Role": + var roleValue = subtree.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(roleValue)) + { + role = roleValue; + } + + break; + case "type": + case "Type": + Enum.TryParse(subtree.ReadElementContentAsString(), true, out type); + break; + case "order": + case "sortorder": + case "SortOrder": + if (int.TryParse( + subtree.ReadElementContentAsString(), + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out var intVal)) + { + sortOrder = intVal; + } + + break; + case "thumb": + var thumb = subtree.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(thumb)) + { + imageUrl = thumb; + } + + break; + default: + subtree.Skip(); + break; + } + } + + if (string.IsNullOrWhiteSpace(name)) + { + return null; + } + + return new PersonInfo + { + Name = name.Trim(), + Role = role, + Type = type, + SortOrder = sortOrder, + ImageUrl = imageUrl + }; + } +} diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs index cb369d8377..df4d40a100 100644 --- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs @@ -9,6 +9,7 @@ using System.Xml; using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; @@ -929,29 +930,13 @@ namespace MediaBrowser.LocalMetadata.Parsers { case "Person": case "Actor": - { - if (reader.IsEmptyElement) + var person = reader.GetPersonFromXmlNode(); + if (person is not null) { - reader.Read(); - continue; - } - - using (var subtree = reader.ReadSubtree()) - { - foreach (var person in GetPersonsFromXmlNode(subtree)) - { - if (string.IsNullOrWhiteSpace(person.Name)) - { - continue; - } - - item.AddPerson(person); - } + item.AddPerson(person); } break; - } - default: reader.Skip(); break; @@ -1041,83 +1026,6 @@ namespace MediaBrowser.LocalMetadata.Parsers } } - /// <summary> - /// Gets the persons from XML node. - /// </summary> - /// <param name="reader">The reader.</param> - /// <returns>IEnumerable{PersonInfo}.</returns> - private IEnumerable<PersonInfo> GetPersonsFromXmlNode(XmlReader reader) - { - var name = string.Empty; - var type = PersonKind.Actor; // If type is not specified assume actor - var role = string.Empty; - int? sortOrder = null; - - reader.MoveToContent(); - reader.Read(); - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "Name": - name = reader.ReadElementContentAsString(); - break; - - case "Type": - { - var val = reader.ReadElementContentAsString(); - _ = Enum.TryParse(val, true, out type); - - break; - } - - case "Role": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - role = val; - } - - break; - } - - case "SortOrder": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intVal)) - { - sortOrder = intVal; - } - } - - break; - } - - default: - reader.Skip(); - break; - } - } - else - { - reader.Read(); - } - } - - var personInfo = new PersonInfo { Name = name.Trim(), Role = role, Type = type, SortOrder = sortOrder }; - - return new[] { personInfo }; - } - /// <summary> /// Get linked child. /// </summary> diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs index 5b68924acb..e11378c786 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs @@ -13,6 +13,7 @@ using MediaBrowser.Common.Providers; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; @@ -581,27 +582,13 @@ namespace MediaBrowser.XbmcMetadata.Parsers } case "actor": + var person = reader.GetPersonFromXmlNode(); + if (person is not null) { - if (!reader.IsEmptyElement) - { - using (var subtree = reader.ReadSubtree()) - { - var person = GetPersonFromXmlNode(subtree); - - if (!string.IsNullOrWhiteSpace(person.Name)) - { - itemResult.AddPerson(person); - } - } - } - else - { - reader.Read(); - } - - break; + itemResult.AddPerson(person); } + break; case "trailer": { var val = reader.ReadElementContentAsString(); @@ -1196,102 +1183,6 @@ namespace MediaBrowser.XbmcMetadata.Parsers } } - /// <summary> - /// Gets the persons from a XML node. - /// </summary> - /// <param name="reader">The <see cref="XmlReader"/>.</param> - /// <returns>IEnumerable{PersonInfo}.</returns> - private PersonInfo GetPersonFromXmlNode(XmlReader reader) - { - var name = string.Empty; - var type = PersonKind.Actor; // If type is not specified assume actor - var role = string.Empty; - int? sortOrder = null; - string? imageUrl = null; - - reader.MoveToContent(); - reader.Read(); - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "name": - name = reader.ReadElementContentAsString(); - break; - - case "role": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - role = val; - } - - break; - } - - case "type": - { - var val = reader.ReadElementContentAsString(); - if (!Enum.TryParse(val, true, out type)) - { - type = PersonKind.Actor; - } - - break; - } - - case "order": - case "sortorder": - { - var val = reader.ReadElementContentAsString(); - - if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intVal)) - { - sortOrder = intVal; - } - - break; - } - - case "thumb": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - imageUrl = val; - } - - break; - } - - default: - reader.Skip(); - break; - } - } - else - { - reader.Read(); - } - } - - return new PersonInfo - { - Name = name.Trim(), - Role = role, - Type = type, - SortOrder = sortOrder, - ImageUrl = imageUrl - }; - } - internal XmlReaderSettings GetXmlReaderSettings() => new XmlReaderSettings() { From 1a6ec2c74062e28552089a8b85dc5d45224d4e86 Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Fri, 6 Oct 2023 13:40:08 -0400 Subject: [PATCH 652/858] Add GetStringArray and GetPersonArray to XmlReaderExtensions --- .../Extensions/XmlReaderExtensions.cs | 38 +++++++ .../Parsers/BaseItemXmlParser.cs | 100 ++---------------- .../Parsers/BaseNfoParser.cs | 48 ++------- 3 files changed, 53 insertions(+), 133 deletions(-) diff --git a/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs b/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs index cd7db91dd4..8e4b7c8594 100644 --- a/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs +++ b/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.Globalization; +using System.Linq; using System.Xml; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; @@ -104,4 +106,40 @@ public static class XmlReaderExtensions ImageUrl = imageUrl }; } + + /// <summary> + /// Used to split names of comma or pipe delimited genres and people. + /// </summary> + /// <param name="reader">The <see cref="XmlReader"/>.</param> + /// <returns>IEnumerable{System.String}.</returns> + public static IEnumerable<string> GetStringArray(this XmlReader reader) + { + ArgumentNullException.ThrowIfNull(reader); + var value = reader.ReadElementContentAsString(); + + // Only split by comma if there is no pipe in the string + // We have to be careful to not split names like Matthew, Jr. + var separator = !value.Contains('|', StringComparison.Ordinal) + && !value.Contains(';', StringComparison.Ordinal) + ? new[] { ',' } + : new[] { '|', ';' }; + + foreach (var part in value.Trim().Trim(separator).Split(separator)) + { + if (!string.IsNullOrWhiteSpace(part)) + { + yield return part.Trim(); + } + } + } + + /// <summary> + /// Parses a <see cref="PersonInfo"/> array from the xml node. + /// </summary> + /// <param name="reader">The <see cref="XmlReader"/>.</param> + /// <param name="personKind">The <see cref="PersonKind"/>.</param> + /// <returns>The <see cref="IEnumerable{PersonInfo}"/>.</returns> + public static IEnumerable<PersonInfo> GetPersonArray(this XmlReader reader, PersonKind personKind) + => reader.GetStringArray() + .Select(part => new PersonInfo { Name = part, Type = personKind }); } diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs index df4d40a100..d62862be40 100644 --- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs @@ -354,93 +354,40 @@ namespace MediaBrowser.LocalMetadata.Parsers } case "Network": - { - foreach (var name in SplitNames(reader.ReadElementContentAsString())) + foreach (var name in reader.GetStringArray()) { - if (string.IsNullOrWhiteSpace(name)) - { - continue; - } - item.AddStudio(name); } break; - } - case "Director": - { - foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Director })) + foreach (var director in reader.GetPersonArray(PersonKind.Director)) { - if (string.IsNullOrWhiteSpace(p.Name)) - { - continue; - } - - itemResult.AddPerson(p); + itemResult.AddPerson(director); } break; - } - case "Writer": - { - foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Writer })) + foreach (var writer in reader.GetPersonArray(PersonKind.Writer)) { - if (string.IsNullOrWhiteSpace(p.Name)) - { - continue; - } - - itemResult.AddPerson(p); + itemResult.AddPerson(writer); } break; - } - case "Actors": - { - var actors = reader.ReadInnerXml(); - - if (actors.Contains('<', StringComparison.Ordinal)) + foreach (var actor in reader.GetPersonArray(PersonKind.Actor)) { - // This is one of the mis-named "Actors" full nodes created by MB2 - // Create a reader and pass it to the persons node processor - using var xmlReader = XmlReader.Create(new StringReader($"<Persons>{actors}</Persons>")); - FetchDataFromPersonsNode(xmlReader, itemResult); - } - else - { - // Old-style piped string - foreach (var p in SplitNames(actors).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Actor })) - { - if (string.IsNullOrWhiteSpace(p.Name)) - { - continue; - } - - itemResult.AddPerson(p); - } + itemResult.AddPerson(actor); } break; - } - case "GuestStars": - { - foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.GuestStar })) + foreach (var guestStar in reader.GetPersonArray(PersonKind.GuestStar)) { - if (string.IsNullOrWhiteSpace(p.Name)) - { - continue; - } - - itemResult.AddPerson(p); + itemResult.AddPerson(guestStar); } break; - } - case "Trailer": { var val = reader.ReadElementContentAsString(); @@ -1129,34 +1076,5 @@ namespace MediaBrowser.LocalMetadata.Parsers return null; } - - /// <summary> - /// Used to split names of comma or pipe delimited genres and people. - /// </summary> - /// <param name="value">The value.</param> - /// <returns>IEnumerable{System.String}.</returns> - private IEnumerable<string> SplitNames(string value) - { - // Only split by comma if there is no pipe in the string - // We have to be careful to not split names like Matthew, Jr. - var separator = !value.Contains('|', StringComparison.Ordinal) - && !value.Contains(';', StringComparison.Ordinal) ? new[] { ',' } : new[] { '|', ';' }; - - value = value.Trim().Trim(separator); - - return string.IsNullOrWhiteSpace(value) ? Array.Empty<string>() : Split(value, separator, StringSplitOptions.RemoveEmptyEntries); - } - - /// <summary> - /// Provides an additional overload for string.split. - /// </summary> - /// <param name="val">The val.</param> - /// <param name="separators">The separators.</param> - /// <param name="options">The options.</param> - /// <returns>System.String[][].</returns> - private string[] Split(string val, char[] separators, StringSplitOptions options) - { - return val.Split(separators, options); - } } } diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs index e11378c786..797ce59e05 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs @@ -527,21 +527,12 @@ namespace MediaBrowser.XbmcMetadata.Parsers } case "director": + foreach (var director in reader.GetPersonArray(PersonKind.Director)) { - var val = reader.ReadElementContentAsString(); - foreach (var p in SplitNames(val).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Director })) - { - if (string.IsNullOrWhiteSpace(p.Name)) - { - continue; - } - - itemResult.AddPerson(p); - } - - break; + itemResult.AddPerson(director); } + break; case "credits": { var val = reader.ReadElementContentAsString(); @@ -566,21 +557,12 @@ namespace MediaBrowser.XbmcMetadata.Parsers } case "writer": + foreach (var writer in reader.GetPersonArray(PersonKind.Writer)) { - var val = reader.ReadElementContentAsString(); - foreach (var p in SplitNames(val).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Writer })) - { - if (string.IsNullOrWhiteSpace(p.Name)) - { - continue; - } - - itemResult.AddPerson(p); - } - - break; + itemResult.AddPerson(writer); } + break; case "actor": var person = reader.GetPersonFromXmlNode(); if (person is not null) @@ -1192,24 +1174,6 @@ namespace MediaBrowser.XbmcMetadata.Parsers IgnoreComments = true }; - /// <summary> - /// Used to split names of comma or pipe delimited genres and people. - /// </summary> - /// <param name="value">The value.</param> - /// <returns>IEnumerable{System.String}.</returns> - private IEnumerable<string> SplitNames(string value) - { - // Only split by comma if there is no pipe in the string - // We have to be careful to not split names like Matthew, Jr. - var separator = !value.Contains('|', StringComparison.Ordinal) && !value.Contains(';', StringComparison.Ordinal) - ? new[] { ',' } - : new[] { '|', ';' }; - - value = value.Trim().Trim(separator); - - return string.IsNullOrWhiteSpace(value) ? Array.Empty<string>() : value.Split(separator, StringSplitOptions.RemoveEmptyEntries); - } - /// <summary> /// Parses the <see cref="ImageType"/> from the NFO aspect property. /// </summary> From 99832642cebcce3f93b49004a1dd10def3fb227d Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Fri, 6 Oct 2023 14:18:56 -0400 Subject: [PATCH 653/858] Add TryParseDateTime and TryParseDateTimeExact to XmlReaderExtensions --- .../Extensions/XmlReaderExtensions.cs | 47 ++++++++++++ .../Parsers/BaseItemXmlParser.cs | 43 ++--------- .../Parsers/BaseNfoParser.cs | 72 +++++-------------- 3 files changed, 73 insertions(+), 89 deletions(-) diff --git a/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs b/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs index 8e4b7c8594..cb239e3145 100644 --- a/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs +++ b/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Xml; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; +using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Extensions; @@ -13,6 +14,52 @@ namespace MediaBrowser.Controller.Extensions; /// </summary> public static class XmlReaderExtensions { + /// <summary> + /// Parses a <see cref="DateTime"/> from the current node. + /// </summary> + /// <param name="reader">The <see cref="XmlReader"/>.</param> + /// <param name="logger">The <see cref="ILogger"/> to use on failure.</param> + /// <param name="value">The parsed <see cref="DateTime"/>.</param> + /// <returns>A value indicating whether the parsing succeeded.</returns> + public static bool TryReadDateTime(this XmlReader reader, ILogger logger, out DateTime value) + { + ArgumentNullException.ThrowIfNull(reader); + ArgumentNullException.ThrowIfNull(logger); + + var text = reader.ReadElementContentAsString(); + if (DateTime.TryParse( + text, + CultureInfo.InvariantCulture, + DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, + out value)) + { + return true; + } + + logger.LogWarning("Invalid date: {Date}", text); + return false; + } + + /// <summary> + /// Parses a <see cref="DateTime"/> from the current node. + /// </summary> + /// <param name="reader">The <see cref="XmlReader"/>.</param> + /// <param name="formatString">The date format string.</param> + /// <param name="value">The parsed <see cref="DateTime"/>.</param> + /// <returns>A value indicating whether the parsing succeeded.</returns> + public static bool TryReadDateTimeExact(this XmlReader reader, string formatString, out DateTime value) + { + ArgumentNullException.ThrowIfNull(reader); + ArgumentNullException.ThrowIfNull(formatString); + + return DateTime.TryParseExact( + reader.ReadElementContentAsString(), + formatString, + CultureInfo.InvariantCulture, + DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, + out value); + } + /// <summary> /// Parses a <see cref="PersonInfo"/> from the xml node. /// </summary> diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs index d62862be40..b77b2b43e4 100644 --- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs @@ -129,26 +129,13 @@ namespace MediaBrowser.LocalMetadata.Parsers switch (reader.Name) { - // DateCreated case "Added": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) + if (reader.TryReadDateTime(Logger, out var dateCreated)) { - if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out var added)) - { - item.DateCreated = added; - } - else - { - Logger.LogWarning("Invalid Added value found: {Value}", val); - } + item.DateCreated = dateCreated; } break; - } - case "OriginalTitle": { var val = reader.ReadElementContentAsString(); @@ -465,37 +452,21 @@ namespace MediaBrowser.LocalMetadata.Parsers case "BirthDate": case "PremiereDate": case "FirstAired": - { - var firstAired = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(firstAired)) + if (reader.TryReadDateTimeExact("yyyy-MM-dd", out var firstAired)) { - if (DateTime.TryParseExact(firstAired, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal | DateTimeStyles.AdjustToUniversal, out var airDate) && airDate.Year > 1850) - { - item.PremiereDate = airDate; - item.ProductionYear = airDate.Year; - } + item.PremiereDate = firstAired; + item.ProductionYear = firstAired.Year; } break; - } - case "DeathDate": case "EndDate": - { - var firstAired = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(firstAired)) + if (reader.TryReadDateTimeExact("yyyy-MM-dd", out var endDate)) { - if (DateTime.TryParseExact(firstAired, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal | DateTimeStyles.AdjustToUniversal, out var airDate) && airDate.Year > 1850) - { - item.EndDate = airDate; - } + item.EndDate = endDate; } break; - } - case "CollectionNumber": var tmdbCollection = reader.ReadElementContentAsString(); if (!string.IsNullOrWhiteSpace(tmdbCollection)) diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs index 797ce59e05..6e02add775 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs @@ -268,23 +268,13 @@ namespace MediaBrowser.XbmcMetadata.Parsers switch (reader.Name) { - // DateCreated case "dateadded": + if (reader.TryReadDateTime(Logger, out var dateCreated)) { - var val = reader.ReadElementContentAsString(); - - if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var added)) - { - item.DateCreated = added; - } - else - { - Logger.LogWarning("Invalid Added value found: {Value}", val); - } - - break; + item.DateCreated = dateCreated; } + break; case "originaltitle": { var val = reader.ReadElementContentAsString(); @@ -373,9 +363,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers { var val = reader.ReadElementContentAsString(); if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var count) - && Guid.TryParse(nfoConfiguration.UserId, out var guid)) + && Guid.TryParse(nfoConfiguration.UserId, out var playCountUserId)) { - var user = _userManager.GetUserById(guid); + var user = _userManager.GetUserById(playCountUserId); userData = _userDataManager.GetUserData(user, item); userData.PlayCount = count; _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None); @@ -385,26 +375,16 @@ namespace MediaBrowser.XbmcMetadata.Parsers } case "lastplayed": + if (reader.TryReadDateTime(Logger, out var lastPlayed) + && Guid.TryParse(nfoConfiguration.UserId, out var lastPlayedUserId)) { - var val = reader.ReadElementContentAsString(); - if (Guid.TryParse(nfoConfiguration.UserId, out var guid)) - { - if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var added)) - { - var user = _userManager.GetUserById(guid); - userData = _userDataManager.GetUserData(user, item); - userData.LastPlayedDate = added; - _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None); - } - else - { - Logger.LogWarning("Invalid lastplayed value found: {Value}", val); - } - } - - break; + var user = _userManager.GetUserById(lastPlayedUserId); + userData = _userDataManager.GetUserData(user, item); + userData.LastPlayedDate = lastPlayed; + _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None); } + break; case "countrycode": { var val = reader.ReadElementContentAsString(); @@ -641,34 +621,20 @@ namespace MediaBrowser.XbmcMetadata.Parsers case "formed": case "premiered": case "releasedate": + if (reader.TryReadDateTimeExact(nfoConfiguration.ReleaseDateFormat, out var releaseDate)) { - var formatString = nfoConfiguration.ReleaseDateFormat; - - var val = reader.ReadElementContentAsString(); - - if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var date) && date.Year > 1850) - { - item.PremiereDate = date; - item.ProductionYear = date.Year; - } - - break; + item.PremiereDate = releaseDate; + item.ProductionYear = releaseDate.Year; } + break; case "enddate": + if (reader.TryReadDateTimeExact(nfoConfiguration.ReleaseDateFormat, out var endDate)) { - var formatString = nfoConfiguration.ReleaseDateFormat; - - var val = reader.ReadElementContentAsString(); - - if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var date) && date.Year > 1850) - { - item.EndDate = date; - } - - break; + item.EndDate = endDate; } + break; case "genre": { var val = reader.ReadElementContentAsString(); From 8a7a1cc72337a5190ac093a471c2bf3369022be2 Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Fri, 6 Oct 2023 14:53:05 -0400 Subject: [PATCH 654/858] Add ReadNormalizedString to XmlReaderExtensions --- .../Extensions/XmlReaderExtensions.cs | 12 + .../Parsers/BaseItemXmlParser.cs | 225 +++--------------- .../Parsers/PlaylistXmlParser.cs | 9 +- .../Parsers/BaseNfoParser.cs | 159 ++++--------- .../Parsers/EpisodeNfoParser.cs | 14 +- .../Parsers/MovieNfoParser.cs | 29 +-- .../Parsers/SeasonNfoParser.cs | 14 +- .../Parsers/SeriesNfoParser.cs | 22 +- 8 files changed, 112 insertions(+), 372 deletions(-) diff --git a/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs b/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs index cb239e3145..661133b771 100644 --- a/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs +++ b/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs @@ -14,6 +14,18 @@ namespace MediaBrowser.Controller.Extensions; /// </summary> public static class XmlReaderExtensions { + /// <summary> + /// Reads a trimmed string from the current node. + /// </summary> + /// <param name="reader">The <see cref="XmlReader"/>.</param> + /// <returns>The trimmed content.</returns> + public static string ReadNormalizedString(this XmlReader reader) + { + ArgumentNullException.ThrowIfNull(reader); + + return reader.ReadElementContentAsString().Trim(); + } + /// <summary> /// Parses a <see cref="DateTime"/> from the current node. /// </summary> diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs index b77b2b43e4..d115bc2496 100644 --- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs @@ -137,21 +137,11 @@ namespace MediaBrowser.LocalMetadata.Parsers break; case "OriginalTitle": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrEmpty(val)) - { - item.OriginalTitle = val; - } - + item.OriginalTitle = reader.ReadNormalizedString(); break; - } - case "LocalTitle": - item.Name = reader.ReadElementContentAsString(); + item.Name = reader.ReadNormalizedString(); break; - case "CriticRating": { var text = reader.ReadElementContentAsString(); @@ -165,63 +155,26 @@ namespace MediaBrowser.LocalMetadata.Parsers } case "SortTitle": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - item.ForcedSortName = val; - } - + item.ForcedSortName = reader.ReadNormalizedString(); break; - } - case "Overview": case "Description": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - item.Overview = val; - } - + item.Overview = reader.ReadNormalizedString(); break; - } - case "Language": - { - var val = reader.ReadElementContentAsString(); - - item.PreferredMetadataLanguage = val; - + item.PreferredMetadataLanguage = reader.ReadNormalizedString(); break; - } - case "CountryCode": - { - var val = reader.ReadElementContentAsString(); - - item.PreferredMetadataCountryCode = val; - + item.PreferredMetadataCountryCode = reader.ReadNormalizedString(); break; - } - case "PlaceOfBirth": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) + var placeOfBirth = reader.ReadNormalizedString(); + if (!string.IsNullOrEmpty(placeOfBirth) && item is Person person) { - if (item is Person person) - { - person.ProductionLocations = new[] { val }; - } + person.ProductionLocations = new[] { placeOfBirth }; } break; - } - case "LockedFields": { var val = reader.ReadElementContentAsString(); @@ -263,10 +216,7 @@ namespace MediaBrowser.LocalMetadata.Parsers { if (!reader.IsEmptyElement) { - using (var subtree = reader.ReadSubtree()) - { - FetchFromCountriesNode(subtree); - } + reader.Skip(); } else { @@ -278,29 +228,11 @@ namespace MediaBrowser.LocalMetadata.Parsers case "ContentRating": case "MPAARating": - { - var rating = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(rating)) - { - item.OfficialRating = rating; - } - + item.OfficialRating = reader.ReadNormalizedString(); break; - } - case "CustomRating": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - item.CustomRating = val; - } - + item.CustomRating = reader.ReadNormalizedString(); break; - } - case "RunningTime": { var text = reader.ReadElementContentAsString(); @@ -317,16 +249,13 @@ namespace MediaBrowser.LocalMetadata.Parsers } case "AspectRatio": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val) && item is IHasAspectRatio hasAspectRatio) + var aspectRatio = reader.ReadNormalizedString(); + if (!string.IsNullOrEmpty(aspectRatio) && item is IHasAspectRatio hasAspectRatio) { - hasAspectRatio.AspectRatio = val; + hasAspectRatio.AspectRatio = aspectRatio; } break; - } case "LockData": { @@ -376,32 +305,21 @@ namespace MediaBrowser.LocalMetadata.Parsers break; case "Trailer": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) + var trailer = reader.ReadNormalizedString(); + if (!string.IsNullOrEmpty(trailer)) { - item.AddTrailerUrl(val); + item.AddTrailerUrl(trailer); } break; - } - case "DisplayOrder": - { - var val = reader.ReadElementContentAsString(); - - if (item is IHasDisplayOrder hasDisplayOrder) + var displayOrder = reader.ReadNormalizedString(); + if (!string.IsNullOrEmpty(displayOrder) && item is IHasDisplayOrder hasDisplayOrder) { - if (!string.IsNullOrWhiteSpace(val)) - { - hasDisplayOrder.DisplayOrder = val; - } + hasDisplayOrder.DisplayOrder = displayOrder; } break; - } - case "Trailers": { if (!reader.IsEmptyElement) @@ -468,8 +386,8 @@ namespace MediaBrowser.LocalMetadata.Parsers break; case "CollectionNumber": - var tmdbCollection = reader.ReadElementContentAsString(); - if (!string.IsNullOrWhiteSpace(tmdbCollection)) + var tmdbCollection = reader.ReadNormalizedString(); + if (!string.IsNullOrEmpty(tmdbCollection)) { item.SetProviderId(MetadataProvider.TmdbCollection, tmdbCollection); } @@ -672,41 +590,6 @@ namespace MediaBrowser.LocalMetadata.Parsers item.Shares = list.ToArray(); } - private void FetchFromCountriesNode(XmlReader reader) - { - reader.MoveToContent(); - reader.Read(); - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "Country": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - } - - break; - } - - default: - reader.Skip(); - break; - } - } - else - { - reader.Read(); - } - } - } - /// <summary> /// Fetches from taglines node. /// </summary> @@ -725,17 +608,8 @@ namespace MediaBrowser.LocalMetadata.Parsers switch (reader.Name) { case "Tagline": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - item.Tagline = val; - } - + item.Tagline = reader.ReadNormalizedString(); break; - } - default: reader.Skip(); break; @@ -766,17 +640,13 @@ namespace MediaBrowser.LocalMetadata.Parsers switch (reader.Name) { case "Genre": - { - var genre = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(genre)) + var genre = reader.ReadNormalizedString(); + if (!string.IsNullOrEmpty(genre)) { item.AddGenre(genre); } break; - } - default: reader.Skip(); break; @@ -804,17 +674,13 @@ namespace MediaBrowser.LocalMetadata.Parsers switch (reader.Name) { case "Tag": - { - var tag = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(tag)) + var tag = reader.ReadNormalizedString(); + if (!string.IsNullOrEmpty(tag)) { tags.Add(tag); } break; - } - default: reader.Skip(); break; @@ -880,17 +746,13 @@ namespace MediaBrowser.LocalMetadata.Parsers switch (reader.Name) { case "Trailer": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) + var trailer = reader.ReadNormalizedString(); + if (!string.IsNullOrEmpty(trailer)) { - item.AddTrailerUrl(val); + item.AddTrailerUrl(trailer); } break; - } - default: reader.Skip(); break; @@ -921,17 +783,13 @@ namespace MediaBrowser.LocalMetadata.Parsers switch (reader.Name) { case "Studio": - { - var studio = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(studio)) + var studio = reader.ReadNormalizedString(); + if (!string.IsNullOrEmpty(studio)) { item.AddStudio(studio); } break; - } - default: reader.Skip(); break; @@ -964,17 +822,11 @@ namespace MediaBrowser.LocalMetadata.Parsers switch (reader.Name) { case "Path": - { - linkedItem.Path = reader.ReadElementContentAsString(); + linkedItem.Path = reader.ReadNormalizedString(); break; - } - case "ItemId": - { - linkedItem.LibraryItemId = reader.ReadElementContentAsString(); + linkedItem.LibraryItemId = reader.ReadNormalizedString(); break; - } - default: reader.Skip(); break; @@ -1015,11 +867,8 @@ namespace MediaBrowser.LocalMetadata.Parsers switch (reader.Name) { case "UserId": - { - item.UserId = reader.ReadElementContentAsString(); + item.UserId = reader.ReadNormalizedString(); break; - } - case "CanEdit": { item.CanEdit = string.Equals(reader.ReadElementContentAsString(), "true", StringComparison.OrdinalIgnoreCase); @@ -1027,10 +876,8 @@ namespace MediaBrowser.LocalMetadata.Parsers } default: - { reader.Skip(); break; - } } } else diff --git a/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs index 88b190f2b4..879a3616bd 100644 --- a/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Xml; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Providers; using Microsoft.Extensions.Logging; @@ -30,12 +31,8 @@ namespace MediaBrowser.LocalMetadata.Parsers switch (reader.Name) { case "PlaylistMediaType": - { - item.PlaylistMediaType = reader.ReadElementContentAsString(); - + item.PlaylistMediaType = reader.ReadNormalizedString(); break; - } - case "PlaylistItems": if (!reader.IsEmptyElement) @@ -94,10 +91,8 @@ namespace MediaBrowser.LocalMetadata.Parsers } default: - { reader.Skip(); break; - } } } else diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs index 6e02add775..af405d7559 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs @@ -276,27 +276,16 @@ namespace MediaBrowser.XbmcMetadata.Parsers break; case "originaltitle": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrEmpty(val)) - { - item.OriginalTitle = val; - } - - break; - } - + item.OriginalTitle = reader.ReadNormalizedString(); + break; case "name": case "title": case "localtitle": - item.Name = reader.ReadElementContentAsString(); + item.Name = reader.ReadNormalizedString(); break; - case "sortname": - item.SortName = reader.ReadElementContentAsString(); + item.SortName = reader.ReadNormalizedString(); break; - case "criticrating": { var text = reader.ReadElementContentAsString(); @@ -310,40 +299,16 @@ namespace MediaBrowser.XbmcMetadata.Parsers } case "sorttitle": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - item.ForcedSortName = val; - } - - break; - } - + item.ForcedSortName = reader.ReadNormalizedString(); + break; case "biography": case "plot": case "review": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - item.Overview = val; - } - - break; - } - + item.Overview = reader.ReadNormalizedString(); + break; case "language": - { - var val = reader.ReadElementContentAsString(); - - item.PreferredMetadataLanguage = val; - - break; - } - + item.PreferredMetadataLanguage = reader.ReadNormalizedString(); + break; case "watched": { var val = reader.ReadElementContentAsBoolean(); @@ -386,14 +351,8 @@ namespace MediaBrowser.XbmcMetadata.Parsers break; case "countrycode": - { - var val = reader.ReadElementContentAsString(); - - item.PreferredMetadataCountryCode = val; - - break; - } - + item.PreferredMetadataCountryCode = reader.ReadNormalizedString(); + break; case "lockedfields": { var val = reader.ReadElementContentAsString(); @@ -415,9 +374,8 @@ namespace MediaBrowser.XbmcMetadata.Parsers } case "tagline": - item.Tagline = reader.ReadElementContentAsString(); + item.Tagline = reader.ReadNormalizedString(); break; - case "country": { var val = reader.ReadElementContentAsString(); @@ -434,29 +392,11 @@ namespace MediaBrowser.XbmcMetadata.Parsers } case "mpaa": - { - var rating = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(rating)) - { - item.OfficialRating = rating; - } - - break; - } - + item.OfficialRating = reader.ReadNormalizedString(); + break; case "customrating": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - item.CustomRating = val; - } - - break; - } - + item.CustomRating = reader.ReadNormalizedString(); + break; case "runtime": { var text = reader.ReadElementContentAsString(); @@ -470,18 +410,13 @@ namespace MediaBrowser.XbmcMetadata.Parsers } case "aspectratio": + var aspectRatio = reader.ReadNormalizedString(); + if (!string.IsNullOrEmpty(aspectRatio) && item is IHasAspectRatio hasAspectRatio) { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val) - && item is IHasAspectRatio hasAspectRatio) - { - hasAspectRatio.AspectRatio = val; - } - - break; + hasAspectRatio.AspectRatio = aspectRatio; } + break; case "lockdata": { var val = reader.ReadElementContentAsString(); @@ -495,17 +430,13 @@ namespace MediaBrowser.XbmcMetadata.Parsers } case "studio": + var studio = reader.ReadNormalizedString(); + if (!string.IsNullOrEmpty(studio)) { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - item.AddStudio(val); - } - - break; + item.AddStudio(studio); } + break; case "director": foreach (var director in reader.GetPersonArray(PersonKind.Director)) { @@ -552,31 +483,24 @@ namespace MediaBrowser.XbmcMetadata.Parsers break; case "trailer": + var trailer = reader.ReadNormalizedString(); + if (!string.IsNullOrEmpty(trailer)) { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - val = val.Replace("plugin://plugin.video.youtube/?action=play_video&videoid=", BaseNfoSaver.YouTubeWatchUrl, StringComparison.OrdinalIgnoreCase); - - item.AddTrailerUrl(val); - } - - break; + item.AddTrailerUrl(trailer.Replace( + "plugin://plugin.video.youtube/?action=play_video&videoid=", + BaseNfoSaver.YouTubeWatchUrl, + StringComparison.OrdinalIgnoreCase)); } + break; case "displayorder": + var displayOrder = reader.ReadNormalizedString(); + if (!string.IsNullOrEmpty(displayOrder) && item is IHasDisplayOrder hasDisplayOrder) { - var val = reader.ReadElementContentAsString(); - - if (item is IHasDisplayOrder hasDisplayOrder && !string.IsNullOrWhiteSpace(val)) - { - hasDisplayOrder.DisplayOrder = val; - } - - break; + hasDisplayOrder.DisplayOrder = displayOrder; } + break; case "year": { var val = reader.ReadElementContentAsString(); @@ -656,16 +580,13 @@ namespace MediaBrowser.XbmcMetadata.Parsers case "style": case "tag": + var tag = reader.ReadNormalizedString(); + if (!string.IsNullOrEmpty(tag)) { - var val = reader.ReadElementContentAsString(); - if (!string.IsNullOrWhiteSpace(val)) - { - item.AddTag(val); - } - - break; + item.AddTag(tag); } + break; case "fileinfo": { if (!reader.IsEmptyElement) diff --git a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs index d2f349ad7a..e7676eb3a1 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Xml; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using Microsoft.Extensions.Logging; @@ -237,17 +238,8 @@ namespace MediaBrowser.XbmcMetadata.Parsers } case "showtitle": - { - var showtitle = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(showtitle)) - { - item.SeriesName = showtitle; - } - - break; - } - + item.SeriesName = reader.ReadNormalizedString(); + break; default: base.FetchDataFromXmlNode(reader, itemResult); break; diff --git a/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs index ecfed6873c..16ea5e3eae 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs @@ -5,6 +5,7 @@ using System.Xml; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; @@ -113,31 +114,23 @@ namespace MediaBrowser.XbmcMetadata.Parsers } case "artist": + var artist = reader.ReadNormalizedString(); + if (!string.IsNullOrEmpty(artist) && item is MusicVideo artistVideo) { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val) && item is MusicVideo movie) - { - var list = movie.Artists.ToList(); - list.Add(val); - movie.Artists = list.ToArray(); - } - - break; + var list = artistVideo.Artists.ToList(); + list.Add(artist); + artistVideo.Artists = list.ToArray(); } + break; case "album": + var album = reader.ReadNormalizedString(); + if (!string.IsNullOrEmpty(album) && item is MusicVideo albumVideo) { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val) && item is MusicVideo movie) - { - movie.Album = val; - } - - break; + albumVideo.Album = album; } + break; default: base.FetchDataFromXmlNode(reader, itemResult); break; diff --git a/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs index 51d5f932bc..486a2588a1 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs @@ -2,6 +2,7 @@ using System.Globalization; using System.Xml; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using Microsoft.Extensions.Logging; @@ -56,17 +57,8 @@ namespace MediaBrowser.XbmcMetadata.Parsers } case "seasonname": - { - var name = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(name)) - { - item.Name = name; - } - - break; - } - + item.Name = reader.ReadNormalizedString(); + break; default: base.FetchDataFromXmlNode(reader, itemResult); break; diff --git a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs index f22b861eba..dbcfe7997a 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs @@ -1,9 +1,9 @@ using System; -using System.Collections.Generic; using System.Globalization; using System.Xml; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; @@ -76,23 +76,11 @@ namespace MediaBrowser.XbmcMetadata.Parsers } case "airs_dayofweek": - { - item.AirDays = TVUtils.GetAirDays(reader.ReadElementContentAsString()); - break; - } - + item.AirDays = TVUtils.GetAirDays(reader.ReadElementContentAsString()); + break; case "airs_time": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - item.AirTime = val; - } - - break; - } - + item.AirTime = reader.ReadNormalizedString(); + break; case "status": { var status = reader.ReadElementContentAsString(); From 0e51ffa16983e6133d6b6ea295d84b0121241d79 Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Fri, 6 Oct 2023 15:35:26 -0400 Subject: [PATCH 655/858] Add TryReadInt to XmlReaderExtensions --- .../Extensions/XmlReaderExtensions.cs | 13 ++ .../Parsers/BaseItemXmlParser.cs | 23 +-- .../Parsers/BaseNfoParser.cs | 41 ++---- .../Parsers/EpisodeNfoParser.cs | 136 ++++-------------- .../Parsers/SeasonNfoParser.cs | 15 +- 5 files changed, 62 insertions(+), 166 deletions(-) diff --git a/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs b/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs index 661133b771..1b3fa9d239 100644 --- a/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs +++ b/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs @@ -26,6 +26,19 @@ public static class XmlReaderExtensions return reader.ReadElementContentAsString().Trim(); } + /// <summary> + /// Reads an int from the current node. + /// </summary> + /// <param name="reader">The <see cref="XmlReader"/>.</param> + /// <param name="value">The parsed <c>int</c>.</param> + /// <returns>A value indicating whether the parsing succeeded.</returns> + public static bool TryReadInt(this XmlReader reader, out int value) + { + ArgumentNullException.ThrowIfNull(reader); + + return int.TryParse(reader.ReadElementContentAsString(), CultureInfo.InvariantCulture, out value); + } + /// <summary> /// Parses a <see cref="DateTime"/> from the current node. /// </summary> diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs index d115bc2496..0181c864d5 100644 --- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs @@ -234,20 +234,16 @@ namespace MediaBrowser.LocalMetadata.Parsers item.CustomRating = reader.ReadNormalizedString(); break; case "RunningTime": - { - var text = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(text)) + var runtimeText = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(runtimeText)) { - if (int.TryParse(text.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime)) + if (int.TryParse(runtimeText.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime)) { item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks; } } break; - } - case "AspectRatio": var aspectRatio = reader.ReadNormalizedString(); if (!string.IsNullOrEmpty(aspectRatio) && item is IHasAspectRatio hasAspectRatio) @@ -256,7 +252,6 @@ namespace MediaBrowser.LocalMetadata.Parsers } break; - case "LockData": { var val = reader.ReadElementContentAsString(); @@ -336,20 +331,12 @@ namespace MediaBrowser.LocalMetadata.Parsers } case "ProductionYear": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) + if (reader.TryReadInt(out var productionYear) && productionYear > 1850) { - if (int.TryParse(val, out var productionYear) && productionYear > 1850) - { - item.ProductionYear = productionYear; - } + item.ProductionYear = productionYear; } break; - } - case "Rating": case "IMDBrating": { diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs index af405d7559..eb7c021426 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs @@ -325,20 +325,16 @@ namespace MediaBrowser.XbmcMetadata.Parsers } case "playcount": + if (reader.TryReadInt(out var count) + && Guid.TryParse(nfoConfiguration.UserId, out var playCountUserId)) { - var val = reader.ReadElementContentAsString(); - if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var count) - && Guid.TryParse(nfoConfiguration.UserId, out var playCountUserId)) - { - var user = _userManager.GetUserById(playCountUserId); - userData = _userDataManager.GetUserData(user, item); - userData.PlayCount = count; - _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None); - } - - break; + var user = _userManager.GetUserById(playCountUserId); + userData = _userDataManager.GetUserData(user, item); + userData.PlayCount = count; + _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None); } + break; case "lastplayed": if (reader.TryReadDateTime(Logger, out var lastPlayed) && Guid.TryParse(nfoConfiguration.UserId, out var lastPlayedUserId)) @@ -398,17 +394,13 @@ namespace MediaBrowser.XbmcMetadata.Parsers item.CustomRating = reader.ReadNormalizedString(); break; case "runtime": + var runtimeText = reader.ReadElementContentAsString(); + if (int.TryParse(runtimeText.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime)) { - var text = reader.ReadElementContentAsString(); - - if (int.TryParse(text.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime)) - { - item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks; - } - - break; + item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks; } + break; case "aspectratio": var aspectRatio = reader.ReadNormalizedString(); if (!string.IsNullOrEmpty(aspectRatio) && item is IHasAspectRatio hasAspectRatio) @@ -502,17 +494,12 @@ namespace MediaBrowser.XbmcMetadata.Parsers break; case "year": + if (reader.TryReadInt(out var productionYear) && productionYear > 1850) { - var val = reader.ReadElementContentAsString(); - - if (int.TryParse(val, out var productionYear) && productionYear > 1850) - { - item.ProductionYear = productionYear; - } - - break; + item.ProductionYear = productionYear; } + break; case "rating": { var rating = reader.ReadElementContentAsString(); diff --git a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs index e7676eb3a1..79966f8b80 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs @@ -1,5 +1,4 @@ using System; -using System.Globalization; using System.IO; using System.Threading; using System.Xml; @@ -113,130 +112,49 @@ namespace MediaBrowser.XbmcMetadata.Parsers switch (reader.Name) { case "season": + if (reader.TryReadInt(out var seasonNumber)) { - var number = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(number)) - { - if (int.TryParse(number, out var num)) - { - item.ParentIndexNumber = num; - } - } - - break; + item.ParentIndexNumber = seasonNumber; } + break; case "episode": + if (reader.TryReadInt(out var episodeNumber)) { - var number = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(number)) - { - if (int.TryParse(number, out var num)) - { - item.IndexNumber = num; - } - } - - break; + item.IndexNumber = episodeNumber; } + break; case "episodenumberend": + if (reader.TryReadInt(out var episodeNumberEnd)) { - var number = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(number)) - { - if (int.TryParse(number, out var num)) - { - item.IndexNumberEnd = num; - } - } - - break; + item.IndexNumberEnd = episodeNumberEnd; } + break; case "airsbefore_episode": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - // int.TryParse is local aware, so it can be problematic, force us culture - if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval)) - { - item.AirsBeforeEpisodeNumber = rval; - } - } - - break; - } - - case "airsafter_season": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - // int.TryParse is local aware, so it can be problematic, force us culture - if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval)) - { - item.AirsAfterSeasonNumber = rval; - } - } - - break; - } - - case "airsbefore_season": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - // int.TryParse is local aware, so it can be problematic, force us culture - if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval)) - { - item.AirsBeforeSeasonNumber = rval; - } - } - - break; - } - - case "displayseason": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - // int.TryParse is local aware, so it can be problematic, force us culture - if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval)) - { - item.AirsBeforeSeasonNumber = rval; - } - } - - break; - } - case "displayepisode": + if (reader.TryReadInt(out var airsBeforeEpisode)) { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - // int.TryParse is local aware, so it can be problematic, force us culture - if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval)) - { - item.AirsBeforeEpisodeNumber = rval; - } - } - - break; + item.AirsBeforeEpisodeNumber = airsBeforeEpisode; } + break; + case "airsafter_season": + if (reader.TryReadInt(out var airsAfterSeason)) + { + item.AirsAfterSeasonNumber = airsAfterSeason; + } + + break; + case "airsbefore_season": + case "displayseason": + if (reader.TryReadInt(out var airsBeforeSeason)) + { + item.AirsBeforeSeasonNumber = airsBeforeSeason; + } + + break; case "showtitle": item.SeriesName = reader.ReadNormalizedString(); break; diff --git a/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs index 486a2588a1..e13f0d9976 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs @@ -1,4 +1,3 @@ -using System.Globalization; using System.Xml; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Entities.TV; @@ -42,20 +41,12 @@ namespace MediaBrowser.XbmcMetadata.Parsers switch (reader.Name) { case "seasonnumber": + if (reader.TryReadInt(out var seasonNumber)) { - var number = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(number)) - { - if (int.TryParse(number, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num)) - { - item.IndexNumber = num; - } - } - - break; + item.IndexNumber = seasonNumber; } + break; case "seasonname": item.Name = reader.ReadNormalizedString(); break; From 1d0ecd3188adbe2e7b3eb0b74a9d786b3e1b86dc Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Fri, 6 Oct 2023 16:18:33 -0400 Subject: [PATCH 656/858] More miscellaneous cleanup --- .../Parsers/BaseItemXmlParser.cs | 14 +- .../Parsers/BaseNfoParser.cs | 436 +++++++----------- 2 files changed, 165 insertions(+), 285 deletions(-) diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs index 0181c864d5..f980c6c4aa 100644 --- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs @@ -253,17 +253,8 @@ namespace MediaBrowser.LocalMetadata.Parsers break; case "LockData": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - item.IsLocked = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); - } - + item.IsLocked = string.Equals(reader.ReadElementContentAsString(), "true", StringComparison.OrdinalIgnoreCase); break; - } - case "Network": foreach (var name in reader.GetStringArray()) { @@ -857,11 +848,8 @@ namespace MediaBrowser.LocalMetadata.Parsers item.UserId = reader.ReadNormalizedString(); break; case "CanEdit": - { item.CanEdit = string.Equals(reader.ReadElementContentAsString(), "true", StringComparison.OrdinalIgnoreCase); break; - } - default: reader.Skip(); break; diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs index eb7c021426..22210d8697 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs @@ -262,9 +262,8 @@ namespace MediaBrowser.XbmcMetadata.Parsers protected virtual void FetchDataFromXmlNode(XmlReader reader, MetadataResult<T> itemResult) { var item = itemResult.Item; - var nfoConfiguration = _config.GetNfoConfiguration(); - UserItemData? userData = null; + UserItemData? userData; switch (reader.Name) { @@ -287,17 +286,13 @@ namespace MediaBrowser.XbmcMetadata.Parsers item.SortName = reader.ReadNormalizedString(); break; case "criticrating": + var criticRatingText = reader.ReadElementContentAsString(); + if (float.TryParse(criticRatingText, CultureInfo.InvariantCulture, out var value)) { - var text = reader.ReadElementContentAsString(); - - if (float.TryParse(text, CultureInfo.InvariantCulture, out var value)) - { - item.CriticRating = value; - } - - break; + item.CriticRating = value; } + break; case "sorttitle": item.ForcedSortName = reader.ReadNormalizedString(); break; @@ -310,20 +305,16 @@ namespace MediaBrowser.XbmcMetadata.Parsers item.PreferredMetadataLanguage = reader.ReadNormalizedString(); break; case "watched": + var played = reader.ReadElementContentAsBoolean(); + if (!string.IsNullOrWhiteSpace(nfoConfiguration.UserId)) { - var val = reader.ReadElementContentAsBoolean(); - - if (!string.IsNullOrWhiteSpace(nfoConfiguration.UserId)) - { - var user = _userManager.GetUserById(Guid.Parse(nfoConfiguration.UserId)); - userData = _userDataManager.GetUserData(user, item); - userData.Played = val; - _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None); - } - - break; + var user = _userManager.GetUserById(Guid.Parse(nfoConfiguration.UserId)); + userData = _userDataManager.GetUserData(user, item); + userData.Played = played; + _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None); } + break; case "playcount": if (reader.TryReadInt(out var count) && Guid.TryParse(nfoConfiguration.UserId, out var playCountUserId)) @@ -410,17 +401,8 @@ namespace MediaBrowser.XbmcMetadata.Parsers break; case "lockdata": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - item.IsLocked = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); - } - - break; - } - + item.IsLocked = string.Equals(reader.ReadElementContentAsString(), "true", StringComparison.OrdinalIgnoreCase); + break; case "studio": var studio = reader.ReadNormalizedString(); if (!string.IsNullOrEmpty(studio)) @@ -501,33 +483,17 @@ namespace MediaBrowser.XbmcMetadata.Parsers break; case "rating": + var rating = reader.ReadElementContentAsString().Replace(',', '.'); + // All external meta is saving this as '.' for decimal I believe...but just to be sure + if (float.TryParse(rating, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var communityRating)) { - var rating = reader.ReadElementContentAsString(); - - // All external meta is saving this as '.' for decimal I believe...but just to be sure - if (float.TryParse(rating.Replace(',', '.'), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var val)) - { - item.CommunityRating = val; - } - - break; + item.CommunityRating = communityRating; } + break; case "ratings": - { - if (!reader.IsEmptyElement) - { - using var subtree = reader.ReadSubtree(); - FetchFromRatingsNode(subtree, item); - } - else - { - reader.Read(); - } - - break; - } - + FetchFromRatingsNode(reader, item); + break; case "aired": case "formed": case "premiered": @@ -575,46 +541,26 @@ namespace MediaBrowser.XbmcMetadata.Parsers break; case "fileinfo": - { - if (!reader.IsEmptyElement) - { - using (var subtree = reader.ReadSubtree()) - { - FetchFromFileInfoNode(subtree, item); - } - } - else - { - reader.Read(); - } - - break; - } - + FetchFromFileInfoNode(reader, item); + break; case "uniqueid": + if (reader.IsEmptyElement) { - if (reader.IsEmptyElement) - { - reader.Read(); - break; - } - - var provider = reader.GetAttribute("type"); - var id = reader.ReadElementContentAsString(); - if (!string.IsNullOrWhiteSpace(provider) && !string.IsNullOrWhiteSpace(id)) - { - item.SetProviderId(provider, id); - } - + reader.Read(); break; } + var provider = reader.GetAttribute("type"); + var providerId = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(provider) && !string.IsNullOrWhiteSpace(providerId)) + { + item.SetProviderId(provider, providerId); + } + + break; case "thumb": - { - FetchThumbNode(reader, itemResult, "thumb"); - break; - } - + FetchThumbNode(reader, itemResult, "thumb"); + break; case "fanart": { if (reader.IsEmptyElement) @@ -719,242 +665,188 @@ namespace MediaBrowser.XbmcMetadata.Parsers } } - private void FetchFromFileInfoNode(XmlReader reader, T item) + private void FetchFromFileInfoNode(XmlReader parentReader, T item) { + if (parentReader.IsEmptyElement) + { + parentReader.Read(); + return; + } + + using var reader = parentReader.ReadSubtree(); reader.MoveToContent(); reader.Read(); // Loop through each element while (!reader.EOF && reader.ReadState == ReadState.Interactive) { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "streamdetails": - { - if (reader.IsEmptyElement) - { - reader.Read(); - continue; - } - - using (var subtree = reader.ReadSubtree()) - { - FetchFromStreamDetailsNode(subtree, item); - } - - break; - } - - default: - reader.Skip(); - break; - } - } - else + if (reader.NodeType != XmlNodeType.Element) { reader.Read(); + continue; + } + + switch (reader.Name) + { + case "streamdetails": + FetchFromStreamDetailsNode(reader, item); + break; + default: + reader.Skip(); + break; } } } - private void FetchFromStreamDetailsNode(XmlReader reader, T item) + private void FetchFromStreamDetailsNode(XmlReader parentReader, T item) { + if (parentReader.IsEmptyElement) + { + parentReader.Read(); + return; + } + + using var reader = parentReader.ReadSubtree(); reader.MoveToContent(); reader.Read(); // Loop through each element while (!reader.EOF && reader.ReadState == ReadState.Interactive) { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "video": - { - if (reader.IsEmptyElement) - { - reader.Read(); - continue; - } - - using (var subtree = reader.ReadSubtree()) - { - FetchFromVideoNode(subtree, item); - } - - break; - } - - case "subtitle": - { - if (reader.IsEmptyElement) - { - reader.Read(); - continue; - } - - using (var subtree = reader.ReadSubtree()) - { - FetchFromSubtitleNode(subtree, item); - } - - break; - } - - default: - reader.Skip(); - break; - } - } - else + if (reader.NodeType != XmlNodeType.Element) { reader.Read(); + continue; + } + + switch (reader.Name) + { + case "video": + FetchFromVideoNode(reader, item); + break; + case "subtitle": + FetchFromSubtitleNode(reader, item); + break; + default: + reader.Skip(); + break; } } } - private void FetchFromVideoNode(XmlReader reader, T item) + private void FetchFromVideoNode(XmlReader parentReader, T item) { + if (parentReader.IsEmptyElement) + { + parentReader.Read(); + return; + } + + using var reader = parentReader.ReadSubtree(); reader.MoveToContent(); reader.Read(); // Loop through each element while (!reader.EOF && reader.ReadState == ReadState.Interactive) { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "format3d": - { - var val = reader.ReadElementContentAsString(); - - var video = item as Video; - - if (video is not null) - { - if (string.Equals("HSBS", val, StringComparison.OrdinalIgnoreCase)) - { - video.Video3DFormat = Video3DFormat.HalfSideBySide; - } - else if (string.Equals("HTAB", val, StringComparison.OrdinalIgnoreCase)) - { - video.Video3DFormat = Video3DFormat.HalfTopAndBottom; - } - else if (string.Equals("FTAB", val, StringComparison.OrdinalIgnoreCase)) - { - video.Video3DFormat = Video3DFormat.FullTopAndBottom; - } - else if (string.Equals("FSBS", val, StringComparison.OrdinalIgnoreCase)) - { - video.Video3DFormat = Video3DFormat.FullSideBySide; - } - else if (string.Equals("MVC", val, StringComparison.OrdinalIgnoreCase)) - { - video.Video3DFormat = Video3DFormat.MVC; - } - } - - break; - } - - case "aspect": - { - var val = reader.ReadElementContentAsString(); - - if (item is Video video) - { - video.AspectRatio = val; - } - - break; - } - - case "width": - { - var val = reader.ReadElementContentAsInt(); - - if (item is Video video) - { - video.Width = val; - } - - break; - } - - case "height": - { - var val = reader.ReadElementContentAsInt(); - - if (item is Video video) - { - video.Height = val; - } - - break; - } - - case "durationinseconds": - { - var val = reader.ReadElementContentAsInt(); - - if (item is Video video) - { - video.RunTimeTicks = new TimeSpan(0, 0, val).Ticks; - } - - break; - } - - default: - reader.Skip(); - break; - } - } - else + if (reader.NodeType != XmlNodeType.Element || item is not Video video) { reader.Read(); + continue; + } + + switch (reader.Name) + { + case "format3d": + var format = reader.ReadElementContentAsString(); + if (string.Equals("HSBS", format, StringComparison.OrdinalIgnoreCase)) + { + video.Video3DFormat = Video3DFormat.HalfSideBySide; + } + else if (string.Equals("HTAB", format, StringComparison.OrdinalIgnoreCase)) + { + video.Video3DFormat = Video3DFormat.HalfTopAndBottom; + } + else if (string.Equals("FTAB", format, StringComparison.OrdinalIgnoreCase)) + { + video.Video3DFormat = Video3DFormat.FullTopAndBottom; + } + else if (string.Equals("FSBS", format, StringComparison.OrdinalIgnoreCase)) + { + video.Video3DFormat = Video3DFormat.FullSideBySide; + } + else if (string.Equals("MVC", format, StringComparison.OrdinalIgnoreCase)) + { + video.Video3DFormat = Video3DFormat.MVC; + } + + break; + case "aspect": + video.AspectRatio = reader.ReadNormalizedString(); + break; + case "width": + video.Width = reader.ReadElementContentAsInt(); + break; + case "height": + video.Height = reader.ReadElementContentAsInt(); + break; + case "durationinseconds": + video.RunTimeTicks = new TimeSpan(0, 0, reader.ReadElementContentAsInt()).Ticks; + break; + default: + reader.Skip(); + break; } } } - private void FetchFromSubtitleNode(XmlReader reader, T item) + private void FetchFromSubtitleNode(XmlReader parentReader, T item) { + if (parentReader.IsEmptyElement) + { + parentReader.Read(); + return; + } + + using var reader = parentReader.ReadSubtree(); reader.MoveToContent(); reader.Read(); // Loop through each element while (!reader.EOF && reader.ReadState == ReadState.Interactive) { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "language": - _ = reader.ReadElementContentAsString(); - if (item is Video video) - { - video.HasSubtitles = true; - } - - break; - - default: - reader.Skip(); - break; - } - } - else + if (reader.NodeType != XmlNodeType.Element) { reader.Read(); + continue; + } + + switch (reader.Name) + { + case "language": + _ = reader.ReadElementContentAsString(); + if (item is Video video) + { + video.HasSubtitles = true; + } + + break; + default: + reader.Skip(); + break; } } } - private void FetchFromRatingsNode(XmlReader reader, T item) + private void FetchFromRatingsNode(XmlReader parentReader, T item) { + if (parentReader.IsEmptyElement) + { + parentReader.Read(); + return; + } + + using var reader = parentReader.ReadSubtree(); reader.MoveToContent(); reader.Read(); From 1dd6442e89ee93127b475e820cca64c804f178ea Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Fri, 6 Oct 2023 16:43:50 -0400 Subject: [PATCH 657/858] Use extension methods in GetPersonFromXmlNode --- .../Extensions/XmlReaderExtensions.cs | 24 ++++--------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs b/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs index 1b3fa9d239..6be760b2fa 100644 --- a/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs +++ b/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs @@ -122,16 +122,11 @@ public static class XmlReaderExtensions { case "name": case "Name": - name = subtree.ReadElementContentAsString(); + name = subtree.ReadNormalizedString(); break; case "role": case "Role": - var roleValue = subtree.ReadElementContentAsString(); - if (!string.IsNullOrWhiteSpace(roleValue)) - { - role = roleValue; - } - + role = subtree.ReadNormalizedString(); break; case "type": case "Type": @@ -140,23 +135,14 @@ public static class XmlReaderExtensions case "order": case "sortorder": case "SortOrder": - if (int.TryParse( - subtree.ReadElementContentAsString(), - NumberStyles.Integer, - CultureInfo.InvariantCulture, - out var intVal)) + if (subtree.TryReadInt(out var sortOrderVal)) { - sortOrder = intVal; + sortOrder = sortOrderVal; } break; case "thumb": - var thumb = subtree.ReadElementContentAsString(); - if (!string.IsNullOrWhiteSpace(thumb)) - { - imageUrl = thumb; - } - + imageUrl = subtree.ReadNormalizedString(); break; default: subtree.Skip(); From 40e1c5f4c6901469c1c7f3f763f83d1b38e5e979 Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Fri, 6 Oct 2023 16:56:50 -0400 Subject: [PATCH 658/858] Remove logger parameter from XmlReaderExtensions.TryReadDateTime --- .../Extensions/XmlReaderExtensions.cs | 22 +++++-------------- .../Parsers/BaseItemXmlParser.cs | 2 +- .../Parsers/BaseNfoParser.cs | 4 ++-- 3 files changed, 9 insertions(+), 19 deletions(-) diff --git a/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs b/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs index 6be760b2fa..aa097714a5 100644 --- a/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs +++ b/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Xml; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; -using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Extensions; @@ -43,26 +42,17 @@ public static class XmlReaderExtensions /// Parses a <see cref="DateTime"/> from the current node. /// </summary> /// <param name="reader">The <see cref="XmlReader"/>.</param> - /// <param name="logger">The <see cref="ILogger"/> to use on failure.</param> /// <param name="value">The parsed <see cref="DateTime"/>.</param> /// <returns>A value indicating whether the parsing succeeded.</returns> - public static bool TryReadDateTime(this XmlReader reader, ILogger logger, out DateTime value) + public static bool TryReadDateTime(this XmlReader reader, out DateTime value) { ArgumentNullException.ThrowIfNull(reader); - ArgumentNullException.ThrowIfNull(logger); - var text = reader.ReadElementContentAsString(); - if (DateTime.TryParse( - text, - CultureInfo.InvariantCulture, - DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, - out value)) - { - return true; - } - - logger.LogWarning("Invalid date: {Date}", text); - return false; + return DateTime.TryParse( + reader.ReadElementContentAsString(), + CultureInfo.InvariantCulture, + DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, + out value); } /// <summary> diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs index f980c6c4aa..8a870e0d9b 100644 --- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs @@ -130,7 +130,7 @@ namespace MediaBrowser.LocalMetadata.Parsers switch (reader.Name) { case "Added": - if (reader.TryReadDateTime(Logger, out var dateCreated)) + if (reader.TryReadDateTime(out var dateCreated)) { item.DateCreated = dateCreated; } diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs index 22210d8697..47a127950d 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs @@ -268,7 +268,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers switch (reader.Name) { case "dateadded": - if (reader.TryReadDateTime(Logger, out var dateCreated)) + if (reader.TryReadDateTime(out var dateCreated)) { item.DateCreated = dateCreated; } @@ -327,7 +327,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers break; case "lastplayed": - if (reader.TryReadDateTime(Logger, out var lastPlayed) + if (reader.TryReadDateTime(out var lastPlayed) && Guid.TryParse(nfoConfiguration.UserId, out var lastPlayedUserId)) { var user = _userManager.GetUserById(lastPlayedUserId); From c38fbece0328e26d9d1c7a6772cba87081c122eb Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Fri, 6 Oct 2023 16:57:36 -0400 Subject: [PATCH 659/858] Remove unnecessary Trim() from GetPersonFromXmlNode --- MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs b/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs index aa097714a5..2742f21e36 100644 --- a/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs +++ b/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs @@ -147,7 +147,7 @@ public static class XmlReaderExtensions return new PersonInfo { - Name = name.Trim(), + Name = name, Role = role, Type = type, SortOrder = sortOrder, From 56aa37a314df17ba7608d7d9d00925f9e4816a89 Mon Sep 17 00:00:00 2001 From: herby2212 <12448284+herby2212@users.noreply.github.com> Date: Sat, 7 Oct 2023 20:52:16 +0200 Subject: [PATCH 660/858] Switch to named placeholders --- Emby.Server.Implementations/Session/SessionManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 14a62626c5..a6155aa621 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -667,7 +667,7 @@ namespace Emby.Server.Implementations.Session foreach (var session in inactiveSessions) { - _logger.LogDebug("Session {0} has been inactive for {1} minutes. Stopping it.", session.Id, _config.Configuration.InactiveSessionThreshold); + _logger.LogDebug("Session {Session} has been inactive for {InactiveTime} minutes. Stopping it.", session.Id, _config.Configuration.InactiveSessionThreshold); try { @@ -684,7 +684,7 @@ namespace Emby.Server.Implementations.Session } catch (Exception ex) { - _logger.LogDebug(ex, "Error calling SendPlaystateCommand for stopping inactive session {0}.", session.Id); + _logger.LogDebug(ex, "Error calling SendPlaystateCommand for stopping inactive session {Session}.", session.Id); } } } From a3d3ec7e0b5bf3faed0a657196efe7012af5ae38 Mon Sep 17 00:00:00 2001 From: Stepan Goremykin <goremukin@gmail.com> Date: Sat, 7 Oct 2023 23:41:45 +0200 Subject: [PATCH 661/858] Remove redundant SuppressFinalize call --- Emby.Server.Implementations/IO/FileRefresher.cs | 1 - .../LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs | 2 -- Emby.Server.Implementations/Udp/UdpServer.cs | 2 -- 3 files changed, 5 deletions(-) diff --git a/Emby.Server.Implementations/IO/FileRefresher.cs b/Emby.Server.Implementations/IO/FileRefresher.cs index 15b1836eba..e75cab64c9 100644 --- a/Emby.Server.Implementations/IO/FileRefresher.cs +++ b/Emby.Server.Implementations/IO/FileRefresher.cs @@ -210,7 +210,6 @@ namespace Emby.Server.Implementations.IO DisposeTimer(); _disposed = true; - GC.SuppressFinalize(this); } } } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs index a8b090635e..68383a5547 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs @@ -44,8 +44,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun StopStreaming(socket).GetAwaiter().GetResult(); } } - - GC.SuppressFinalize(this); } public async Task<bool> CheckTunerAvailability(IPAddress remoteIP, int tuner, CancellationToken cancellationToken) diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs index a3bbd6df0c..832793d29e 100644 --- a/Emby.Server.Implementations/Udp/UdpServer.cs +++ b/Emby.Server.Implementations/Udp/UdpServer.cs @@ -126,8 +126,6 @@ namespace Emby.Server.Implementations.Udp } _udpSocket?.Dispose(); - - GC.SuppressFinalize(this); } } } From 8925390ad4c9215755396a531263b654bed4d208 Mon Sep 17 00:00:00 2001 From: Stepan Goremykin <goremukin@gmail.com> Date: Sat, 7 Oct 2023 23:42:15 +0200 Subject: [PATCH 662/858] Remove redundant cast --- Emby.Server.Implementations/Net/SocketFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Net/SocketFactory.cs b/Emby.Server.Implementations/Net/SocketFactory.cs index 51e92953df..3091398b7b 100644 --- a/Emby.Server.Implementations/Net/SocketFactory.cs +++ b/Emby.Server.Implementations/Net/SocketFactory.cs @@ -83,7 +83,7 @@ namespace Emby.Server.Implementations.Net try { var interfaceIndex = bindInterface.Index; - var interfaceIndexSwapped = (int)IPAddress.HostToNetworkOrder(interfaceIndex); + var interfaceIndexSwapped = IPAddress.HostToNetworkOrder(interfaceIndex); socket.MulticastLoopback = false; socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); From 73309f2649778e50709388e5208404c2dc71b30a Mon Sep 17 00:00:00 2001 From: Stepan Goremykin <goremukin@gmail.com> Date: Sat, 7 Oct 2023 23:50:45 +0200 Subject: [PATCH 663/858] Pass cancellation token --- Emby.Server.Implementations/Channels/ChannelManager.cs | 2 +- .../ScheduledTasks/Tasks/ChapterImagesTask.cs | 5 +++-- Emby.Server.Implementations/Updates/InstallationManager.cs | 7 ++++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs index 961e225e9e..9e7b453867 100644 --- a/Emby.Server.Implementations/Channels/ChannelManager.cs +++ b/Emby.Server.Implementations/Channels/ChannelManager.cs @@ -1156,7 +1156,7 @@ namespace Emby.Server.Implementations.Channels if (info.People is not null && info.People.Count > 0) { - _libraryManager.UpdatePeople(item, info.People); + await _libraryManager.UpdatePeopleAsync(item, info.People, cancellationToken).ConfigureAwait(false); } } else if (forceUpdate) diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs index 6ad6c4cbd6..c4658c32ba 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs @@ -115,7 +115,8 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks { try { - previouslyFailedImages = File.ReadAllText(failHistoryPath) + var failHistoryText = await File.ReadAllTextAsync(failHistoryPath, cancellationToken).ConfigureAwait(false); + previouslyFailedImages = failHistoryText .Split('|', StringSplitOptions.RemoveEmptyEntries) .ToList(); } @@ -156,7 +157,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks } string text = string.Join('|', previouslyFailedImages); - File.WriteAllText(failHistoryPath, text); + await File.WriteAllTextAsync(failHistoryPath, text, cancellationToken).ConfigureAwait(false); } numComplete++; diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index 6c198b6f99..38c3fb4fa2 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -524,14 +524,15 @@ namespace Emby.Server.Implementations.Updates using var md5 = MD5.Create(); cancellationToken.ThrowIfCancellationRequested(); - var hash = Convert.ToHexString(md5.ComputeHash(stream)); - if (!string.Equals(package.Checksum, hash, StringComparison.OrdinalIgnoreCase)) + var hash = await md5.ComputeHashAsync(stream, cancellationToken).ConfigureAwait(false); + var hashHex = Convert.ToHexString(hash); + if (!string.Equals(package.Checksum, hashHex, StringComparison.OrdinalIgnoreCase)) { _logger.LogError( "The checksums didn't match while installing {Package}, expected: {Expected}, got: {Received}", package.Name, package.Checksum, - hash); + hashHex); throw new InvalidDataException("The checksum of the received data doesn't match."); } From 72faeed1db0220b303c99e88c0a980f991ab2415 Mon Sep 17 00:00:00 2001 From: Stepan Goremykin <goremukin@gmail.com> Date: Sat, 7 Oct 2023 23:52:13 +0200 Subject: [PATCH 664/858] Remove redundant semicolon --- RSSDP/SsdpCommunicationsServer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs index 0dce6c3bfa..001d2a55ec 100644 --- a/RSSDP/SsdpCommunicationsServer.cs +++ b/RSSDP/SsdpCommunicationsServer.cs @@ -423,7 +423,7 @@ namespace Rssdp.Infrastructure { try { - var result = await socket.ReceiveMessageFromAsync(receiveBuffer, SocketFlags.None, new IPEndPoint(IPAddress.Any, 0), CancellationToken.None).ConfigureAwait(false);; + var result = await socket.ReceiveMessageFromAsync(receiveBuffer, SocketFlags.None, new IPEndPoint(IPAddress.Any, 0), CancellationToken.None).ConfigureAwait(false); if (result.ReceivedBytes > 0) { From d6b557d9ee0f9b133f66cc3caf67b3068a492f44 Mon Sep 17 00:00:00 2001 From: Stepan Goremykin <goremukin@gmail.com> Date: Sat, 7 Oct 2023 23:56:07 +0200 Subject: [PATCH 665/858] Move declaration closer to usage --- MediaBrowser.Providers/MediaInfo/AudioFileProber.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index 44f998742f..59b7c6e279 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -107,7 +107,6 @@ namespace MediaBrowser.Providers.MediaInfo if (libraryOptions.EnableLUFSScan) { - string output; using (var process = new Process() { StartInfo = new ProcessStartInfo @@ -131,7 +130,7 @@ namespace MediaBrowser.Providers.MediaInfo } using var reader = process.StandardError; - output = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false); + var output = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); MatchCollection split = LUFSRegex().Matches(output); From 508c48a8ba0431160374a8ed8c60eef5b132b8c7 Mon Sep 17 00:00:00 2001 From: Stepan Goremykin <goremukin@gmail.com> Date: Sat, 7 Oct 2023 23:58:48 +0200 Subject: [PATCH 666/858] Inline out variables --- RSSDP/SsdpDeviceLocator.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/RSSDP/SsdpDeviceLocator.cs b/RSSDP/SsdpDeviceLocator.cs index 59f4c5070b..1afb86de40 100644 --- a/RSSDP/SsdpDeviceLocator.cs +++ b/RSSDP/SsdpDeviceLocator.cs @@ -447,10 +447,9 @@ namespace Rssdp.Infrastructure private string GetFirstHeaderStringValue(string headerName, HttpResponseMessage message) { string retVal = null; - IEnumerable<string> values; if (message.Headers.Contains(headerName)) { - message.Headers.TryGetValues(headerName, out values); + message.Headers.TryGetValues(headerName, out var values); if (values is not null) { retVal = values.FirstOrDefault(); @@ -463,10 +462,9 @@ namespace Rssdp.Infrastructure private string GetFirstHeaderStringValue(string headerName, HttpRequestMessage message) { string retVal = null; - IEnumerable<string> values; if (message.Headers.Contains(headerName)) { - message.Headers.TryGetValues(headerName, out values); + message.Headers.TryGetValues(headerName, out var values); if (values is not null) { retVal = values.FirstOrDefault(); @@ -479,10 +477,9 @@ namespace Rssdp.Infrastructure private Uri GetFirstHeaderUriValue(string headerName, HttpRequestMessage request) { string value = null; - IEnumerable<string> values; if (request.Headers.Contains(headerName)) { - request.Headers.TryGetValues(headerName, out values); + request.Headers.TryGetValues(headerName, out var values); if (values is not null) { value = values.FirstOrDefault(); @@ -496,10 +493,9 @@ namespace Rssdp.Infrastructure private Uri GetFirstHeaderUriValue(string headerName, HttpResponseMessage response) { string value = null; - IEnumerable<string> values; if (response.Headers.Contains(headerName)) { - response.Headers.TryGetValues(headerName, out values); + response.Headers.TryGetValues(headerName, out var values); if (values is not null) { value = values.FirstOrDefault(); From 0870af330df1040eddf76ae2b7cde3e97061b745 Mon Sep 17 00:00:00 2001 From: Stepan Goremykin <goremukin@gmail.com> Date: Sun, 8 Oct 2023 00:15:38 +0200 Subject: [PATCH 667/858] Remove redundant verbatim string prefixes --- Emby.Naming/Common/NamingOptions.cs | 8 +- .../Encoder/MediaEncoder.cs | 2 +- .../Parsers/BaseNfoParser.cs | 2 +- .../AudioBook/AudioBookResolverTests.cs | 8 +- .../ExternalFiles/ExternalPathParserTests.cs | 6 +- .../Music/MultiDiscAlbumTests.cs | 56 +++--- .../TV/DailyEpisodeTests.cs | 10 +- .../TV/EpisodeNumberWithoutSeasonTests.cs | 20 +- .../TV/MultiEpisodeTests.cs | 114 ++++++------ .../TV/SeasonFolderTests.cs | 34 ++-- .../TV/SeasonNumberTests.cs | 4 +- .../TV/SimpleEpisodeTests.cs | 4 +- .../Video/CleanDateTimeTests.cs | 60 +++--- .../Video/Format3DTests.cs | 2 +- .../Video/MultiVersionTests.cs | 172 +++++++++--------- .../Jellyfin.Naming.Tests/Video/StackTests.cs | 4 +- .../Jellyfin.Naming.Tests/Video/StubTests.cs | 2 +- .../Video/VideoListResolverTests.cs | 56 +++--- .../Video/VideoResolverTests.cs | 36 ++-- .../Manager/ItemImageProviderTests.cs | 4 +- .../MediaInfo/MediaInfoResolverTests.cs | 8 +- 21 files changed, 306 insertions(+), 306 deletions(-) diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index 2bd089ed8f..692edae4a9 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -318,7 +318,7 @@ namespace Emby.Naming.Common new EpisodeExpression(@"[\._ -]()[Ee][Pp]_?([0-9]+)([^\\/]*)$"), // <!-- foo.E01., foo.e01. --> new EpisodeExpression(@"[^\\/]*?()\.?[Ee]([0-9]+)\.([^\\/]*)$"), - new EpisodeExpression(@"(?<year>[0-9]{4})[._ -](?<month>[0-9]{2})[._ -](?<day>[0-9]{2})", true) + new EpisodeExpression("(?<year>[0-9]{4})[._ -](?<month>[0-9]{2})[._ -](?<day>[0-9]{2})", true) { DateTimeFormats = new[] { @@ -328,7 +328,7 @@ namespace Emby.Naming.Common "yyyy MM dd" } }, - new EpisodeExpression(@"(?<day>[0-9]{2})[._ -](?<month>[0-9]{2})[._ -](?<year>[0-9]{4})", true) + new EpisodeExpression("(?<day>[0-9]{2})[._ -](?<month>[0-9]{2})[._ -](?<year>[0-9]{4})", true) { DateTimeFormats = new[] { @@ -417,7 +417,7 @@ namespace Emby.Naming.Common }, // "1-12 episode title" - new EpisodeExpression(@"([0-9]+)-([0-9]+)"), + new EpisodeExpression("([0-9]+)-([0-9]+)"), // "01 - blah.avi", "01-blah.avi" new EpisodeExpression(@".*(\\|\/)(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*\s?-\s?[^\\\/]*$") @@ -712,7 +712,7 @@ namespace Emby.Naming.Common // Chapter is often beginning of filename "^(?<chapter>[0-9]+)", // Part if often ending of filename - @"(?<!ch(?:apter) )(?<part>[0-9]+)$", + "(?<!ch(?:apter) )(?<part>[0-9]+)$", // Sometimes named as 0001_005 (chapter_part) "(?<chapter>[0-9]+)_(?<part>[0-9]+)", // Some audiobooks are ripped from cd's, and will be named by disk number. diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 90ef937203..9bd99f030c 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -177,7 +177,7 @@ namespace MediaBrowser.MediaEncoding.Encoder if (_ffmpegPath is not null) { // Determine a probe path from the mpeg path - _ffprobePath = FfprobePathRegex().Replace(_ffmpegPath, @"ffprobe$1"); + _ffprobePath = FfprobePathRegex().Replace(_ffmpegPath, "ffprobe$1"); // Interrogate to understand what coders are supported var validator = new EncoderValidator(_logger, _ffmpegPath); diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs index 5b68924acb..af01b8be25 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs @@ -159,7 +159,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers // Find last closing Tag // Need to do this in two steps to account for random > characters after the closing xml - var index = xml.LastIndexOf(@"</", StringComparison.Ordinal); + var index = xml.LastIndexOf("</", StringComparison.Ordinal); // If closing tag exists, move to end of Tag if (index != -1) diff --git a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs index c72a3315e7..9b9c1ec347 100644 --- a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs +++ b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs @@ -14,18 +14,18 @@ namespace Jellyfin.Naming.Tests.AudioBook data.Add( new AudioBookFileInfo( - @"/server/AudioBooks/Larry Potter/Larry Potter.mp3", + "/server/AudioBooks/Larry Potter/Larry Potter.mp3", "mp3")); data.Add( new AudioBookFileInfo( - @"/server/AudioBooks/Berry Potter/Chapter 1 .ogg", + "/server/AudioBooks/Berry Potter/Chapter 1 .ogg", "ogg", chapterNumber: 1)); data.Add( new AudioBookFileInfo( - @"/server/AudioBooks/Nerry Potter/Part 3 - Chapter 2.mp3", + "/server/AudioBooks/Nerry Potter/Part 3 - Chapter 2.mp3", "mp3", chapterNumber: 2, partNumber: 3)); @@ -49,7 +49,7 @@ namespace Jellyfin.Naming.Tests.AudioBook [Fact] public void Resolve_InvalidExtension() { - var result = new AudioBookResolver(_namingOptions).Resolve(@"/server/AudioBooks/Larry Potter/Larry Potter.mp9"); + var result = new AudioBookResolver(_namingOptions).Resolve("/server/AudioBooks/Larry Potter/Larry Potter.mp9"); Assert.Null(result); } diff --git a/tests/Jellyfin.Naming.Tests/ExternalFiles/ExternalPathParserTests.cs b/tests/Jellyfin.Naming.Tests/ExternalFiles/ExternalPathParserTests.cs index 97949adffa..ba602b5d2e 100644 --- a/tests/Jellyfin.Naming.Tests/ExternalFiles/ExternalPathParserTests.cs +++ b/tests/Jellyfin.Naming.Tests/ExternalFiles/ExternalPathParserTests.cs @@ -20,11 +20,11 @@ public class ExternalPathParserTests var hindiCultureDto = new CultureDto("Hindi", "Hindi", "hi", new[] { "hin" }); var localizationManager = new Mock<ILocalizationManager>(MockBehavior.Loose); - localizationManager.Setup(lm => lm.FindLanguageInfo(It.IsRegex(@"en.*", RegexOptions.IgnoreCase))) + localizationManager.Setup(lm => lm.FindLanguageInfo(It.IsRegex("en.*", RegexOptions.IgnoreCase))) .Returns(englishCultureDto); - localizationManager.Setup(lm => lm.FindLanguageInfo(It.IsRegex(@"fr.*", RegexOptions.IgnoreCase))) + localizationManager.Setup(lm => lm.FindLanguageInfo(It.IsRegex("fr.*", RegexOptions.IgnoreCase))) .Returns(frenchCultureDto); - localizationManager.Setup(lm => lm.FindLanguageInfo(It.IsRegex(@"hi.*", RegexOptions.IgnoreCase))) + localizationManager.Setup(lm => lm.FindLanguageInfo(It.IsRegex("hi.*", RegexOptions.IgnoreCase))) .Returns(hindiCultureDto); _audioPathParser = new ExternalPathParser(new NamingOptions(), localizationManager.Object, DlnaProfileType.Audio); diff --git a/tests/Jellyfin.Naming.Tests/Music/MultiDiscAlbumTests.cs b/tests/Jellyfin.Naming.Tests/Music/MultiDiscAlbumTests.cs index c9a295a4ce..471616797f 100644 --- a/tests/Jellyfin.Naming.Tests/Music/MultiDiscAlbumTests.cs +++ b/tests/Jellyfin.Naming.Tests/Music/MultiDiscAlbumTests.cs @@ -12,34 +12,34 @@ namespace Jellyfin.Naming.Tests.Music [InlineData("", false)] [InlineData("C:/", false)] [InlineData("/home/", false)] - [InlineData(@"blah blah", false)] - [InlineData(@"D:/music/weezer/03 Pinkerton", false)] - [InlineData(@"D:/music/michael jackson/Bad (2012 Remaster)", false)] - [InlineData(@"cd1", true)] - [InlineData(@"disc18", true)] - [InlineData(@"disk10", true)] - [InlineData(@"vol7", true)] - [InlineData(@"volume1", true)] - [InlineData(@"cd 1", true)] - [InlineData(@"disc 1", true)] - [InlineData(@"disk 1", true)] - [InlineData(@"disk", false)] - [InlineData(@"disk ·", false)] - [InlineData(@"disk a", false)] - [InlineData(@"disk volume", false)] - [InlineData(@"disc disc", false)] - [InlineData(@"disk disc 6", false)] - [InlineData(@"cd - 1", true)] - [InlineData(@"disc- 1", true)] - [InlineData(@"disk - 1", true)] - [InlineData(@"Disc 01 (Hugo Wolf · 24 Lieder)", true)] - [InlineData(@"Disc 04 (Encores and Folk Songs)", true)] - [InlineData(@"Disc04 (Encores and Folk Songs)", true)] - [InlineData(@"Disc 04(Encores and Folk Songs)", true)] - [InlineData(@"Disc04(Encores and Folk Songs)", true)] - [InlineData(@"D:/Video/MBTestLibrary/VideoTest/music/.38 special/anth/Disc 2", true)] - [InlineData(@"[1985] Opportunities (Let's make lots of money) (1985)", false)] - [InlineData(@"Blah 04(Encores and Folk Songs)", false)] + [InlineData("blah blah", false)] + [InlineData("D:/music/weezer/03 Pinkerton", false)] + [InlineData("D:/music/michael jackson/Bad (2012 Remaster)", false)] + [InlineData("cd1", true)] + [InlineData("disc18", true)] + [InlineData("disk10", true)] + [InlineData("vol7", true)] + [InlineData("volume1", true)] + [InlineData("cd 1", true)] + [InlineData("disc 1", true)] + [InlineData("disk 1", true)] + [InlineData("disk", false)] + [InlineData("disk ·", false)] + [InlineData("disk a", false)] + [InlineData("disk volume", false)] + [InlineData("disc disc", false)] + [InlineData("disk disc 6", false)] + [InlineData("cd - 1", true)] + [InlineData("disc- 1", true)] + [InlineData("disk - 1", true)] + [InlineData("Disc 01 (Hugo Wolf · 24 Lieder)", true)] + [InlineData("Disc 04 (Encores and Folk Songs)", true)] + [InlineData("Disc04 (Encores and Folk Songs)", true)] + [InlineData("Disc 04(Encores and Folk Songs)", true)] + [InlineData("Disc04(Encores and Folk Songs)", true)] + [InlineData("D:/Video/MBTestLibrary/VideoTest/music/.38 special/anth/Disc 2", true)] + [InlineData("[1985] Opportunities (Let's make lots of money) (1985)", false)] + [InlineData("Blah 04(Encores and Folk Songs)", false)] public void AlbumParser_MultidiscPath_Identifies(string path, bool result) { var parser = new AlbumParser(_namingOptions); diff --git a/tests/Jellyfin.Naming.Tests/TV/DailyEpisodeTests.cs b/tests/Jellyfin.Naming.Tests/TV/DailyEpisodeTests.cs index d0d3d82928..f2cd360e53 100644 --- a/tests/Jellyfin.Naming.Tests/TV/DailyEpisodeTests.cs +++ b/tests/Jellyfin.Naming.Tests/TV/DailyEpisodeTests.cs @@ -9,11 +9,11 @@ namespace Jellyfin.Naming.Tests.TV private readonly EpisodeResolver _resolver = new EpisodeResolver(new NamingOptions()); [Theory] - [InlineData(@"/server/anything_1996.11.14.mp4", "anything", 1996, 11, 14)] - [InlineData(@"/server/anything_1996-11-14.mp4", "anything", 1996, 11, 14)] - [InlineData(@"/server/james.corden.2017.04.20.anne.hathaway.720p.hdtv.x264-crooks.mkv", "james.corden", 2017, 04, 20)] - [InlineData(@"/server/ABC News 2018_03_24_19_00_00.mkv", "ABC News", 2018, 03, 24)] - [InlineData(@"/server/Jeopardy 2023 07 14 HDTV x264 AC3.mkv", "Jeopardy", 2023, 07, 14)] + [InlineData("/server/anything_1996.11.14.mp4", "anything", 1996, 11, 14)] + [InlineData("/server/anything_1996-11-14.mp4", "anything", 1996, 11, 14)] + [InlineData("/server/james.corden.2017.04.20.anne.hathaway.720p.hdtv.x264-crooks.mkv", "james.corden", 2017, 04, 20)] + [InlineData("/server/ABC News 2018_03_24_19_00_00.mkv", "ABC News", 2018, 03, 24)] + [InlineData("/server/Jeopardy 2023 07 14 HDTV x264 AC3.mkv", "Jeopardy", 2023, 07, 14)] // TODO: [InlineData(@"/server/anything_14.11.1996.mp4", "anything", 1996, 11, 14)] // TODO: [InlineData(@"/server/A Daily Show - (2015-01-15) - Episode Name - [720p].mkv", "A Daily Show", 2015, 01, 15)] // TODO: [InlineData(@"/server/Last Man Standing_KTLADT_2018_05_25_01_28_00.wtv", "Last Man Standing", 2018, 05, 25)] diff --git a/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberWithoutSeasonTests.cs b/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberWithoutSeasonTests.cs index 1da5a30a81..1727b22476 100644 --- a/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberWithoutSeasonTests.cs +++ b/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberWithoutSeasonTests.cs @@ -9,16 +9,16 @@ namespace Jellyfin.Naming.Tests.TV private readonly EpisodeResolver _resolver = new EpisodeResolver(new NamingOptions()); [Theory] - [InlineData(8, @"The Simpsons/The Simpsons.S25E08.Steal this episode.mp4")] - [InlineData(2, @"The Simpsons/The Simpsons - 02 - Ep Name.avi")] - [InlineData(2, @"The Simpsons/02.avi")] - [InlineData(2, @"The Simpsons/02 - Ep Name.avi")] - [InlineData(2, @"The Simpsons/02-Ep Name.avi")] - [InlineData(2, @"The Simpsons/02.EpName.avi")] - [InlineData(2, @"The Simpsons/The Simpsons - 02.avi")] - [InlineData(2, @"The Simpsons/The Simpsons - 02 Ep Name.avi")] - [InlineData(7, @"GJ Club (2013)/GJ Club - 07.mkv")] - [InlineData(17, @"Case Closed (1996-2007)/Case Closed - 317.mkv")] + [InlineData(8, "The Simpsons/The Simpsons.S25E08.Steal this episode.mp4")] + [InlineData(2, "The Simpsons/The Simpsons - 02 - Ep Name.avi")] + [InlineData(2, "The Simpsons/02.avi")] + [InlineData(2, "The Simpsons/02 - Ep Name.avi")] + [InlineData(2, "The Simpsons/02-Ep Name.avi")] + [InlineData(2, "The Simpsons/02.EpName.avi")] + [InlineData(2, "The Simpsons/The Simpsons - 02.avi")] + [InlineData(2, "The Simpsons/The Simpsons - 02 Ep Name.avi")] + [InlineData(7, "GJ Club (2013)/GJ Club - 07.mkv")] + [InlineData(17, "Case Closed (1996-2007)/Case Closed - 317.mkv")] // TODO: [InlineData(2, @"The Simpsons/The Simpsons 5 - 02 - Ep Name.avi")] // TODO: [InlineData(2, @"The Simpsons/The Simpsons 5 - 02 Ep Name.avi")] // TODO: [InlineData(7, @"Seinfeld/Seinfeld 0807 The Checks.avi")] diff --git a/tests/Jellyfin.Naming.Tests/TV/MultiEpisodeTests.cs b/tests/Jellyfin.Naming.Tests/TV/MultiEpisodeTests.cs index ffaa64c3f1..6d6591abf8 100644 --- a/tests/Jellyfin.Naming.Tests/TV/MultiEpisodeTests.cs +++ b/tests/Jellyfin.Naming.Tests/TV/MultiEpisodeTests.cs @@ -9,66 +9,66 @@ namespace Jellyfin.Naming.Tests.TV private readonly EpisodePathParser _episodePathParser = new EpisodePathParser(new NamingOptions()); [Theory] - [InlineData(@"Season 1/4x01 – 20 Hours in America (1).mkv", null)] - [InlineData(@"Season 1/01x02 blah.avi", null)] - [InlineData(@"Season 1/S01x02 blah.avi", null)] - [InlineData(@"Season 1/S01E02 blah.avi", null)] - [InlineData(@"Season 1/S01xE02 blah.avi", null)] - [InlineData(@"Season 1/seriesname 01x02 blah.avi", null)] - [InlineData(@"Season 1/seriesname S01x02 blah.avi", null)] - [InlineData(@"Season 1/seriesname S01E02 blah.avi", null)] - [InlineData(@"Season 1/seriesname S01xE02 blah.avi", null)] - [InlineData(@"Season 2/02x03 - 04 Ep Name.mp4", null)] - [InlineData(@"Season 2/My show name 02x03 - 04 Ep Name.mp4", null)] - [InlineData(@"Season 2/Elementary - 02x03 - 02x04 - 02x15 - Ep Name.mp4", 15)] - [InlineData(@"Season 2/02x03 - 02x04 - 02x15 - Ep Name.mp4", 15)] - [InlineData(@"Season 2/02x03-04-15 - Ep Name.mp4", 15)] - [InlineData(@"Season 2/Elementary - 02x03-04-15 - Ep Name.mp4", 15)] - [InlineData(@"Season 02/02x03-E15 - Ep Name.mp4", 15)] - [InlineData(@"Season 02/Elementary - 02x03-E15 - Ep Name.mp4", 15)] - [InlineData(@"Season 02/02x03 - x04 - x15 - Ep Name.mp4", 15)] - [InlineData(@"Season 02/Elementary - 02x03 - x04 - x15 - Ep Name.mp4", 15)] - [InlineData(@"Season 02/02x03x04x15 - Ep Name.mp4", 15)] - [InlineData(@"Season 02/Elementary - 02x03x04x15 - Ep Name.mp4", 15)] - [InlineData(@"Season 1/Elementary - S01E23-E24-E26 - The Woman.mp4", 26)] - [InlineData(@"Season 1/S01E23-E24-E26 - The Woman.mp4", 26)] + [InlineData("Season 1/4x01 – 20 Hours in America (1).mkv", null)] + [InlineData("Season 1/01x02 blah.avi", null)] + [InlineData("Season 1/S01x02 blah.avi", null)] + [InlineData("Season 1/S01E02 blah.avi", null)] + [InlineData("Season 1/S01xE02 blah.avi", null)] + [InlineData("Season 1/seriesname 01x02 blah.avi", null)] + [InlineData("Season 1/seriesname S01x02 blah.avi", null)] + [InlineData("Season 1/seriesname S01E02 blah.avi", null)] + [InlineData("Season 1/seriesname S01xE02 blah.avi", null)] + [InlineData("Season 2/02x03 - 04 Ep Name.mp4", null)] + [InlineData("Season 2/My show name 02x03 - 04 Ep Name.mp4", null)] + [InlineData("Season 2/Elementary - 02x03 - 02x04 - 02x15 - Ep Name.mp4", 15)] + [InlineData("Season 2/02x03 - 02x04 - 02x15 - Ep Name.mp4", 15)] + [InlineData("Season 2/02x03-04-15 - Ep Name.mp4", 15)] + [InlineData("Season 2/Elementary - 02x03-04-15 - Ep Name.mp4", 15)] + [InlineData("Season 02/02x03-E15 - Ep Name.mp4", 15)] + [InlineData("Season 02/Elementary - 02x03-E15 - Ep Name.mp4", 15)] + [InlineData("Season 02/02x03 - x04 - x15 - Ep Name.mp4", 15)] + [InlineData("Season 02/Elementary - 02x03 - x04 - x15 - Ep Name.mp4", 15)] + [InlineData("Season 02/02x03x04x15 - Ep Name.mp4", 15)] + [InlineData("Season 02/Elementary - 02x03x04x15 - Ep Name.mp4", 15)] + [InlineData("Season 1/Elementary - S01E23-E24-E26 - The Woman.mp4", 26)] + [InlineData("Season 1/S01E23-E24-E26 - The Woman.mp4", 26)] // Four Digits seasons - [InlineData(@"Season 2009/2009x02 blah.avi", null)] - [InlineData(@"Season 2009/S2009x02 blah.avi", null)] - [InlineData(@"Season 2009/S2009E02 blah.avi", null)] - [InlineData(@"Season 2009/S2009xE02 blah.avi", null)] - [InlineData(@"Season 2009/seriesname 2009x02 blah.avi", null)] - [InlineData(@"Season 2009/seriesname S2009x02 blah.avi", null)] - [InlineData(@"Season 2009/seriesname S2009E02 blah.avi", null)] - [InlineData(@"Season 2009/seriesname S2009xE02 blah.avi", null)] - [InlineData(@"Season 2009/Elementary - 2009x03 - 2009x04 - 2009x15 - Ep Name.mp4", 15)] - [InlineData(@"Season 2009/2009x03 - 2009x04 - 2009x15 - Ep Name.mp4", 15)] - [InlineData(@"Season 2009/2009x03-04-15 - Ep Name.mp4", 15)] - [InlineData(@"Season 2009/Elementary - 2009x03-04-15 - Ep Name.mp4", 15)] - [InlineData(@"Season 2009/2009x03-E15 - Ep Name.mp4", 15)] - [InlineData(@"Season 2009/Elementary - 2009x03-E15 - Ep Name.mp4", 15)] - [InlineData(@"Season 2009/2009x03 - x04 - x15 - Ep Name.mp4", 15)] - [InlineData(@"Season 2009/Elementary - 2009x03 - x04 - x15 - Ep Name.mp4", 15)] - [InlineData(@"Season 2009/2009x03x04x15 - Ep Name.mp4", 15)] - [InlineData(@"Season 2009/Elementary - 2009x03x04x15 - Ep Name.mp4", 15)] - [InlineData(@"Season 2009/Elementary - S2009E23-E24-E26 - The Woman.mp4", 26)] - [InlineData(@"Season 2009/S2009E23-E24-E26 - The Woman.mp4", 26)] + [InlineData("Season 2009/2009x02 blah.avi", null)] + [InlineData("Season 2009/S2009x02 blah.avi", null)] + [InlineData("Season 2009/S2009E02 blah.avi", null)] + [InlineData("Season 2009/S2009xE02 blah.avi", null)] + [InlineData("Season 2009/seriesname 2009x02 blah.avi", null)] + [InlineData("Season 2009/seriesname S2009x02 blah.avi", null)] + [InlineData("Season 2009/seriesname S2009E02 blah.avi", null)] + [InlineData("Season 2009/seriesname S2009xE02 blah.avi", null)] + [InlineData("Season 2009/Elementary - 2009x03 - 2009x04 - 2009x15 - Ep Name.mp4", 15)] + [InlineData("Season 2009/2009x03 - 2009x04 - 2009x15 - Ep Name.mp4", 15)] + [InlineData("Season 2009/2009x03-04-15 - Ep Name.mp4", 15)] + [InlineData("Season 2009/Elementary - 2009x03-04-15 - Ep Name.mp4", 15)] + [InlineData("Season 2009/2009x03-E15 - Ep Name.mp4", 15)] + [InlineData("Season 2009/Elementary - 2009x03-E15 - Ep Name.mp4", 15)] + [InlineData("Season 2009/2009x03 - x04 - x15 - Ep Name.mp4", 15)] + [InlineData("Season 2009/Elementary - 2009x03 - x04 - x15 - Ep Name.mp4", 15)] + [InlineData("Season 2009/2009x03x04x15 - Ep Name.mp4", 15)] + [InlineData("Season 2009/Elementary - 2009x03x04x15 - Ep Name.mp4", 15)] + [InlineData("Season 2009/Elementary - S2009E23-E24-E26 - The Woman.mp4", 26)] + [InlineData("Season 2009/S2009E23-E24-E26 - The Woman.mp4", 26)] // Without season number - [InlineData(@"Season 1/02 - blah.avi", null)] - [InlineData(@"Season 2/02 - blah 14 blah.avi", null)] - [InlineData(@"Season 1/02 - blah-02 a.avi", null)] - [InlineData(@"Season 2/02.avi", null)] - [InlineData(@"Season 1/02-03 - blah.avi", 3)] - [InlineData(@"Season 2/02-04 - blah 14 blah.avi", 4)] - [InlineData(@"Season 1/02-05 - blah-02 a.avi", 5)] - [InlineData(@"Season 2/02-04.avi", 4)] - [InlineData(@"Season 2 /[HorribleSubs] Hunter X Hunter - 136[720p].mkv", null)] + [InlineData("Season 1/02 - blah.avi", null)] + [InlineData("Season 2/02 - blah 14 blah.avi", null)] + [InlineData("Season 1/02 - blah-02 a.avi", null)] + [InlineData("Season 2/02.avi", null)] + [InlineData("Season 1/02-03 - blah.avi", 3)] + [InlineData("Season 2/02-04 - blah 14 blah.avi", 4)] + [InlineData("Season 1/02-05 - blah-02 a.avi", 5)] + [InlineData("Season 2/02-04.avi", 4)] + [InlineData("Season 2 /[HorribleSubs] Hunter X Hunter - 136[720p].mkv", null)] // With format specification that must not be detected as ending episode number - [InlineData(@"Season 1/series-s09e14-1080p.mkv", null)] - [InlineData(@"Season 1/series-s09e14-720p.mkv", null)] - [InlineData(@"Season 1/series-s09e14-720i.mkv", null)] - [InlineData(@"Season 1/MOONLIGHTING_s01e01-e04.mkv", 4)] - [InlineData(@"Season 1/MOONLIGHTING_s01e01-e04", 4)] + [InlineData("Season 1/series-s09e14-1080p.mkv", null)] + [InlineData("Season 1/series-s09e14-720p.mkv", null)] + [InlineData("Season 1/series-s09e14-720i.mkv", null)] + [InlineData("Season 1/MOONLIGHTING_s01e01-e04.mkv", 4)] + [InlineData("Season 1/MOONLIGHTING_s01e01-e04", 4)] public void TestGetEndingEpisodeNumberFromFile(string filename, int? endingEpisodeNumber) { var result = _episodePathParser.Parse(filename, false); diff --git a/tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs b/tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs index 55af33836c..6773bbeb19 100644 --- a/tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs +++ b/tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs @@ -6,23 +6,23 @@ namespace Jellyfin.Naming.Tests.TV public class SeasonFolderTests { [Theory] - [InlineData(@"/Drive/Season 1", 1, true)] - [InlineData(@"/Drive/Season 2", 2, true)] - [InlineData(@"/Drive/Season 02", 2, true)] - [InlineData(@"/Drive/Seinfeld/S02", 2, true)] - [InlineData(@"/Drive/Seinfeld/2", 2, true)] - [InlineData(@"/Drive/Season 2009", 2009, true)] - [InlineData(@"/Drive/Season1", 1, true)] - [InlineData(@"The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH", 4, true)] - [InlineData(@"/Drive/Season 7 (2016)", 7, false)] - [InlineData(@"/Drive/Staffel 7 (2016)", 7, false)] - [InlineData(@"/Drive/Stagione 7 (2016)", 7, false)] - [InlineData(@"/Drive/Season (8)", null, false)] - [InlineData(@"/Drive/3.Staffel", 3, false)] - [InlineData(@"/Drive/s06e05", null, false)] - [InlineData(@"/Drive/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv", null, false)] - [InlineData(@"/Drive/extras", 0, true)] - [InlineData(@"/Drive/specials", 0, true)] + [InlineData("/Drive/Season 1", 1, true)] + [InlineData("/Drive/Season 2", 2, true)] + [InlineData("/Drive/Season 02", 2, true)] + [InlineData("/Drive/Seinfeld/S02", 2, true)] + [InlineData("/Drive/Seinfeld/2", 2, true)] + [InlineData("/Drive/Season 2009", 2009, true)] + [InlineData("/Drive/Season1", 1, true)] + [InlineData("The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH", 4, true)] + [InlineData("/Drive/Season 7 (2016)", 7, false)] + [InlineData("/Drive/Staffel 7 (2016)", 7, false)] + [InlineData("/Drive/Stagione 7 (2016)", 7, false)] + [InlineData("/Drive/Season (8)", null, false)] + [InlineData("/Drive/3.Staffel", 3, false)] + [InlineData("/Drive/s06e05", null, false)] + [InlineData("/Drive/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv", null, false)] + [InlineData("/Drive/extras", 0, true)] + [InlineData("/Drive/specials", 0, true)] public void GetSeasonNumberFromPathTest(string path, int? seasonNumber, bool isSeasonDirectory) { var result = SeasonPathParser.Parse(path, true, true); diff --git a/tests/Jellyfin.Naming.Tests/TV/SeasonNumberTests.cs b/tests/Jellyfin.Naming.Tests/TV/SeasonNumberTests.cs index 58ec1b5d28..94a953de3e 100644 --- a/tests/Jellyfin.Naming.Tests/TV/SeasonNumberTests.cs +++ b/tests/Jellyfin.Naming.Tests/TV/SeasonNumberTests.cs @@ -51,8 +51,8 @@ namespace Jellyfin.Naming.Tests.TV [InlineData("Season 2009/Elementary - S2009E23-E24-E26 - The Woman.mp4", 2009)] [InlineData("Season 2009/S2009E23-E24-E26 - The Woman.mp4", 2009)] [InlineData("Series/1-12 - The Woman.mp4", 1)] - [InlineData(@"Running Man/Running Man S2017E368.mkv", 2017)] - [InlineData(@"Case Closed (1996-2007)/Case Closed - 317.mkv", 3)] + [InlineData("Running Man/Running Man S2017E368.mkv", 2017)] + [InlineData("Case Closed (1996-2007)/Case Closed - 317.mkv", 3)] // TODO: [InlineData(@"Seinfeld/Seinfeld 0807 The Checks.avi", 8)] public void GetSeasonNumberFromEpisodeFileTest(string path, int? expected) { diff --git a/tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs b/tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs index fa46ecc3ae..3721cd28cf 100644 --- a/tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs +++ b/tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs @@ -21,8 +21,8 @@ namespace Jellyfin.Naming.Tests.TV [InlineData("Series/4x12 - The Woman.mp4", "", 4, 12)] [InlineData("Series/LA X, Pt. 1_s06e32.mp4", "LA X, Pt. 1", 6, 32)] [InlineData("[Baz-Bar]Foo - [1080p][Multiple Subtitle]/[Baz-Bar] Foo - 05 [1080p][Multiple Subtitle].mkv", "Foo", null, 5)] - [InlineData(@"/Foo/The.Series.Name.S01E04.WEBRip.x264-Baz[Bar]/the.series.name.s01e04.webrip.x264-Baz[Bar].mkv", "The.Series.Name", 1, 4)] - [InlineData(@"Love.Death.and.Robots.S01.1080p.NF.WEB-DL.DDP5.1.x264-NTG/Love.Death.and.Robots.S01E01.Sonnies.Edge.1080p.NF.WEB-DL.DDP5.1.x264-NTG.mkv", "Love.Death.and.Robots", 1, 1)] + [InlineData("/Foo/The.Series.Name.S01E04.WEBRip.x264-Baz[Bar]/the.series.name.s01e04.webrip.x264-Baz[Bar].mkv", "The.Series.Name", 1, 4)] + [InlineData("Love.Death.and.Robots.S01.1080p.NF.WEB-DL.DDP5.1.x264-NTG/Love.Death.and.Robots.S01E01.Sonnies.Edge.1080p.NF.WEB-DL.DDP5.1.x264-NTG.mkv", "Love.Death.and.Robots", 1, 1)] [InlineData("[YuiSubs] Tensura Nikki - Tensei Shitara Slime Datta Ken/[YuiSubs] Tensura Nikki - Tensei Shitara Slime Datta Ken - 12 (NVENC H.265 1080p).mkv", "Tensura Nikki - Tensei Shitara Slime Datta Ken", null, 12)] [InlineData("[Baz-Bar]Foo - 01 - 12[1080p][Multiple Subtitle]/[Baz-Bar] Foo - 05 [1080p][Multiple Subtitle].mkv", "Foo", null, 5)] [InlineData("Series/4-12 - The Woman.mp4", "", 4, 12, 12)] diff --git a/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs b/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs index b1141df479..62d60e5a4b 100644 --- a/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs @@ -10,34 +10,34 @@ namespace Jellyfin.Naming.Tests.Video private readonly NamingOptions _namingOptions = new NamingOptions(); [Theory] - [InlineData(@"The Wolf of Wall Street (2013).mkv", "The Wolf of Wall Street", 2013)] - [InlineData(@"The Wolf of Wall Street 2 (2013).mkv", "The Wolf of Wall Street 2", 2013)] - [InlineData(@"The Wolf of Wall Street - 2 (2013).mkv", "The Wolf of Wall Street - 2", 2013)] - [InlineData(@"The Wolf of Wall Street 2001 (2013).mkv", "The Wolf of Wall Street 2001", 2013)] - [InlineData(@"300 (2006).mkv", "300", 2006)] - [InlineData(@"d:/movies/300 (2006).mkv", "300", 2006)] - [InlineData(@"300 2 (2006).mkv", "300 2", 2006)] - [InlineData(@"300 - 2 (2006).mkv", "300 - 2", 2006)] - [InlineData(@"300 2001 (2006).mkv", "300 2001", 2006)] - [InlineData(@"curse.of.chucky.2013.stv.unrated.multi.1080p.bluray.x264-rough", "curse.of.chucky", 2013)] - [InlineData(@"curse.of.chucky.2013.stv.unrated.multi.2160p.bluray.x264-rough", "curse.of.chucky", 2013)] - [InlineData(@"/server/Movies/300 (2007)/300 (2006).bluray.disc", "300", 2006)] - [InlineData(@"Arrival.2016.2160p.Blu-Ray.HEVC.mkv", "Arrival", 2016)] - [InlineData(@"The Wolf of Wall Street (2013)", "The Wolf of Wall Street", 2013)] - [InlineData(@"The Wolf of Wall Street 2 (2013)", "The Wolf of Wall Street 2", 2013)] - [InlineData(@"The Wolf of Wall Street - 2 (2013)", "The Wolf of Wall Street - 2", 2013)] - [InlineData(@"The Wolf of Wall Street 2001 (2013)", "The Wolf of Wall Street 2001", 2013)] - [InlineData(@"300 (2006)", "300", 2006)] - [InlineData(@"d:/movies/300 (2006)", "300", 2006)] - [InlineData(@"300 2 (2006)", "300 2", 2006)] - [InlineData(@"300 - 2 (2006)", "300 - 2", 2006)] - [InlineData(@"300 2001 (2006)", "300 2001", 2006)] - [InlineData(@"/server/Movies/300 (2007)/300 (2006)", "300", 2006)] - [InlineData(@"/server/Movies/300 (2007)/300 (2006).mkv", "300", 2006)] - [InlineData(@"American.Psycho.mkv", "American.Psycho.mkv", null)] - [InlineData(@"American Psycho.mkv", "American Psycho.mkv", null)] - [InlineData(@"[rec].mkv", "[rec].mkv", null)] - [InlineData(@"St. Vincent (2014)", "St. Vincent", 2014)] + [InlineData("The Wolf of Wall Street (2013).mkv", "The Wolf of Wall Street", 2013)] + [InlineData("The Wolf of Wall Street 2 (2013).mkv", "The Wolf of Wall Street 2", 2013)] + [InlineData("The Wolf of Wall Street - 2 (2013).mkv", "The Wolf of Wall Street - 2", 2013)] + [InlineData("The Wolf of Wall Street 2001 (2013).mkv", "The Wolf of Wall Street 2001", 2013)] + [InlineData("300 (2006).mkv", "300", 2006)] + [InlineData("d:/movies/300 (2006).mkv", "300", 2006)] + [InlineData("300 2 (2006).mkv", "300 2", 2006)] + [InlineData("300 - 2 (2006).mkv", "300 - 2", 2006)] + [InlineData("300 2001 (2006).mkv", "300 2001", 2006)] + [InlineData("curse.of.chucky.2013.stv.unrated.multi.1080p.bluray.x264-rough", "curse.of.chucky", 2013)] + [InlineData("curse.of.chucky.2013.stv.unrated.multi.2160p.bluray.x264-rough", "curse.of.chucky", 2013)] + [InlineData("/server/Movies/300 (2007)/300 (2006).bluray.disc", "300", 2006)] + [InlineData("Arrival.2016.2160p.Blu-Ray.HEVC.mkv", "Arrival", 2016)] + [InlineData("The Wolf of Wall Street (2013)", "The Wolf of Wall Street", 2013)] + [InlineData("The Wolf of Wall Street 2 (2013)", "The Wolf of Wall Street 2", 2013)] + [InlineData("The Wolf of Wall Street - 2 (2013)", "The Wolf of Wall Street - 2", 2013)] + [InlineData("The Wolf of Wall Street 2001 (2013)", "The Wolf of Wall Street 2001", 2013)] + [InlineData("300 (2006)", "300", 2006)] + [InlineData("d:/movies/300 (2006)", "300", 2006)] + [InlineData("300 2 (2006)", "300 2", 2006)] + [InlineData("300 - 2 (2006)", "300 - 2", 2006)] + [InlineData("300 2001 (2006)", "300 2001", 2006)] + [InlineData("/server/Movies/300 (2007)/300 (2006)", "300", 2006)] + [InlineData("/server/Movies/300 (2007)/300 (2006).mkv", "300", 2006)] + [InlineData("American.Psycho.mkv", "American.Psycho.mkv", null)] + [InlineData("American Psycho.mkv", "American Psycho.mkv", null)] + [InlineData("[rec].mkv", "[rec].mkv", null)] + [InlineData("St. Vincent (2014)", "St. Vincent", 2014)] [InlineData("Super movie(2009).mp4", "Super movie", 2009)] [InlineData("Drug War 2013.mp4", "Drug War", 2013)] [InlineData("My Movie (1997) - GreatestReleaseGroup 2019.mp4", "My Movie", 1997)] @@ -45,9 +45,9 @@ namespace Jellyfin.Naming.Tests.Video [InlineData("First Man (2018) 1080p.mkv", "First Man", 2018)] [InlineData("Maximum Ride - 2016 - WEBDL-1080p - x264 AC3.mkv", "Maximum Ride", 2016)] // FIXME: [InlineData("Robin Hood [Multi-Subs] [2018].mkv", "Robin Hood", 2018)] - [InlineData(@"3.Days.to.Kill.2014.720p.BluRay.x264.YIFY.mkv", "3.Days.to.Kill", 2014)] // In this test case, running CleanDateTime first produces no date, so it will attempt to run CleanString first and then CleanDateTime again + [InlineData("3.Days.to.Kill.2014.720p.BluRay.x264.YIFY.mkv", "3.Days.to.Kill", 2014)] // In this test case, running CleanDateTime first produces no date, so it will attempt to run CleanString first and then CleanDateTime again [InlineData("3 days to kill (2005).mkv", "3 days to kill", 2005)] - [InlineData(@"Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - Ozlem.mp4", "Rain Man", 1988)] + [InlineData("Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - Ozlem.mp4", "Rain Man", 1988)] [InlineData("My Movie 2013.12.09", "My Movie 2013.12.09", null)] [InlineData("My Movie 2013-12-09", "My Movie 2013-12-09", null)] [InlineData("My Movie 20131209", "My Movie 20131209", null)] diff --git a/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs b/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs index 511a014a60..fccce5bffc 100644 --- a/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs @@ -22,7 +22,7 @@ namespace Jellyfin.Naming.Tests.Video [Fact] public void Test3DName() { - var result = VideoResolver.ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.3d.hsbs.mkv", _namingOptions); + var result = VideoResolver.ResolveFile("C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.3d.hsbs.mkv", _namingOptions); Assert.Equal("hsbs", result?.Format3D); Assert.Equal("Oblivion", result?.Name); diff --git a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs index 294f11ee74..183ec89848 100644 --- a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs @@ -15,10 +15,10 @@ namespace Jellyfin.Naming.Tests.Video { var files = new[] { - @"/movies/X-Men Days of Future Past/X-Men Days of Future Past - 1080p.mkv", - @"/movies/X-Men Days of Future Past/X-Men Days of Future Past-trailer.mp4", - @"/movies/X-Men Days of Future Past/X-Men Days of Future Past - [hsbs].mkv", - @"/movies/X-Men Days of Future Past/X-Men Days of Future Past [hsbs].mkv" + "/movies/X-Men Days of Future Past/X-Men Days of Future Past - 1080p.mkv", + "/movies/X-Men Days of Future Past/X-Men Days of Future Past-trailer.mp4", + "/movies/X-Men Days of Future Past/X-Men Days of Future Past - [hsbs].mkv", + "/movies/X-Men Days of Future Past/X-Men Days of Future Past [hsbs].mkv" }; var result = VideoListResolver.Resolve( @@ -34,10 +34,10 @@ namespace Jellyfin.Naming.Tests.Video { var files = new[] { - @"/movies/X-Men Days of Future Past/X-Men Days of Future Past - apple.mkv", - @"/movies/X-Men Days of Future Past/X-Men Days of Future Past-trailer.mp4", - @"/movies/X-Men Days of Future Past/X-Men Days of Future Past - banana.mkv", - @"/movies/X-Men Days of Future Past/X-Men Days of Future Past [banana].mp4" + "/movies/X-Men Days of Future Past/X-Men Days of Future Past - apple.mkv", + "/movies/X-Men Days of Future Past/X-Men Days of Future Past-trailer.mp4", + "/movies/X-Men Days of Future Past/X-Men Days of Future Past - banana.mkv", + "/movies/X-Men Days of Future Past/X-Men Days of Future Past [banana].mp4" }; var result = VideoListResolver.Resolve( @@ -54,8 +54,8 @@ namespace Jellyfin.Naming.Tests.Video { var files = new[] { - @"/movies/The Phantom of the Opera (1925)/The Phantom of the Opera (1925) - 1925 version.mkv", - @"/movies/The Phantom of the Opera (1925)/The Phantom of the Opera (1925) - 1929 version.mkv" + "/movies/The Phantom of the Opera (1925)/The Phantom of the Opera (1925) - 1925 version.mkv", + "/movies/The Phantom of the Opera (1925)/The Phantom of the Opera (1925) - 1929 version.mkv" }; var result = VideoListResolver.Resolve( @@ -71,13 +71,13 @@ namespace Jellyfin.Naming.Tests.Video { var files = new[] { - @"/movies/M/Movie 1.mkv", - @"/movies/M/Movie 2.mkv", - @"/movies/M/Movie 3.mkv", - @"/movies/M/Movie 4.mkv", - @"/movies/M/Movie 5.mkv", - @"/movies/M/Movie 6.mkv", - @"/movies/M/Movie 7.mkv" + "/movies/M/Movie 1.mkv", + "/movies/M/Movie 2.mkv", + "/movies/M/Movie 3.mkv", + "/movies/M/Movie 4.mkv", + "/movies/M/Movie 5.mkv", + "/movies/M/Movie 6.mkv", + "/movies/M/Movie 7.mkv" }; var result = VideoListResolver.Resolve( @@ -93,14 +93,14 @@ namespace Jellyfin.Naming.Tests.Video { var files = new[] { - @"/movies/Movie/Movie.mkv", - @"/movies/Movie/Movie-2.mkv", - @"/movies/Movie/Movie-3.mkv", - @"/movies/Movie/Movie-4.mkv", - @"/movies/Movie/Movie-5.mkv", - @"/movies/Movie/Movie-6.mkv", - @"/movies/Movie/Movie-7.mkv", - @"/movies/Movie/Movie-8.mkv" + "/movies/Movie/Movie.mkv", + "/movies/Movie/Movie-2.mkv", + "/movies/Movie/Movie-3.mkv", + "/movies/Movie/Movie-4.mkv", + "/movies/Movie/Movie-5.mkv", + "/movies/Movie/Movie-6.mkv", + "/movies/Movie/Movie-7.mkv", + "/movies/Movie/Movie-8.mkv" }; var result = VideoListResolver.Resolve( @@ -116,15 +116,15 @@ namespace Jellyfin.Naming.Tests.Video { var files = new[] { - @"/movies/Mo/Movie 1.mkv", - @"/movies/Mo/Movie 2.mkv", - @"/movies/Mo/Movie 3.mkv", - @"/movies/Mo/Movie 4.mkv", - @"/movies/Mo/Movie 5.mkv", - @"/movies/Mo/Movie 6.mkv", - @"/movies/Mo/Movie 7.mkv", - @"/movies/Mo/Movie 8.mkv", - @"/movies/Mo/Movie 9.mkv" + "/movies/Mo/Movie 1.mkv", + "/movies/Mo/Movie 2.mkv", + "/movies/Mo/Movie 3.mkv", + "/movies/Mo/Movie 4.mkv", + "/movies/Mo/Movie 5.mkv", + "/movies/Mo/Movie 6.mkv", + "/movies/Mo/Movie 7.mkv", + "/movies/Mo/Movie 8.mkv", + "/movies/Mo/Movie 9.mkv" }; var result = VideoListResolver.Resolve( @@ -140,11 +140,11 @@ namespace Jellyfin.Naming.Tests.Video { var files = new[] { - @"/movies/Movie/Movie 1.mkv", - @"/movies/Movie/Movie 2.mkv", - @"/movies/Movie/Movie 3.mkv", - @"/movies/Movie/Movie 4.mkv", - @"/movies/Movie/Movie 5.mkv" + "/movies/Movie/Movie 1.mkv", + "/movies/Movie/Movie 2.mkv", + "/movies/Movie/Movie 3.mkv", + "/movies/Movie/Movie 4.mkv", + "/movies/Movie/Movie 5.mkv" }; var result = VideoListResolver.Resolve( @@ -162,11 +162,11 @@ namespace Jellyfin.Naming.Tests.Video var files = new[] { - @"/movies/Iron Man/Iron Man.mkv", - @"/movies/Iron Man/Iron Man (2008).mkv", - @"/movies/Iron Man/Iron Man (2009).mkv", - @"/movies/Iron Man/Iron Man (2010).mkv", - @"/movies/Iron Man/Iron Man (2011).mkv" + "/movies/Iron Man/Iron Man.mkv", + "/movies/Iron Man/Iron Man (2008).mkv", + "/movies/Iron Man/Iron Man (2009).mkv", + "/movies/Iron Man/Iron Man (2010).mkv", + "/movies/Iron Man/Iron Man (2011).mkv" }; var result = VideoListResolver.Resolve( @@ -182,13 +182,13 @@ namespace Jellyfin.Naming.Tests.Video { var files = new[] { - @"/movies/Iron Man/Iron Man.mkv", - @"/movies/Iron Man/Iron Man-720p.mkv", - @"/movies/Iron Man/Iron Man-test.mkv", - @"/movies/Iron Man/Iron Man-bluray.mkv", - @"/movies/Iron Man/Iron Man-3d.mkv", - @"/movies/Iron Man/Iron Man-3d-hsbs.mkv", - @"/movies/Iron Man/Iron Man[test].mkv" + "/movies/Iron Man/Iron Man.mkv", + "/movies/Iron Man/Iron Man-720p.mkv", + "/movies/Iron Man/Iron Man-test.mkv", + "/movies/Iron Man/Iron Man-bluray.mkv", + "/movies/Iron Man/Iron Man-3d.mkv", + "/movies/Iron Man/Iron Man-3d-hsbs.mkv", + "/movies/Iron Man/Iron Man[test].mkv" }; var result = VideoListResolver.Resolve( @@ -211,13 +211,13 @@ namespace Jellyfin.Naming.Tests.Video { var files = new[] { - @"/movies/Iron Man/Iron Man.mkv", - @"/movies/Iron Man/Iron Man - 720p.mkv", - @"/movies/Iron Man/Iron Man - test.mkv", - @"/movies/Iron Man/Iron Man - bluray.mkv", - @"/movies/Iron Man/Iron Man - 3d.mkv", - @"/movies/Iron Man/Iron Man - 3d-hsbs.mkv", - @"/movies/Iron Man/Iron Man [test].mkv" + "/movies/Iron Man/Iron Man.mkv", + "/movies/Iron Man/Iron Man - 720p.mkv", + "/movies/Iron Man/Iron Man - test.mkv", + "/movies/Iron Man/Iron Man - bluray.mkv", + "/movies/Iron Man/Iron Man - 3d.mkv", + "/movies/Iron Man/Iron Man - 3d-hsbs.mkv", + "/movies/Iron Man/Iron Man [test].mkv" }; var result = VideoListResolver.Resolve( @@ -240,8 +240,8 @@ namespace Jellyfin.Naming.Tests.Video { var files = new[] { - @"/movies/Iron Man/Iron Man - B (2006).mkv", - @"/movies/Iron Man/Iron Man - C (2007).mkv" + "/movies/Iron Man/Iron Man - B (2006).mkv", + "/movies/Iron Man/Iron Man - C (2007).mkv" }; var result = VideoListResolver.Resolve( @@ -256,13 +256,13 @@ namespace Jellyfin.Naming.Tests.Video { var files = new[] { - @"/movies/Iron Man/Iron Man.mkv", - @"/movies/Iron Man/Iron Man_720p.mkv", - @"/movies/Iron Man/Iron Man_test.mkv", - @"/movies/Iron Man/Iron Man_bluray.mkv", - @"/movies/Iron Man/Iron Man_3d.mkv", - @"/movies/Iron Man/Iron Man_3d-hsbs.mkv", - @"/movies/Iron Man/Iron Man_3d.hsbs.mkv" + "/movies/Iron Man/Iron Man.mkv", + "/movies/Iron Man/Iron Man_720p.mkv", + "/movies/Iron Man/Iron Man_test.mkv", + "/movies/Iron Man/Iron Man_bluray.mkv", + "/movies/Iron Man/Iron Man_3d.mkv", + "/movies/Iron Man/Iron Man_3d-hsbs.mkv", + "/movies/Iron Man/Iron Man_3d.hsbs.mkv" }; var result = VideoListResolver.Resolve( @@ -280,11 +280,11 @@ namespace Jellyfin.Naming.Tests.Video var files = new[] { - @"/movies/Iron Man/Iron Man (2007).mkv", - @"/movies/Iron Man/Iron Man (2008).mkv", - @"/movies/Iron Man/Iron Man (2009).mkv", - @"/movies/Iron Man/Iron Man (2010).mkv", - @"/movies/Iron Man/Iron Man (2011).mkv" + "/movies/Iron Man/Iron Man (2007).mkv", + "/movies/Iron Man/Iron Man (2008).mkv", + "/movies/Iron Man/Iron Man (2009).mkv", + "/movies/Iron Man/Iron Man (2010).mkv", + "/movies/Iron Man/Iron Man (2011).mkv" }; var result = VideoListResolver.Resolve( @@ -300,8 +300,8 @@ namespace Jellyfin.Naming.Tests.Video { var files = new[] { - @"/movies/Blade Runner (1982)/Blade Runner (1982) [Final Cut] [1080p HEVC AAC].mkv", - @"/movies/Blade Runner (1982)/Blade Runner (1982) [EE by ADM] [480p HEVC AAC,AAC,AAC].mkv" + "/movies/Blade Runner (1982)/Blade Runner (1982) [Final Cut] [1080p HEVC AAC].mkv", + "/movies/Blade Runner (1982)/Blade Runner (1982) [EE by ADM] [480p HEVC AAC,AAC,AAC].mkv" }; var result = VideoListResolver.Resolve( @@ -317,8 +317,8 @@ namespace Jellyfin.Naming.Tests.Video { var files = new[] { - @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) [1080p] Blu-ray.x264.DTS.mkv", - @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) [2160p] Blu-ray.x265.AAC.mkv" + "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) [1080p] Blu-ray.x264.DTS.mkv", + "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) [2160p] Blu-ray.x265.AAC.mkv" }; var result = VideoListResolver.Resolve( @@ -334,12 +334,12 @@ namespace Jellyfin.Naming.Tests.Video { var files = new[] { - @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - Theatrical Release.mkv", - @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - Directors Cut.mkv", - @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p.mkv", - @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 2160p.mkv", - @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 720p.mkv", - @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016).mkv", + "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - Theatrical Release.mkv", + "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - Directors Cut.mkv", + "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p.mkv", + "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 2160p.mkv", + "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 720p.mkv", + "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016).mkv", }; var result = VideoListResolver.Resolve( @@ -361,8 +361,8 @@ namespace Jellyfin.Naming.Tests.Video { var files = new[] { - @"/movies/John Wick - Kapitel 3 (2019) [imdbid=tt6146586]/John Wick - Kapitel 3 (2019) [imdbid=tt6146586] - Version 1.mkv", - @"/movies/John Wick - Kapitel 3 (2019) [imdbid=tt6146586]/John Wick - Kapitel 3 (2019) [imdbid=tt6146586] - Version 2.mkv" + "/movies/John Wick - Kapitel 3 (2019) [imdbid=tt6146586]/John Wick - Kapitel 3 (2019) [imdbid=tt6146586] - Version 1.mkv", + "/movies/John Wick - Kapitel 3 (2019) [imdbid=tt6146586]/John Wick - Kapitel 3 (2019) [imdbid=tt6146586] - Version 2.mkv" }; var result = VideoListResolver.Resolve( @@ -378,8 +378,8 @@ namespace Jellyfin.Naming.Tests.Video { var files = new[] { - @"/movies/John Wick - Chapter 3 (2019)/John Wick - Chapter 3 (2019) [Version 1].mkv", - @"/movies/John Wick - Chapter 3 (2019)/John Wick - Chapter 3 (2019) [Version 2.mkv" + "/movies/John Wick - Chapter 3 (2019)/John Wick - Chapter 3 (2019) [Version 1].mkv", + "/movies/John Wick - Chapter 3 (2019)/John Wick - Chapter 3 (2019) [Version 2.mkv" }; var result = VideoListResolver.Resolve( diff --git a/tests/Jellyfin.Naming.Tests/Video/StackTests.cs b/tests/Jellyfin.Naming.Tests/Video/StackTests.cs index 97b52f7495..c95703f536 100644 --- a/tests/Jellyfin.Naming.Tests/Video/StackTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/StackTests.cs @@ -384,8 +384,8 @@ namespace Jellyfin.Naming.Tests.Video // No stacking here because there is no part/disc/etc var files = new[] { - @"M:/Movies (DVD)/Movies (Musical)/The Sound of Music/The Sound of Music (1965) (Disc 01)", - @"M:/Movies (DVD)/Movies (Musical)/The Sound of Music/The Sound of Music (1965) (Disc 02)" + "M:/Movies (DVD)/Movies (Musical)/The Sound of Music/The Sound of Music (1965) (Disc 01)", + "M:/Movies (DVD)/Movies (Musical)/The Sound of Music/The Sound of Music (1965) (Disc 02)" }; var result = StackResolver.ResolveDirectories(files, _namingOptions).ToList(); diff --git a/tests/Jellyfin.Naming.Tests/Video/StubTests.cs b/tests/Jellyfin.Naming.Tests/Video/StubTests.cs index 1d50df7a60..fc852ae85b 100644 --- a/tests/Jellyfin.Naming.Tests/Video/StubTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/StubTests.cs @@ -29,7 +29,7 @@ namespace Jellyfin.Naming.Tests.Video [Fact] public void TestStubName() { - var result = VideoResolver.ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.dvd.disc", _namingOptions); + var result = VideoResolver.ResolveFile("C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.dvd.disc", _namingOptions); Assert.Equal("Oblivion", result?.Name); } diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs index 0316377d49..377f82eac7 100644 --- a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs @@ -200,8 +200,8 @@ namespace Jellyfin.Naming.Tests.Video { var files = new[] { - @"M:/Movies (DVD)/Movies (Musical)/Sound of Music (1965)/Sound of Music Disc 1", - @"M:/Movies (DVD)/Movies (Musical)/Sound of Music (1965)/Sound of Music Disc 2" + "M:/Movies (DVD)/Movies (Musical)/Sound of Music (1965)/Sound of Music Disc 1", + "M:/Movies (DVD)/Movies (Musical)/Sound of Music (1965)/Sound of Music Disc 2" }; var result = VideoListResolver.Resolve( @@ -217,8 +217,8 @@ namespace Jellyfin.Naming.Tests.Video // These should be considered separate, unrelated videos var files = new[] { - @"My movie #1.mp4", - @"My movie #2.mp4" + "My movie #1.mp4", + "My movie #2.mp4" }; var result = VideoListResolver.Resolve( @@ -233,10 +233,10 @@ namespace Jellyfin.Naming.Tests.Video { var files = new[] { - @"No (2012) part1.mp4", - @"No (2012) part2.mp4", - @"No (2012) part1-trailer.mp4", - @"No (2012)-trailer.mp4" + "No (2012) part1.mp4", + "No (2012) part2.mp4", + "No (2012) part1-trailer.mp4", + "No (2012)-trailer.mp4" }; var result = VideoListResolver.Resolve( @@ -254,10 +254,10 @@ namespace Jellyfin.Naming.Tests.Video { var files = new[] { - @"/Movies/Top Gun (1984)/movie.mp4", - @"/Movies/Top Gun (1984)/Top Gun (1984)-trailer.mp4", - @"/Movies/Top Gun (1984)/Top Gun (1984)-trailer2.mp4", - @"/Movies/trailer.mp4" + "/Movies/Top Gun (1984)/movie.mp4", + "/Movies/Top Gun (1984)/Top Gun (1984)-trailer.mp4", + "/Movies/Top Gun (1984)/Top Gun (1984)-trailer2.mp4", + "/Movies/trailer.mp4" }; var result = VideoListResolver.Resolve( @@ -276,10 +276,10 @@ namespace Jellyfin.Naming.Tests.Video { var files = new[] { - @"/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Counterfeit Racks (2011) Disc 1 cd1.avi", - @"/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Counterfeit Racks (2011) Disc 1 cd2.avi", - @"/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Disc 2 cd1.avi", - @"/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Disc 2 cd2.avi" + "/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Counterfeit Racks (2011) Disc 1 cd1.avi", + "/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Counterfeit Racks (2011) Disc 1 cd2.avi", + "/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Disc 2 cd1.avi", + "/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Disc 2 cd2.avi" }; var result = VideoListResolver.Resolve( @@ -294,7 +294,7 @@ namespace Jellyfin.Naming.Tests.Video { var files = new[] { - @"/nas-markrobbo78/Videos/INDEX HTPC/Movies/Watched/3 - ACTION/Argo (2012)/movie.mkv" + "/nas-markrobbo78/Videos/INDEX HTPC/Movies/Watched/3 - ACTION/Argo (2012)/movie.mkv" }; var result = VideoListResolver.Resolve( @@ -309,7 +309,7 @@ namespace Jellyfin.Naming.Tests.Video { var files = new[] { - @"The Colony.mkv" + "The Colony.mkv" }; var result = VideoListResolver.Resolve( @@ -324,8 +324,8 @@ namespace Jellyfin.Naming.Tests.Video { var files = new[] { - @"Four Sisters and a Wedding - A.avi", - @"Four Sisters and a Wedding - B.avi" + "Four Sisters and a Wedding - A.avi", + "Four Sisters and a Wedding - B.avi" }; var result = VideoListResolver.Resolve( @@ -342,8 +342,8 @@ namespace Jellyfin.Naming.Tests.Video { var files = new[] { - @"Four Rooms - A.avi", - @"Four Rooms - A.mp4" + "Four Rooms - A.avi", + "Four Rooms - A.mp4" }; var result = VideoListResolver.Resolve( @@ -358,8 +358,8 @@ namespace Jellyfin.Naming.Tests.Video { var files = new[] { - @"/Server/Despicable Me/Despicable Me (2010).mkv", - @"/Server/Despicable Me/trailer.mkv" + "/Server/Despicable Me/Despicable Me (2010).mkv", + "/Server/Despicable Me/trailer.mkv" }; var result = VideoListResolver.Resolve( @@ -376,8 +376,8 @@ namespace Jellyfin.Naming.Tests.Video { var files = new[] { - @"/Server/Despicable Me/Despicable Me (2010).mkv", - @"/Server/Despicable Me/trailers/some title.mkv" + "/Server/Despicable Me/Despicable Me (2010).mkv", + "/Server/Despicable Me/trailers/some title.mkv" }; var result = VideoListResolver.Resolve( @@ -394,8 +394,8 @@ namespace Jellyfin.Naming.Tests.Video { var files = new[] { - @"/Movies/Despicable Me/Despicable Me.mkv", - @"/Movies/Despicable Me/trailers/trailer.mkv" + "/Movies/Despicable Me/Despicable Me.mkv", + "/Movies/Despicable Me/trailers/trailer.mkv" }; var result = VideoListResolver.Resolve( diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs index 33a99e107f..8455a56a1b 100644 --- a/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs @@ -15,26 +15,26 @@ namespace Jellyfin.Naming.Tests.Video var data = new TheoryData<VideoFileInfo>(); data.Add( new VideoFileInfo( - path: @"/server/Movies/7 Psychos.mkv/7 Psychos.mkv", + path: "/server/Movies/7 Psychos.mkv/7 Psychos.mkv", container: "mkv", name: "7 Psychos")); data.Add( new VideoFileInfo( - path: @"/server/Movies/3 days to kill (2005)/3 days to kill (2005).mkv", + path: "/server/Movies/3 days to kill (2005)/3 days to kill (2005).mkv", container: "mkv", name: "3 days to kill", year: 2005)); data.Add( new VideoFileInfo( - path: @"/server/Movies/American Psycho/American.Psycho.mkv", + path: "/server/Movies/American Psycho/American.Psycho.mkv", container: "mkv", name: "American.Psycho")); data.Add( new VideoFileInfo( - path: @"/server/Movies/brave (2007)/brave (2006).3d.sbs.mkv", + path: "/server/Movies/brave (2007)/brave (2006).3d.sbs.mkv", container: "mkv", name: "brave", year: 2006, @@ -43,14 +43,14 @@ namespace Jellyfin.Naming.Tests.Video data.Add( new VideoFileInfo( - path: @"/server/Movies/300 (2007)/300 (2006).3d1.sbas.mkv", + path: "/server/Movies/300 (2007)/300 (2006).3d1.sbas.mkv", container: "mkv", name: "300", year: 2006)); data.Add( new VideoFileInfo( - path: @"/server/Movies/300 (2007)/300 (2006).3d.sbs.mkv", + path: "/server/Movies/300 (2007)/300 (2006).3d.sbs.mkv", container: "mkv", name: "300", year: 2006, @@ -59,7 +59,7 @@ namespace Jellyfin.Naming.Tests.Video data.Add( new VideoFileInfo( - path: @"/server/Movies/brave (2007)/brave (2006)-trailer.bluray.disc", + path: "/server/Movies/brave (2007)/brave (2006)-trailer.bluray.disc", container: "disc", name: "brave", year: 2006, @@ -68,7 +68,7 @@ namespace Jellyfin.Naming.Tests.Video data.Add( new VideoFileInfo( - path: @"/server/Movies/300 (2007)/300 (2006)-trailer.bluray.disc", + path: "/server/Movies/300 (2007)/300 (2006)-trailer.bluray.disc", container: "disc", name: "300", year: 2006, @@ -77,7 +77,7 @@ namespace Jellyfin.Naming.Tests.Video data.Add( new VideoFileInfo( - path: @"/server/Movies/Brave (2007)/Brave (2006).bluray.disc", + path: "/server/Movies/Brave (2007)/Brave (2006).bluray.disc", container: "disc", name: "Brave", year: 2006, @@ -86,7 +86,7 @@ namespace Jellyfin.Naming.Tests.Video data.Add( new VideoFileInfo( - path: @"/server/Movies/300 (2007)/300 (2006).bluray.disc", + path: "/server/Movies/300 (2007)/300 (2006).bluray.disc", container: "disc", name: "300", year: 2006, @@ -95,7 +95,7 @@ namespace Jellyfin.Naming.Tests.Video data.Add( new VideoFileInfo( - path: @"/server/Movies/300 (2007)/300 (2006)-trailer.mkv", + path: "/server/Movies/300 (2007)/300 (2006)-trailer.mkv", container: "mkv", name: "300", year: 2006, @@ -103,7 +103,7 @@ namespace Jellyfin.Naming.Tests.Video data.Add( new VideoFileInfo( - path: @"/server/Movies/Brave (2007)/Brave (2006)-trailer.mkv", + path: "/server/Movies/Brave (2007)/Brave (2006)-trailer.mkv", container: "mkv", name: "Brave", year: 2006, @@ -111,28 +111,28 @@ namespace Jellyfin.Naming.Tests.Video data.Add( new VideoFileInfo( - path: @"/server/Movies/300 (2007)/300 (2006).mkv", + path: "/server/Movies/300 (2007)/300 (2006).mkv", container: "mkv", name: "300", year: 2006)); data.Add( new VideoFileInfo( - path: @"/server/Movies/Bad Boys (1995)/Bad Boys (1995).mkv", + path: "/server/Movies/Bad Boys (1995)/Bad Boys (1995).mkv", container: "mkv", name: "Bad Boys", year: 1995)); data.Add( new VideoFileInfo( - path: @"/server/Movies/Brave (2007)/Brave (2006).mkv", + path: "/server/Movies/Brave (2007)/Brave (2006).mkv", container: "mkv", name: "Brave", year: 2006)); data.Add( new VideoFileInfo( - path: @"/server/Movies/Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - JEFF/Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - JEFF.mp4", + path: "/server/Movies/Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - JEFF/Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - JEFF.mp4", container: "mp4", name: "Rain Man", year: 1988)); @@ -174,8 +174,8 @@ namespace Jellyfin.Naming.Tests.Video { var paths = new[] { - @"/Server/Iron Man", - @"Batman", + "/Server/Iron Man", + "Batman", string.Empty }; diff --git a/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs b/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs index f157f01e5a..d5ab6ab4be 100644 --- a/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs +++ b/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs @@ -352,11 +352,11 @@ namespace Jellyfin.Providers.Tests.Manager { if (forceRefresh) { - Assert.Matches(@"image url [0-9]", image.Path); + Assert.Matches("image url [0-9]", image.Path); } else { - Assert.DoesNotMatch(@"image url [0-9]", image.Path); + Assert.DoesNotMatch("image url [0-9]", image.Path); } } } diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/MediaInfoResolverTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/MediaInfoResolverTests.cs index 6ee4b8ef22..2b38675121 100644 --- a/tests/Jellyfin.Providers.Tests/MediaInfo/MediaInfoResolverTests.cs +++ b/tests/Jellyfin.Providers.Tests/MediaInfo/MediaInfoResolverTests.cs @@ -29,7 +29,7 @@ public class MediaInfoResolverTests public const string VideoDirectoryPath = "Test Data/Video"; public const string VideoDirectoryRegex = @"Test Data[/\\]Video"; public const string MetadataDirectoryPath = "library/00/00000000000000000000000000000000"; - public const string MetadataDirectoryRegex = @"library.*"; + public const string MetadataDirectoryRegex = "library.*"; private readonly ILocalizationManager _localizationManager; private readonly MediaInfoResolver _subtitleResolver; @@ -49,7 +49,7 @@ public class MediaInfoResolverTests var englishCultureDto = new CultureDto("English", "English", "en", new[] { "eng" }); var localizationManager = new Mock<ILocalizationManager>(MockBehavior.Loose); - localizationManager.Setup(lm => lm.FindLanguageInfo(It.IsRegex(@"en.*", RegexOptions.IgnoreCase))) + localizationManager.Setup(lm => lm.FindLanguageInfo(It.IsRegex("en.*", RegexOptions.IgnoreCase))) .Returns(englishCultureDto); _localizationManager = localizationManager.Object; @@ -79,7 +79,7 @@ public class MediaInfoResolverTests { // need a media source manager capable of returning something other than file protocol var mediaSourceManager = new Mock<IMediaSourceManager>(); - mediaSourceManager.Setup(m => m.GetPathProtocol(It.IsRegex(@"http.*"))) + mediaSourceManager.Setup(m => m.GetPathProtocol(It.IsRegex("http.*"))) .Returns(MediaProtocol.Http); BaseItem.MediaSourceManager = mediaSourceManager.Object; @@ -186,7 +186,7 @@ public class MediaInfoResolverTests { // need a media source manager capable of returning something other than file protocol var mediaSourceManager = new Mock<IMediaSourceManager>(); - mediaSourceManager.Setup(m => m.GetPathProtocol(It.IsRegex(@"http.*"))) + mediaSourceManager.Setup(m => m.GetPathProtocol(It.IsRegex("http.*"))) .Returns(MediaProtocol.Http); BaseItem.MediaSourceManager = mediaSourceManager.Object; From a37dc3da96a5445178de01c43a683230bc14882f Mon Sep 17 00:00:00 2001 From: Stepan Goremykin <goremukin@gmail.com> Date: Sun, 8 Oct 2023 00:17:48 +0200 Subject: [PATCH 668/858] Use async overload --- .../Library/LibraryManager.cs | 2 +- .../Plugins/PluginManagerTests.cs | 14 +++++++------- .../OpenApiSpecTests.cs | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index b0a4a4151a..9de36d73cd 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -2858,7 +2858,7 @@ namespace Emby.Server.Implementations.Library { var path = Path.Combine(virtualFolderPath, collectionType.ToString().ToLowerInvariant() + ".collection"); - File.WriteAllBytes(path, Array.Empty<byte>()); + await File.WriteAllBytesAsync(path, Array.Empty<byte>()).ConfigureAwait(false); } CollectionFolder.SaveLibraryOptions(virtualFolderPath, options); diff --git a/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs index d4b90dac02..f2a08e43cf 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs @@ -191,13 +191,13 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins }; var metafilePath = Path.Combine(_pluginPath, "meta.json"); - File.WriteAllText(metafilePath, JsonSerializer.Serialize(partial, _options)); + await File.WriteAllTextAsync(metafilePath, JsonSerializer.Serialize(partial, _options)); var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, _tempPath, new Version(1, 0)); await pluginManager.PopulateManifest(packageInfo, new Version(1, 0), _pluginPath, PluginStatus.Active); - var resultBytes = File.ReadAllBytes(metafilePath); + var resultBytes = await File.ReadAllBytesAsync(metafilePath); var result = JsonSerializer.Deserialize<PluginManifest>(resultBytes, _options); Assert.NotNull(result); @@ -231,7 +231,7 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins await pluginManager.PopulateManifest(packageInfo, new Version(1, 0), _pluginPath, PluginStatus.Active); var metafilePath = Path.Combine(_pluginPath, "meta.json"); - var resultBytes = File.ReadAllBytes(metafilePath); + var resultBytes = await File.ReadAllBytesAsync(metafilePath); var result = JsonSerializer.Deserialize<PluginManifest>(resultBytes, _options); Assert.NotNull(result); @@ -251,13 +251,13 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins }; var metafilePath = Path.Combine(_pluginPath, "meta.json"); - File.WriteAllText(metafilePath, JsonSerializer.Serialize(partial, _options)); + await File.WriteAllTextAsync(metafilePath, JsonSerializer.Serialize(partial, _options)); var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, _tempPath, new Version(1, 0)); await pluginManager.PopulateManifest(packageInfo, new Version(1, 0), _pluginPath, PluginStatus.Active); - var resultBytes = File.ReadAllBytes(metafilePath); + var resultBytes = await File.ReadAllBytesAsync(metafilePath); var result = JsonSerializer.Deserialize<PluginManifest>(resultBytes, _options); Assert.NotNull(result); @@ -277,13 +277,13 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins }; var metafilePath = Path.Combine(_pluginPath, "meta.json"); - File.WriteAllText(metafilePath, JsonSerializer.Serialize(partial, _options)); + await File.WriteAllTextAsync(metafilePath, JsonSerializer.Serialize(partial, _options)); var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, _tempPath, new Version(1, 0)); await pluginManager.PopulateManifest(packageInfo, new Version(1, 0), _pluginPath, PluginStatus.Active); - var resultBytes = File.ReadAllBytes(metafilePath); + var resultBytes = await File.ReadAllBytesAsync(metafilePath); var result = JsonSerializer.Deserialize<PluginManifest>(resultBytes, _options); Assert.NotNull(result); diff --git a/tests/Jellyfin.Server.Integration.Tests/OpenApiSpecTests.cs b/tests/Jellyfin.Server.Integration.Tests/OpenApiSpecTests.cs index 0ade345a1a..c083974d3e 100644 --- a/tests/Jellyfin.Server.Integration.Tests/OpenApiSpecTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/OpenApiSpecTests.cs @@ -34,7 +34,7 @@ namespace Jellyfin.Server.Integration.Tests var responseBody = await response.Content.ReadAsStringAsync(); string outputPath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? ".", "openapi.json")); _outputHelper.WriteLine("Writing OpenAPI Spec JSON to '{0}'.", outputPath); - File.WriteAllText(outputPath, responseBody); + await File.WriteAllTextAsync(outputPath, responseBody); } } } From 3f9ee316d52fd9623012641c7d270df2cbba8c6b Mon Sep 17 00:00:00 2001 From: Stepan Goremykin <goremukin@gmail.com> Date: Sun, 8 Oct 2023 00:22:36 +0200 Subject: [PATCH 669/858] Use non nullable property type when possible --- MediaBrowser.Providers/Movies/ImdbExternalId.cs | 2 +- MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs | 2 +- .../Plugins/AudioDb/AudioDbAlbumExternalId.cs | 2 +- .../Plugins/AudioDb/AudioDbArtistExternalId.cs | 2 +- .../Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs | 2 +- .../Plugins/AudioDb/AudioDbOtherArtistExternalId.cs | 2 +- .../Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs | 2 +- .../Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs | 2 +- .../Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs | 2 +- .../Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs | 2 +- .../Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs | 2 +- .../Plugins/MusicBrainz/MusicBrainzTrackId.cs | 2 +- .../Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs | 2 +- .../Plugins/Tmdb/Movies/TmdbMovieExternalId.cs | 2 +- .../Plugins/Tmdb/People/TmdbPersonExternalId.cs | 2 +- MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs | 2 +- MediaBrowser.Providers/TV/Zap2ItExternalId.cs | 2 +- 17 files changed, 17 insertions(+), 17 deletions(-) diff --git a/MediaBrowser.Providers/Movies/ImdbExternalId.cs b/MediaBrowser.Providers/Movies/ImdbExternalId.cs index d00f37db58..a8d74aa0b5 100644 --- a/MediaBrowser.Providers/Movies/ImdbExternalId.cs +++ b/MediaBrowser.Providers/Movies/ImdbExternalId.cs @@ -22,7 +22,7 @@ namespace MediaBrowser.Providers.Movies public ExternalIdMediaType? Type => null; /// <inheritdoc /> - public string? UrlFormatString => "https://www.imdb.com/title/{0}"; + public string UrlFormatString => "https://www.imdb.com/title/{0}"; /// <inheritdoc /> public bool Supports(IHasProviderIds item) diff --git a/MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs b/MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs index 1bb5e1ea83..8151ab4715 100644 --- a/MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs +++ b/MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs @@ -19,7 +19,7 @@ namespace MediaBrowser.Providers.Movies public ExternalIdMediaType? Type => ExternalIdMediaType.Person; /// <inheritdoc /> - public string? UrlFormatString => "https://www.imdb.com/name/{0}"; + public string UrlFormatString => "https://www.imdb.com/name/{0}"; /// <inheritdoc /> public bool Supports(IHasProviderIds item) => item is Person; diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs index 3a400575bc..138cfef19a 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs @@ -19,7 +19,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb public ExternalIdMediaType? Type => null; /// <inheritdoc /> - public string? UrlFormatString => "https://www.theaudiodb.com/album/{0}"; + public string UrlFormatString => "https://www.theaudiodb.com/album/{0}"; /// <inheritdoc /> public bool Supports(IHasProviderIds item) => item is MusicAlbum; diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs index b9e57eb265..8aceb48c0c 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs @@ -19,7 +19,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb public ExternalIdMediaType? Type => ExternalIdMediaType.Artist; /// <inheritdoc /> - public string? UrlFormatString => "https://www.theaudiodb.com/artist/{0}"; + public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}"; /// <inheritdoc /> public bool Supports(IHasProviderIds item) => item is MusicArtist; diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs index f8f6253ffd..014481da24 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs @@ -19,7 +19,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb public ExternalIdMediaType? Type => ExternalIdMediaType.Album; /// <inheritdoc /> - public string? UrlFormatString => "https://www.theaudiodb.com/album/{0}"; + public string UrlFormatString => "https://www.theaudiodb.com/album/{0}"; /// <inheritdoc /> public bool Supports(IHasProviderIds item) => item is Audio; diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs index fd598c9186..7875391043 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs @@ -19,7 +19,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist; /// <inheritdoc /> - public string? UrlFormatString => "https://www.theaudiodb.com/artist/{0}"; + public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}"; /// <inheritdoc /> public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum; diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs index f7850781e0..825fe32fa2 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs @@ -20,7 +20,7 @@ public class MusicBrainzAlbumArtistExternalId : IExternalId public ExternalIdMediaType? Type => ExternalIdMediaType.AlbumArtist; /// <inheritdoc /> - public string? UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}"; + public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}"; /// <inheritdoc /> public bool Supports(IHasProviderIds item) => item is Audio; diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs index a9d4472e7d..b7d53984c5 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs @@ -20,7 +20,7 @@ public class MusicBrainzAlbumExternalId : IExternalId public ExternalIdMediaType? Type => ExternalIdMediaType.Album; /// <inheritdoc /> - public string? UrlFormatString => Plugin.Instance!.Configuration.Server + "/release/{0}"; + public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/release/{0}"; /// <inheritdoc /> public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum; diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs index b89e67270a..b3f001618d 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs @@ -20,7 +20,7 @@ public class MusicBrainzArtistExternalId : IExternalId public ExternalIdMediaType? Type => ExternalIdMediaType.Artist; /// <inheritdoc /> - public string? UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}"; + public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}"; /// <inheritdoc /> public bool Supports(IHasProviderIds item) => item is MusicArtist; diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs index fdaa5574f0..a0a922293d 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs @@ -20,7 +20,7 @@ public class MusicBrainzOtherArtistExternalId : IExternalId public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist; /// <inheritdoc /> - public string? UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}"; + public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}"; /// <inheritdoc /> public bool Supports(IHasProviderIds item) => item is Audio or MusicAlbum; diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs index 0baab9955d..47b6d69633 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs @@ -20,7 +20,7 @@ public class MusicBrainzReleaseGroupExternalId : IExternalId public ExternalIdMediaType? Type => ExternalIdMediaType.ReleaseGroup; /// <inheritdoc /> - public string? UrlFormatString => Plugin.Instance!.Configuration.Server + "/release-group/{0}"; + public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/release-group/{0}"; /// <inheritdoc /> public bool Supports(IHasProviderIds item) => item is Audio or MusicAlbum; diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs index 5c974c4111..cb4345660d 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs @@ -20,7 +20,7 @@ public class MusicBrainzTrackId : IExternalId public ExternalIdMediaType? Type => ExternalIdMediaType.Track; /// <inheritdoc /> - public string? UrlFormatString => Plugin.Instance!.Configuration.Server + "/track/{0}"; + public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/track/{0}"; /// <inheritdoc /> public bool Supports(IHasProviderIds item) => item is Audio; diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs index 0e768bb832..d453a4ff44 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs @@ -21,7 +21,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets public ExternalIdMediaType? Type => ExternalIdMediaType.BoxSet; /// <inheritdoc /> - public string? UrlFormatString => TmdbUtils.BaseTmdbUrl + "collection/{0}"; + public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "collection/{0}"; /// <inheritdoc /> public bool Supports(IHasProviderIds item) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs index 38d2c5c69a..6d6032e8f6 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs @@ -21,7 +21,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies public ExternalIdMediaType? Type => ExternalIdMediaType.Movie; /// <inheritdoc /> - public string? UrlFormatString => TmdbUtils.BaseTmdbUrl + "movie/{0}"; + public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "movie/{0}"; /// <inheritdoc /> public bool Supports(IHasProviderIds item) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs index 027399aec2..d26a70028c 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs @@ -20,7 +20,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People public ExternalIdMediaType? Type => ExternalIdMediaType.Person; /// <inheritdoc /> - public string? UrlFormatString => TmdbUtils.BaseTmdbUrl + "person/{0}"; + public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "person/{0}"; /// <inheritdoc /> public bool Supports(IHasProviderIds item) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs index df04cb2e73..5f2d7909a9 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs @@ -20,7 +20,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV public ExternalIdMediaType? Type => ExternalIdMediaType.Series; /// <inheritdoc /> - public string? UrlFormatString => TmdbUtils.BaseTmdbUrl + "tv/{0}"; + public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "tv/{0}"; /// <inheritdoc /> public bool Supports(IHasProviderIds item) diff --git a/MediaBrowser.Providers/TV/Zap2ItExternalId.cs b/MediaBrowser.Providers/TV/Zap2ItExternalId.cs index 087e4036a5..3cb18e4248 100644 --- a/MediaBrowser.Providers/TV/Zap2ItExternalId.cs +++ b/MediaBrowser.Providers/TV/Zap2ItExternalId.cs @@ -19,7 +19,7 @@ namespace MediaBrowser.Providers.TV public ExternalIdMediaType? Type => null; /// <inheritdoc /> - public string? UrlFormatString => "http://tvlistings.zap2it.com/overview.html?programSeriesId={0}"; + public string UrlFormatString => "http://tvlistings.zap2it.com/overview.html?programSeriesId={0}"; /// <inheritdoc /> public bool Supports(IHasProviderIds item) => item is Series; From 8ea812b65d5287dad9c599d03dba8ad2994b244a Mon Sep 17 00:00:00 2001 From: Stepan Goremykin <goremukin@gmail.com> Date: Sun, 8 Oct 2023 00:26:12 +0200 Subject: [PATCH 670/858] Reduce string literal length by using verbatim string --- Emby.Naming/Common/NamingOptions.cs | 2 +- .../IO/ManagedFileSystem.cs | 2 +- .../Library/Resolvers/TV/SeasonResolver.cs | 2 +- .../Users/UserManager.cs | 2 +- .../MediaEncoding/EncodingHelper.cs | 14 +++++------ .../Encoder/MediaEncoder.cs | 10 ++++---- .../MediaInfo/AudioFileProber.cs | 2 +- .../TV/EpisodePathParserTest.cs | 8 +++---- .../Library/PathExtensionsTests.cs | 24 +++++++++---------- .../Plugins/PluginManagerTests.cs | 4 ++-- .../Parsers/MovieNfoParserTests.cs | 2 +- 11 files changed, 36 insertions(+), 36 deletions(-) diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index 692edae4a9..b63c8f10e5 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -376,7 +376,7 @@ namespace Emby.Naming.Common IsNamed = true, SupportsAbsoluteEpisodeNumbers = false }, - new EpisodeExpression("[\\/._ -]p(?:ar)?t[_. -]()([ivx]+|[0-9]+)([._ -][^\\/]*)$") + new EpisodeExpression(@"[\/._ -]p(?:ar)?t[_. -]()([ivx]+|[0-9]+)([._ -][^\/]*)$") { SupportsAbsoluteEpisodeNumbers = true }, diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 18b00ce0b2..79eca4022a 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -91,7 +91,7 @@ namespace Emby.Server.Implementations.IO } // unc path - if (filePath.StartsWith("\\\\", StringComparison.Ordinal)) + if (filePath.StartsWith(@"\\", StringComparison.Ordinal)) { return filePath; } diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs index e9538a5c97..858c5b2812 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs @@ -62,7 +62,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV var resolver = new Naming.TV.EpisodeResolver(namingOptions); var folderName = System.IO.Path.GetFileName(path); - var testPath = "\\\\test\\" + folderName; + var testPath = @"\\test\" + folderName; var episodeInfo = resolver.Resolve(testPath, true); diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 94ac4798ca..22bfd7e6f2 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -103,7 +103,7 @@ namespace Jellyfin.Server.Implementations.Users // This is some regex that matches only on unicode "word" characters, as well as -, _ and @ // In theory this will cut out most if not all 'control' characters which should help minimize any weirdness // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), periods (.) and spaces ( ) - [GeneratedRegex("^[\\w\\ \\-'._@]+$")] + [GeneratedRegex(@"^[\w\ \-'._@]+$")] private static partial Regex ValidUsernameRegex(); /// <inheritdoc/> diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 14417a5e43..03c778c07f 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -2947,7 +2947,7 @@ namespace MediaBrowser.Controller.MediaEncoding return string.Format( CultureInfo.InvariantCulture, - "scale=trunc(min(max(iw\\,ih*a)\\,min({0}\\,{1}*a))/{2})*{2}:trunc(min(max(iw/a\\,ih)\\,min({0}/a\\,{1}))/2)*2", + @"scale=trunc(min(max(iw\,ih*a)\,min({0}\,{1}*a))/{2})*{2}:trunc(min(max(iw/a\,ih)\,min({0}/a\,{1}))/2)*2", maxWidthParam, maxHeightParam, scaleVal); @@ -2989,7 +2989,7 @@ namespace MediaBrowser.Controller.MediaEncoding return string.Format( CultureInfo.InvariantCulture, - "scale=trunc(min(max(iw\\,ih*a)\\,{0})/{1})*{1}:trunc(ow/a/2)*2", + @"scale=trunc(min(max(iw\,ih*a)\,{0})/{1})*{1}:trunc(ow/a/2)*2", maxWidthParam, scaleVal); } @@ -3001,7 +3001,7 @@ namespace MediaBrowser.Controller.MediaEncoding return string.Format( CultureInfo.InvariantCulture, - "scale=trunc(oh*a/{1})*{1}:min(max(iw/a\\,ih)\\,{0})", + @"scale=trunc(oh*a/{1})*{1}:min(max(iw/a\,ih)\,{0})", maxHeightParam, scaleVal); } @@ -3021,19 +3021,19 @@ namespace MediaBrowser.Controller.MediaEncoding switch (threedFormat.Value) { case Video3DFormat.HalfSideBySide: - filter = "crop=iw/2:ih:0:0,scale=(iw*2):ih,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1,scale={0}:trunc({0}/dar/2)*2"; + filter = @"crop=iw/2:ih:0:0,scale=(iw*2):ih,setdar=dar=a,crop=min(iw\,ih*dar):min(ih\,iw/dar):(iw-min(iw\,iw*sar))/2:(ih - min (ih\,ih/sar))/2,setsar=sar=1,scale={0}:trunc({0}/dar/2)*2"; // hsbs crop width in half,scale to correct size, set the display aspect,crop out any black bars we may have made the scale width to requestedWidth. Work out the correct height based on the display aspect it will maintain the aspect where -1 in this case (3d) may not. break; case Video3DFormat.FullSideBySide: - filter = "crop=iw/2:ih:0:0,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1,scale={0}:trunc({0}/dar/2)*2"; + filter = @"crop=iw/2:ih:0:0,setdar=dar=a,crop=min(iw\,ih*dar):min(ih\,iw/dar):(iw-min(iw\,iw*sar))/2:(ih - min (ih\,ih/sar))/2,setsar=sar=1,scale={0}:trunc({0}/dar/2)*2"; // fsbs crop width in half,set the display aspect,crop out any black bars we may have made the scale width to requestedWidth. break; case Video3DFormat.HalfTopAndBottom: - filter = "crop=iw:ih/2:0:0,scale=(iw*2):ih),setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1,scale={0}:trunc({0}/dar/2)*2"; + filter = @"crop=iw:ih/2:0:0,scale=(iw*2):ih),setdar=dar=a,crop=min(iw\,ih*dar):min(ih\,iw/dar):(iw-min(iw\,iw*sar))/2:(ih - min (ih\,ih/sar))/2,setsar=sar=1,scale={0}:trunc({0}/dar/2)*2"; // htab crop height in half,scale to correct size, set the display aspect,crop out any black bars we may have made the scale width to requestedWidth break; case Video3DFormat.FullTopAndBottom: - filter = "crop=iw:ih/2:0:0,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1,scale={0}:trunc({0}/dar/2)*2"; + filter = @"crop=iw:ih/2:0:0,setdar=dar=a,crop=min(iw\,ih*dar):min(ih\,iw/dar):(iw-min(iw\,iw*sar))/2:(ih - min (ih\,ih/sar))/2,setsar=sar=1,scale={0}:trunc({0}/dar/2)*2"; // ftab crop height in half, set the display aspect,crop out any black bars we may have made the scale width to requestedWidth break; default: diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 9bd99f030c..0241b77a12 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -680,13 +680,13 @@ namespace MediaBrowser.MediaEncoding.Encoder var scaler = threedFormat switch { // hsbs crop width in half,scale to correct size, set the display aspect,crop out any black bars we may have made. Work out the correct height based on the display aspect it will maintain the aspect where -1 in this case (3d) may not. - Video3DFormat.HalfSideBySide => "crop=iw/2:ih:0:0,scale=(iw*2):ih,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1", + Video3DFormat.HalfSideBySide => @"crop=iw/2:ih:0:0,scale=(iw*2):ih,setdar=dar=a,crop=min(iw\,ih*dar):min(ih\,iw/dar):(iw-min(iw\,iw*sar))/2:(ih - min (ih\,ih/sar))/2,setsar=sar=1", // fsbs crop width in half,set the display aspect,crop out any black bars we may have made - Video3DFormat.FullSideBySide => "crop=iw/2:ih:0:0,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1", + Video3DFormat.FullSideBySide => @"crop=iw/2:ih:0:0,setdar=dar=a,crop=min(iw\,ih*dar):min(ih\,iw/dar):(iw-min(iw\,iw*sar))/2:(ih - min (ih\,ih/sar))/2,setsar=sar=1", // htab crop height in half,scale to correct size, set the display aspect,crop out any black bars we may have made - Video3DFormat.HalfTopAndBottom => "crop=iw:ih/2:0:0,scale=(iw*2):ih),setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1", + Video3DFormat.HalfTopAndBottom => @"crop=iw:ih/2:0:0,scale=(iw*2):ih),setdar=dar=a,crop=min(iw\,ih*dar):min(ih\,iw/dar):(iw-min(iw\,iw*sar))/2:(ih - min (ih\,ih/sar))/2,setsar=sar=1", // ftab crop height in half, set the display aspect,crop out any black bars we may have made - Video3DFormat.FullTopAndBottom => "crop=iw:ih/2:0:0,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1", + Video3DFormat.FullTopAndBottom => @"crop=iw:ih/2:0:0,setdar=dar=a,crop=min(iw\,ih*dar):min(ih\,iw/dar):(iw-min(iw\,iw*sar))/2:(ih - min (ih\,ih/sar))/2,setsar=sar=1", _ => "scale=trunc(iw*sar):ih" }; @@ -858,7 +858,7 @@ namespace MediaBrowser.MediaEncoding.Encoder // https://ffmpeg.org/ffmpeg-filters.html#Notes-on-filtergraph-escaping // We need to double escape - return path.Replace('\\', '/').Replace(":", "\\:", StringComparison.Ordinal).Replace("'", "'\\\\\\''", StringComparison.Ordinal); + return path.Replace('\\', '/').Replace(":", "\\:", StringComparison.Ordinal).Replace("'", @"'\\\''", StringComparison.Ordinal); } /// <inheritdoc /> diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index 59b7c6e279..d817042274 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -58,7 +58,7 @@ namespace MediaBrowser.Providers.MediaInfo _mediaSourceManager = mediaSourceManager; } - [GeneratedRegex("I:\\s+(.*?)\\s+LUFS")] + [GeneratedRegex(@"I:\s+(.*?)\s+LUFS")] private static partial Regex LUFSRegex(); /// <summary> diff --git a/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs b/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs index 7604ddc803..5397f13712 100644 --- a/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs +++ b/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs @@ -13,10 +13,10 @@ namespace Jellyfin.Naming.Tests.TV [InlineData("/media/Foo - S04E011", true, "Foo", 4, 11)] [InlineData("/media/Foo/Foo s01x01", true, "Foo", 1, 1)] [InlineData("/media/Foo (2019)/Season 4/Foo (2019).S04E03", true, "Foo (2019)", 4, 3)] - [InlineData("D:\\media\\Foo\\Foo-S01E01", true, "Foo", 1, 1)] - [InlineData("D:\\media\\Foo - S04E011", true, "Foo", 4, 11)] - [InlineData("D:\\media\\Foo\\Foo s01x01", true, "Foo", 1, 1)] - [InlineData("D:\\media\\Foo (2019)\\Season 4\\Foo (2019).S04E03", true, "Foo (2019)", 4, 3)] + [InlineData(@"D:\media\Foo\Foo-S01E01", true, "Foo", 1, 1)] + [InlineData(@"D:\media\Foo - S04E011", true, "Foo", 4, 11)] + [InlineData(@"D:\media\Foo\Foo s01x01", true, "Foo", 1, 1)] + [InlineData(@"D:\media\Foo (2019)\Season 4\Foo (2019).S04E03", true, "Foo (2019)", 4, 3)] [InlineData("/Season 2/Elementary - 02x03-04-15 - Ep Name.mp4", false, "Elementary", 2, 3)] [InlineData("/Season 1/seriesname S01E02 blah.avi", false, "seriesname", 1, 2)] [InlineData("/Running Man/Running Man S2017E368.mkv", false, "Running Man", 2017, 368)] diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs index c33a957e69..1c35eb3f50 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs @@ -48,10 +48,10 @@ namespace Jellyfin.Server.Implementations.Tests.Library [InlineData("C:/Users/jeff/myfile.mkv", "C:/Users/jeff", "/home/jeff", "/home/jeff/myfile.mkv")] [InlineData("C:/Users/jeff/myfile.mkv", "C:/Users/jeff/", "/home/jeff", "/home/jeff/myfile.mkv")] [InlineData("/home/jeff/music/jeff's band/consistently inconsistent.mp3", "/home/jeff/music/jeff's band", "/home/not jeff", "/home/not jeff/consistently inconsistent.mp3")] - [InlineData("C:\\Users\\jeff\\myfile.mkv", "C:\\Users/jeff", "/home/jeff", "/home/jeff/myfile.mkv")] - [InlineData("C:\\Users\\jeff\\myfile.mkv", "C:\\Users/jeff", "/home/jeff/", "/home/jeff/myfile.mkv")] - [InlineData("C:\\Users\\jeff\\myfile.mkv", "C:\\Users/jeff/", "/home/jeff/", "/home/jeff/myfile.mkv")] - [InlineData("C:\\Users\\jeff\\myfile.mkv", "C:\\Users/jeff/", "/", "/myfile.mkv")] + [InlineData(@"C:\Users\jeff\myfile.mkv", "C:\\Users/jeff", "/home/jeff", "/home/jeff/myfile.mkv")] + [InlineData(@"C:\Users\jeff\myfile.mkv", "C:\\Users/jeff", "/home/jeff/", "/home/jeff/myfile.mkv")] + [InlineData(@"C:\Users\jeff\myfile.mkv", "C:\\Users/jeff/", "/home/jeff/", "/home/jeff/myfile.mkv")] + [InlineData(@"C:\Users\jeff\myfile.mkv", "C:\\Users/jeff/", "/", "/myfile.mkv")] [InlineData("/o", "/o", "/s", "/s")] // regression test for #5977 public void TryReplaceSubPath_ValidArgs_Correct(string path, string subPath, string newSubPath, string? expectedResult) { @@ -78,10 +78,10 @@ namespace Jellyfin.Server.Implementations.Tests.Library [Theory] [InlineData(null, '/', null)] [InlineData(null, '\\', null)] - [InlineData("/home/jeff/myfile.mkv", '\\', "\\home\\jeff\\myfile.mkv")] - [InlineData("C:\\Users\\Jeff\\myfile.mkv", '/', "C:/Users/Jeff/myfile.mkv")] - [InlineData("\\home/jeff\\myfile.mkv", '\\', "\\home\\jeff\\myfile.mkv")] - [InlineData("\\home/jeff\\myfile.mkv", '/', "/home/jeff/myfile.mkv")] + [InlineData("/home/jeff/myfile.mkv", '\\', @"\home\jeff\myfile.mkv")] + [InlineData(@"C:\Users\Jeff\myfile.mkv", '/', "C:/Users/Jeff/myfile.mkv")] + [InlineData(@"\home/jeff\myfile.mkv", '\\', @"\home\jeff\myfile.mkv")] + [InlineData(@"\home/jeff\myfile.mkv", '/', "/home/jeff/myfile.mkv")] [InlineData("", '/', "")] public void NormalizePath_SpecifyingSeparator_Normalizes(string path, char separator, string expectedPath) { @@ -90,8 +90,8 @@ namespace Jellyfin.Server.Implementations.Tests.Library [Theory] [InlineData("/home/jeff/myfile.mkv")] - [InlineData("C:\\Users\\Jeff\\myfile.mkv")] - [InlineData("\\home/jeff\\myfile.mkv")] + [InlineData(@"C:\Users\Jeff\myfile.mkv")] + [InlineData(@"\home/jeff\myfile.mkv")] public void NormalizePath_NoArgs_UsesDirectorySeparatorChar(string path) { var separator = Path.DirectorySeparatorChar; @@ -101,8 +101,8 @@ namespace Jellyfin.Server.Implementations.Tests.Library [Theory] [InlineData("/home/jeff/myfile.mkv", '/')] - [InlineData("C:\\Users\\Jeff\\myfile.mkv", '\\')] - [InlineData("\\home/jeff\\myfile.mkv", '/')] + [InlineData(@"C:\Users\Jeff\myfile.mkv", '\\')] + [InlineData(@"\home/jeff\myfile.mkv", '/')] public void NormalizePath_OutVar_Correct(string path, char expectedSeparator) { var result = path.NormalizePath(out var separator); diff --git a/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs index f2a08e43cf..934024826b 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs @@ -119,8 +119,8 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins [InlineData("C:\\some.dll")] // Windows root path. [InlineData("test.txt")] // Not a DLL [InlineData(".././.././../some.dll")] // Traversal with current and parent - [InlineData("..\\.\\..\\.\\..\\some.dll")] // Windows traversal with current and parent - [InlineData("\\\\network\\resource.dll")] // UNC Path + [InlineData(@"..\.\..\.\..\some.dll")] // Windows traversal with current and parent + [InlineData(@"\\network\resource.dll")] // UNC Path [InlineData("https://jellyfin.org/some.dll")] // URL [InlineData("~/some.dll")] // Tilde poses a shell expansion risk, but is a valid path character. public void Constructor_DiscoversUnsafePluginAssembly_Status_Malfunctioned(string unsafePath) diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs index f56f58c6fe..0a153b9cc1 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs @@ -60,7 +60,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers { Exists = true, FullName = OperatingSystem.IsWindows() ? - "C:\\media\\movies\\Justice League (2017).jpg" + @"C:\media\movies\Justice League (2017).jpg" : "/media/movies/Justice League (2017).jpg" }; directoryService.Setup(x => x.GetFile(_localImageFileMetadata.FullName)) From 2360d28cbb8b2538c371e542b004aeb257711d58 Mon Sep 17 00:00:00 2001 From: Stepan Goremykin <goremukin@gmail.com> Date: Sun, 8 Oct 2023 00:31:46 +0200 Subject: [PATCH 671/858] Fix possible double dispose --- Emby.Server.Implementations/Udp/UdpServer.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs index 832793d29e..bda2c2f31c 100644 --- a/Emby.Server.Implementations/Udp/UdpServer.cs +++ b/Emby.Server.Implementations/Udp/UdpServer.cs @@ -27,9 +27,9 @@ namespace Emby.Server.Implementations.Udp private readonly byte[] _receiveBuffer = new byte[8192]; - private Socket _udpSocket; - private IPEndPoint _endpoint; - private bool _disposed = false; + private readonly Socket _udpSocket; + private readonly IPEndPoint _endpoint; + private bool _disposed; /// <summary> /// Initializes a new instance of the <see cref="UdpServer" /> class. @@ -125,7 +125,8 @@ namespace Emby.Server.Implementations.Udp return; } - _udpSocket?.Dispose(); + _udpSocket.Dispose(); + _disposed = true; } } } From 526f9a825c8205942155759afc5fc1e7a8f6fc6a Mon Sep 17 00:00:00 2001 From: Stepan Goremykin <goremukin@gmail.com> Date: Sun, 8 Oct 2023 00:40:58 +0200 Subject: [PATCH 672/858] Make files readonly --- Emby.Dlna/PlayTo/PlayToManager.cs | 4 ++-- Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs | 6 ++---- .../EntryPoints/UdpServerEntryPoint.cs | 6 +++--- MediaBrowser.Controller/LiveTv/LiveTvProgram.cs | 2 +- .../Net/BasePeriodicWebSocketListener.cs | 2 +- MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs | 2 +- MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs | 2 +- 7 files changed, 11 insertions(+), 13 deletions(-) diff --git a/Emby.Dlna/PlayTo/PlayToManager.cs b/Emby.Dlna/PlayTo/PlayToManager.cs index ef617422c4..b05e0a0957 100644 --- a/Emby.Dlna/PlayTo/PlayToManager.cs +++ b/Emby.Dlna/PlayTo/PlayToManager.cs @@ -39,9 +39,9 @@ namespace Emby.Dlna.PlayTo private readonly IMediaSourceManager _mediaSourceManager; private readonly IMediaEncoder _mediaEncoder; + private readonly SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1); + private readonly CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource(); private bool _disposed; - private SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1); - private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource(); public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClientFactory httpClientFactory, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder) { diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs index 6edfad575a..39524be1d4 100644 --- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs +++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs @@ -10,8 +10,6 @@ namespace Emby.Server.Implementations.AppBase /// </summary> public abstract class BaseApplicationPaths : IApplicationPaths { - private string _dataPath; - /// <summary> /// Initializes a new instance of the <see cref="BaseApplicationPaths"/> class. /// </summary> @@ -33,7 +31,7 @@ namespace Emby.Server.Implementations.AppBase CachePath = cacheDirectoryPath; WebPath = webDirectoryPath; - _dataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName; + DataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName; } /// <summary> @@ -55,7 +53,7 @@ namespace Emby.Server.Implementations.AppBase /// Gets the folder path to the data directory. /// </summary> /// <value>The data directory.</value> - public string DataPath => _dataPath; + public string DataPath { get; } /// <inheritdoc /> public string VirtualDataPath => "%AppDataPath%"; diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs index 54191649da..f810bf170b 100644 --- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs +++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs @@ -40,9 +40,9 @@ namespace Emby.Server.Implementations.EntryPoints /// <summary> /// The UDP server. /// </summary> - private List<UdpServer> _udpServers; - private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); - private bool _disposed = false; + private readonly List<UdpServer> _udpServers; + private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); + private bool _disposed; /// <summary> /// Initializes a new instance of the <see cref="UdpServerEntryPoint" /> class. diff --git a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs index c721fb7785..05540d490f 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs @@ -20,7 +20,7 @@ namespace MediaBrowser.Controller.LiveTv { public class LiveTvProgram : BaseItem, IHasLookupInfo<ItemLookupInfo>, IHasStartDate, IHasProgramAttributes { - private static string EmbyServiceName = "Emby"; + private const string EmbyServiceName = "Emby"; public LiveTvProgram() { diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs index e0942e490b..0a706c307a 100644 --- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs +++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs @@ -34,7 +34,7 @@ namespace MediaBrowser.Controller.Net /// <summary> /// The logger. /// </summary> - protected ILogger<BasePeriodicWebSocketListener<TReturnDataType, TStateType>> Logger; + protected readonly ILogger<BasePeriodicWebSocketListener<TReturnDataType, TStateType>> Logger; protected BasePeriodicWebSocketListener(ILogger<BasePeriodicWebSocketListener<TReturnDataType, TStateType>> logger) { diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs index 9e7a1d50a3..1f94d9b23e 100644 --- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs +++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs @@ -8,7 +8,7 @@ namespace MediaBrowser.MediaEncoding.BdInfo; /// </summary> public class BdInfoFileInfo : BDInfo.IO.IFileInfo { - private FileSystemMetadata _impl; + private readonly FileSystemMetadata _impl; /// <summary> /// Initializes a new instance of the <see cref="BdInfoFileInfo" /> class. diff --git a/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs index af581fc5df..9b4e1731d1 100644 --- a/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs +++ b/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs @@ -13,7 +13,7 @@ namespace MediaBrowser.XbmcMetadata.Providers public abstract class BaseNfoProvider<T> : ILocalMetadataProvider<T>, IHasItemChangeMonitor where T : BaseItem, new() { - private IFileSystem _fileSystem; + private readonly IFileSystem _fileSystem; protected BaseNfoProvider(IFileSystem fileSystem) { From 47254d6a2236e079af3cc8c2e37c77d9d1479235 Mon Sep 17 00:00:00 2001 From: Stepan Goremykin <goremukin@gmail.com> Date: Sun, 8 Oct 2023 00:46:15 +0200 Subject: [PATCH 673/858] Remove conditional access when it is known to be not null --- Emby.Server.Implementations/Net/SocketFactory.cs | 6 +++--- Jellyfin.Api/Controllers/LibraryController.cs | 6 +++--- MediaBrowser.Model/Dlna/StreamBuilder.cs | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Emby.Server.Implementations/Net/SocketFactory.cs b/Emby.Server.Implementations/Net/SocketFactory.cs index 3091398b7b..8478fbc4ac 100644 --- a/Emby.Server.Implementations/Net/SocketFactory.cs +++ b/Emby.Server.Implementations/Net/SocketFactory.cs @@ -29,7 +29,7 @@ namespace Emby.Server.Implementations.Net } catch { - socket?.Dispose(); + socket.Dispose(); throw; } @@ -55,7 +55,7 @@ namespace Emby.Server.Implementations.Net } catch { - socket?.Dispose(); + socket.Dispose(); throw; } @@ -97,7 +97,7 @@ namespace Emby.Server.Implementations.Net } catch { - socket?.Dispose(); + socket.Dispose(); throw; } diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index 46c0a8d527..21941ff942 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -294,8 +294,8 @@ public class LibraryController : BaseJellyfinApiController return new AllThemeMediaResult { - ThemeSongsResult = themeSongs?.Value, - ThemeVideosResult = themeVideos?.Value, + ThemeSongsResult = themeSongs.Value, + ThemeVideosResult = themeVideos.Value, SoundtrackSongsResult = new ThemeMediaResult() }; } @@ -490,7 +490,7 @@ public class LibraryController : BaseJellyfinApiController baseItemDtos.Add(_dtoService.GetBaseItemDto(parent, dtoOptions, user)); - parent = parent?.GetParent(); + parent = parent.GetParent(); } return baseItemDtos; diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 889e2494a0..666e787951 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -1313,7 +1313,7 @@ namespace MediaBrowser.Model.Dlna var audioFailureConditions = GetProfileConditionsForVideoAudio(profile.CodecProfiles, container, audioStream.Codec, audioStream.Channels, audioStream.BitRate, audioStream.SampleRate, audioStream.BitDepth, audioStream.Profile, mediaSource.IsSecondaryAudio(audioStream)); var audioStreamFailureReasons = AggregateFailureConditions(mediaSource, profile, "VideoAudioCodecProfile", audioFailureConditions); - if (audioStream?.IsExternal == true) + if (audioStream.IsExternal == true) { audioStreamFailureReasons |= TranscodeReason.AudioIsExternal; } From 963d8e66a2253dd421925b56497581e93ab07aff Mon Sep 17 00:00:00 2001 From: Stepan Goremykin <goremukin@gmail.com> Date: Sun, 8 Oct 2023 00:49:18 +0200 Subject: [PATCH 674/858] Remove redundant type specification --- Jellyfin.Networking/Extensions/NetworkExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Networking/Extensions/NetworkExtensions.cs b/Jellyfin.Networking/Extensions/NetworkExtensions.cs index e45fa3bcb7..910a33c0f0 100644 --- a/Jellyfin.Networking/Extensions/NetworkExtensions.cs +++ b/Jellyfin.Networking/Extensions/NetworkExtensions.cs @@ -204,7 +204,7 @@ public static partial class NetworkExtensions { var ipBlock = splitString.Current; var address = IPAddress.None; - if (negated && ipBlock.StartsWith<char>("!") && IPAddress.TryParse(ipBlock[1..], out var tmpAddress)) + if (negated && ipBlock.StartsWith("!") && IPAddress.TryParse(ipBlock[1..], out var tmpAddress)) { address = tmpAddress; } From f84469d50029801003e76cfe8a156d559182432d Mon Sep 17 00:00:00 2001 From: Stepan Goremykin <goremukin@gmail.com> Date: Sun, 8 Oct 2023 00:50:02 +0200 Subject: [PATCH 675/858] Remove redundant using directives --- Emby.Server.Implementations/HttpServer/WebSocketConnection.cs | 1 - Emby.Server.Implementations/Library/LibraryManager.cs | 1 - Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs | 1 - Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs | 1 - .../LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs | 1 - Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs | 2 -- Jellyfin.Networking/Extensions/NetworkExtensions.cs | 1 - .../Events/Consumers/Security/AuthenticationSucceededLogger.cs | 1 - .../Events/EventingServiceCollectionExtensions.cs | 3 +-- Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs | 1 - Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs | 1 - Jellyfin.Server/Startup.cs | 1 - MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs | 1 - MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs | 1 - MediaBrowser.Controller/Providers/IProviderManager.cs | 1 - MediaBrowser.Providers/Manager/MetadataService.cs | 1 - MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs | 1 - 17 files changed, 1 insertion(+), 19 deletions(-) diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs index 7f620d666d..f83da566b2 100644 --- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs +++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs @@ -12,7 +12,6 @@ using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net.WebSocketMessages; using MediaBrowser.Controller.Net.WebSocketMessages.Outbound; using MediaBrowser.Model.Session; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.HttpServer diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 9de36d73cd..6e6c14a51a 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -46,7 +46,6 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.Library; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Tasks; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Episode = MediaBrowser.Controller.Entities.TV.Episode; using EpisodeInfo = Emby.Naming.TV.EpisodeInfo; diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs index b77c6b204b..c860391fc2 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.IO; using System.Linq; using Emby.Naming.Common; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs index 1721be9e23..ff25ee5854 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs @@ -17,7 +17,6 @@ using MediaBrowser.Controller.LiveTv; using MediaBrowser.Model.Dto; using MediaBrowser.Model.IO; using MediaBrowser.Model.LiveTv; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.LiveTv.TunerHosts diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index 7e588f6812..04b0cb0177 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -27,7 +27,6 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Net; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index 613ea117f4..db5e81df5f 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.IO; using System.Linq; using System.Net.Http; using System.Threading; @@ -22,7 +21,6 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.MediaInfo; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; diff --git a/Jellyfin.Networking/Extensions/NetworkExtensions.cs b/Jellyfin.Networking/Extensions/NetworkExtensions.cs index 910a33c0f0..1b5afee557 100644 --- a/Jellyfin.Networking/Extensions/NetworkExtensions.cs +++ b/Jellyfin.Networking/Extensions/NetworkExtensions.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Net; using System.Net.Sockets; using System.Text.RegularExpressions; diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs index 2ee5b4e88d..3f3a0dec5e 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs @@ -1,7 +1,6 @@ using System.Globalization; using System.Threading.Tasks; using Jellyfin.Data.Entities; -using Jellyfin.Data.Events; using MediaBrowser.Controller.Events; using MediaBrowser.Controller.Events.Authentication; using MediaBrowser.Model.Activity; diff --git a/Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs b/Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs index 9a473de52d..9626817e90 100644 --- a/Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs +++ b/Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs @@ -1,5 +1,4 @@ -using Jellyfin.Data.Events; -using Jellyfin.Data.Events.System; +using Jellyfin.Data.Events.System; using Jellyfin.Data.Events.Users; using Jellyfin.Server.Implementations.Events.Consumers.Library; using Jellyfin.Server.Implementations.Events.Consumers.Security; diff --git a/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs b/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs index 3cb791b571..1664a751de 100644 --- a/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs @@ -3,7 +3,6 @@ using System.IO; using System.Net; using Jellyfin.Server.Helpers; using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Net; using MediaBrowser.Controller.Extensions; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; diff --git a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs index e1a43bb489..ac50474010 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs @@ -3,7 +3,6 @@ using System.Globalization; using System.IO; using Emby.Server.Implementations.Data; using MediaBrowser.Controller; -using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Globalization; using Microsoft.Data.Sqlite; using Microsoft.Extensions.Logging; diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index b759b6bca5..1393f76aab 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -27,7 +27,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting; -using Microsoft.VisualBasic; using Prometheus; namespace Jellyfin.Server diff --git a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs index b263c173eb..6acab13fe0 100644 --- a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs +++ b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using Jellyfin.Extensions; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; diff --git a/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs b/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs index ac20120d97..975218ad75 100644 --- a/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs +++ b/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs @@ -1,4 +1,3 @@ -using System.Threading; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Configuration; diff --git a/MediaBrowser.Controller/Providers/IProviderManager.cs b/MediaBrowser.Controller/Providers/IProviderManager.cs index 16943f6aaa..eb5069b062 100644 --- a/MediaBrowser.Controller/Providers/IProviderManager.cs +++ b/MediaBrowser.Controller/Providers/IProviderManager.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Events; diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index 75291b3178..e336c8825e 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -12,7 +12,6 @@ using Jellyfin.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; -using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; diff --git a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs index f22b861eba..f4b4eccf88 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Globalization; using System.Xml; using MediaBrowser.Common.Configuration; From 160855ffe9aed4f2c379646081bbe1ac642fab70 Mon Sep 17 00:00:00 2001 From: Stepan Goremykin <goremukin@gmail.com> Date: Sun, 8 Oct 2023 00:52:47 +0200 Subject: [PATCH 676/858] Use switch expression --- .../Middleware/ExceptionMiddleware.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Jellyfin.Api/Middleware/ExceptionMiddleware.cs b/Jellyfin.Api/Middleware/ExceptionMiddleware.cs index 060c14f89d..acbb4877d4 100644 --- a/Jellyfin.Api/Middleware/ExceptionMiddleware.cs +++ b/Jellyfin.Api/Middleware/ExceptionMiddleware.cs @@ -122,17 +122,17 @@ public class ExceptionMiddleware private static int GetStatusCode(Exception ex) { - switch (ex) + return ex switch { - case ArgumentException _: return StatusCodes.Status400BadRequest; - case AuthenticationException _: return StatusCodes.Status401Unauthorized; - case SecurityException _: return StatusCodes.Status403Forbidden; - case DirectoryNotFoundException _: - case FileNotFoundException _: - case ResourceNotFoundException _: return StatusCodes.Status404NotFound; - case MethodNotAllowedException _: return StatusCodes.Status405MethodNotAllowed; - default: return StatusCodes.Status500InternalServerError; - } + ArgumentException => StatusCodes.Status400BadRequest, + AuthenticationException => StatusCodes.Status401Unauthorized, + SecurityException => StatusCodes.Status403Forbidden, + DirectoryNotFoundException => StatusCodes.Status404NotFound, + FileNotFoundException => StatusCodes.Status404NotFound, + ResourceNotFoundException => StatusCodes.Status404NotFound, + MethodNotAllowedException => StatusCodes.Status405MethodNotAllowed, + _ => StatusCodes.Status500InternalServerError + }; } private string NormalizeExceptionMessage(string msg) From 6512f85ccb6081d41cd4fc9434a936df67a5f03a Mon Sep 17 00:00:00 2001 From: Stepan Goremykin <goremukin@gmail.com> Date: Sun, 8 Oct 2023 00:55:14 +0200 Subject: [PATCH 677/858] Pass cancellation token --- MediaBrowser.Controller/Entities/Folder.cs | 2 +- RSSDP/SsdpDevicePublisher.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 44fe65103e..e707eedbf6 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -598,7 +598,7 @@ namespace MediaBrowser.Controller.Entities for (var i = 0; i < childrenCount; i++) { - await actionBlock.SendAsync(i).ConfigureAwait(false); + await actionBlock.SendAsync(i, cancellationToken).ConfigureAwait(false); } actionBlock.Complete(); diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs index 950e6fec86..d4a70a9752 100644 --- a/RSSDP/SsdpDevicePublisher.cs +++ b/RSSDP/SsdpDevicePublisher.cs @@ -243,7 +243,7 @@ namespace Rssdp.Infrastructure } // Do not block synchronously as that may tie up a threadpool thread for several seconds. - Task.Delay(_Random.Next(16, (maxWaitInterval * 1000))).ContinueWith((parentTask) => + Task.Delay(_Random.Next(16, (maxWaitInterval * 1000)), cancellationToken).ContinueWith((parentTask) => { // Copying devices to local array here to avoid threading issues/enumerator exceptions. IEnumerable<SsdpDevice> devices = null; @@ -281,7 +281,7 @@ namespace Rssdp.Infrastructure } } } - }); + }, cancellationToken); } private IEnumerable<SsdpDevice> GetAllDevicesAsFlatEnumerable() From cc590f82b9fd29ecbc500cecc335411a4e8f6ec7 Mon Sep 17 00:00:00 2001 From: Pithaya <19533412+Pithaya@users.noreply.github.com> Date: Sun, 8 Oct 2023 01:08:03 +0200 Subject: [PATCH 678/858] Allow people on books --- MediaBrowser.Controller/Entities/Book.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/MediaBrowser.Controller/Entities/Book.cs b/MediaBrowser.Controller/Entities/Book.cs index d75beb06da..519dfa82aa 100644 --- a/MediaBrowser.Controller/Entities/Book.cs +++ b/MediaBrowser.Controller/Entities/Book.cs @@ -24,6 +24,9 @@ namespace MediaBrowser.Controller.Entities public override bool SupportsPositionTicksResume => true; + [JsonIgnore] + public override bool SupportsPeople => true; + [JsonIgnore] public string SeriesPresentationUniqueKey { get; set; } From e6bb86e64955140c66e2b5b6547fddc5e04f8be0 Mon Sep 17 00:00:00 2001 From: Pithaya <19533412+Pithaya@users.noreply.github.com> Date: Sun, 8 Oct 2023 01:08:57 +0200 Subject: [PATCH 679/858] Add a Book ExternalIdMediaType --- MediaBrowser.Model/Providers/ExternalIdMediaType.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Model/Providers/ExternalIdMediaType.cs b/MediaBrowser.Model/Providers/ExternalIdMediaType.cs index 5303c8f58b..ef518369cc 100644 --- a/MediaBrowser.Model/Providers/ExternalIdMediaType.cs +++ b/MediaBrowser.Model/Providers/ExternalIdMediaType.cs @@ -66,6 +66,11 @@ namespace MediaBrowser.Model.Providers /// <summary> /// A music track. /// </summary> - Track = 12 + Track = 12, + + /// <summary> + /// A book. + /// </summary> + Book = 13 } } From 948a67cfeb1aa045099c4486da4eb1fd459a676f Mon Sep 17 00:00:00 2001 From: Pithaya <19533412+Pithaya@users.noreply.github.com> Date: Sun, 8 Oct 2023 01:11:08 +0200 Subject: [PATCH 680/858] Update CONTRIBUTORS.md --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index e3af12a497..dfdfabd189 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -168,6 +168,7 @@ - [TheTyrius](https://github.com/TheTyrius) - [tallbl0nde](https://github.com/tallbl0nde) - [sleepycatcoding](https://github.com/sleepycatcoding) + - [Pithaya](https://github.com/Pithaya) # Emby Contributors From 3259d484ffe22797cd76c680d7ed527e50800702 Mon Sep 17 00:00:00 2001 From: Stepan Goremykin <goremukin@gmail.com> Date: Sun, 8 Oct 2023 01:16:00 +0200 Subject: [PATCH 681/858] Use generated regex --- .../Encoder/EncoderValidator.cs | 14 ++++++++++---- .../Probing/ProbeResultNormalizer.cs | 9 +++++---- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index db119ce5c5..f12ef7e634 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -499,8 +499,8 @@ namespace MediaBrowser.MediaEncoding.Encoder var required = codec == Codec.Encoder ? _requiredEncoders : _requiredDecoders; - var found = Regex - .Matches(output, @"^\s\S{6}\s(?<codec>[\w|-]+)\s+.+$", RegexOptions.Multiline) + var found = CodecRegex() + .Matches(output) .Select(x => x.Groups["codec"].Value) .Where(x => required.Contains(x)); @@ -527,8 +527,8 @@ namespace MediaBrowser.MediaEncoding.Encoder return Enumerable.Empty<string>(); } - var found = Regex - .Matches(output, @"^\s\S{3}\s(?<filter>[\w|-]+)\s+.+$", RegexOptions.Multiline) + var found = FilterRegex() + .Matches(output) .Select(x => x.Groups["filter"].Value) .Where(x => _requiredFilters.Contains(x)); @@ -582,5 +582,11 @@ namespace MediaBrowser.MediaEncoding.Encoder return reader.ReadToEnd(); } } + + [GeneratedRegex("^\\s\\S{6}\\s(?<codec>[\\w|-]+)\\s+.+$", RegexOptions.Multiline)] + private static partial Regex CodecRegex(); + + [GeneratedRegex("^\\s\\S{3}\\s(?<filter>[\\w|-]+)\\s+.+$", RegexOptions.Multiline)] + private static partial Regex FilterRegex(); } } diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 441a3abd46..be1e8a1726 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -22,7 +22,7 @@ namespace MediaBrowser.MediaEncoding.Probing /// <summary> /// Class responsible for normalizing FFprobe output. /// </summary> - public class ProbeResultNormalizer + public partial class ProbeResultNormalizer { // When extracting subtitles, the maximum length to consider (to avoid invalid filenames) private const int MaxSubtitleDescriptionExtractionLength = 100; @@ -31,8 +31,6 @@ namespace MediaBrowser.MediaEncoding.Probing private readonly char[] _nameDelimiters = { '/', '|', ';', '\\' }; - private static readonly Regex _performerPattern = new(@"(?<name>.*) \((?<instrument>.*)\)"); - private readonly ILogger _logger; private readonly ILocalizationManager _localization; @@ -1215,7 +1213,7 @@ namespace MediaBrowser.MediaEncoding.Probing { foreach (var person in Split(performer, false)) { - Match match = _performerPattern.Match(person); + Match match = PerformerRegex().Match(person); // If the performer doesn't have any instrument/role associated, it won't match. In that case, chances are it's simply a band name, so we skip it. if (match.Success) @@ -1654,5 +1652,8 @@ namespace MediaBrowser.MediaEncoding.Probing return TransportStreamTimestamp.Valid; } + + [GeneratedRegex("(?<name>.*) \\((?<instrument>.*)\\)")] + private static partial Regex PerformerRegex(); } } From 2d7835c848cecd11b3fa29f0d1df1d73001a33ac Mon Sep 17 00:00:00 2001 From: Stepan Goremykin <goremukin@gmail.com> Date: Sun, 8 Oct 2023 01:16:43 +0200 Subject: [PATCH 682/858] Join declaration and assignment --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 4 +--- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 03c778c07f..8d51c800f5 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -1745,11 +1745,9 @@ namespace MediaBrowser.Controller.MediaEncoding // Values 0-3, 0 being highest quality but slower var profileScore = 0; - string crf; var qmin = "0"; var qmax = "50"; - - crf = "10"; + var crf = "10"; if (isVc1) { diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 0241b77a12..ef4c9608ce 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -625,9 +625,7 @@ namespace MediaBrowser.MediaEncoding.Encoder private string GetImageResolutionParameter() { - string imageResolutionParameter; - - imageResolutionParameter = _serverConfig.Configuration.ChapterImageResolution switch + var imageResolutionParameter = _serverConfig.Configuration.ChapterImageResolution switch { ImageResolution.P144 => "256x144", ImageResolution.P240 => "426x240", From 212976277d43a1d9a4186c6536e4a8cb3eefd639 Mon Sep 17 00:00:00 2001 From: Stepan Goremykin <goremukin@gmail.com> Date: Sun, 8 Oct 2023 01:17:32 +0200 Subject: [PATCH 683/858] Remove redundant ToString call for value types --- Emby.Server.Implementations/Data/SqliteItemRepository.cs | 2 +- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 77cf4089be..82d1d0b4e3 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -4382,7 +4382,7 @@ namespace Emby.Server.Implementations.Data foreach (var videoType in query.VideoTypes) { - videoTypes.Add("data like '%\"VideoType\":\"" + videoType.ToString() + "\"%'"); + videoTypes.Add("data like '%\"VideoType\":\"" + videoType + "\"%'"); } whereClauses.Add("(" + string.Join(" OR ", videoTypes) + ")"); diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index ef4c9608ce..07460aaa3c 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -424,7 +424,7 @@ namespace MediaBrowser.MediaEncoding.Encoder if (request.MediaSource.AnalyzeDurationMs > 0) { - analyzeDuration = "-analyzeduration " + (request.MediaSource.AnalyzeDurationMs * 1000).ToString(); + analyzeDuration = "-analyzeduration " + (request.MediaSource.AnalyzeDurationMs * 1000); } else if (!string.IsNullOrEmpty(ffmpegAnalyzeDuration)) { From fdef9356b9ba483e437fbc3a2bc0b6aaf3c05c29 Mon Sep 17 00:00:00 2001 From: Stepan Goremykin <goremukin@gmail.com> Date: Sun, 8 Oct 2023 01:25:37 +0200 Subject: [PATCH 684/858] Use null propagation --- Emby.Dlna/PlayTo/Device.cs | 11 ++++------- .../Data/SqliteItemRepository.cs | 5 +---- .../Entities/BaseItemExtensions.cs | 5 +---- RSSDP/HttpRequestParser.cs | 5 +---- RSSDP/HttpResponseParser.cs | 5 +---- RSSDP/SsdpCommunicationsServer.cs | 14 ++++---------- RSSDP/SsdpDevice.cs | 10 ++-------- RSSDP/SsdpDeviceLocator.cs | 14 ++++---------- RSSDP/SsdpDevicePublisher.cs | 10 ++-------- 9 files changed, 20 insertions(+), 59 deletions(-) diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs index d21cc69132..8dc2184415 100644 --- a/Emby.Dlna/PlayTo/Device.cs +++ b/Emby.Dlna/PlayTo/Device.cs @@ -927,14 +927,11 @@ namespace Emby.Dlna.PlayTo var resElement = container.Element(UPnpNamespaces.Res); - if (resElement is not null) - { - var info = resElement.Attribute(UPnpNamespaces.ProtocolInfo); + var info = resElement?.Attribute(UPnpNamespaces.ProtocolInfo); - if (info is not null && !string.IsNullOrWhiteSpace(info.Value)) - { - return info.Value.Split(':'); - } + if (info is not null && !string.IsNullOrWhiteSpace(info.Value)) + { + return info.Value.Split(':'); } return new string[4]; diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 82d1d0b4e3..e519364c22 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -3540,10 +3540,7 @@ namespace Emby.Server.Implementations.Data .Append(paramName) .Append("))) OR "); - if (statement is not null) - { - statement.TryBind(paramName, query.PersonIds[i]); - } + statement?.TryBind(paramName, query.PersonIds[i]); } clauseBuilder.Length -= Or.Length; diff --git a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs index 615d236c73..dcd22a3b41 100644 --- a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs +++ b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs @@ -95,10 +95,7 @@ namespace MediaBrowser.Controller.Entities } var p = destProps.Find(x => x.Name == sourceProp.Name); - if (p is not null) - { - p.SetValue(dest, v); - } + p?.SetValue(dest, v); } } diff --git a/RSSDP/HttpRequestParser.cs b/RSSDP/HttpRequestParser.cs index a1b4627a93..fab70eae2c 100644 --- a/RSSDP/HttpRequestParser.cs +++ b/RSSDP/HttpRequestParser.cs @@ -33,10 +33,7 @@ namespace Rssdp.Infrastructure } finally { - if (retVal != null) - { - retVal.Dispose(); - } + retVal?.Dispose(); } } diff --git a/RSSDP/HttpResponseParser.cs b/RSSDP/HttpResponseParser.cs index 71b7a7b990..c570c84cbb 100644 --- a/RSSDP/HttpResponseParser.cs +++ b/RSSDP/HttpResponseParser.cs @@ -33,10 +33,7 @@ namespace Rssdp.Infrastructure } catch { - if (retVal != null) - { - retVal.Dispose(); - } + retVal?.Dispose(); throw; } diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs index 001d2a55ec..768fad5ec6 100644 --- a/RSSDP/SsdpCommunicationsServer.cs +++ b/RSSDP/SsdpCommunicationsServer.cs @@ -512,22 +512,16 @@ namespace Rssdp.Infrastructure } var handlers = this.RequestReceived; - if (handlers is not null) - { - handlers(this, new RequestReceivedEventArgs(data, remoteEndPoint, receivedOnlocalIPAddress)); - } + handlers?.Invoke(this, new RequestReceivedEventArgs(data, remoteEndPoint, receivedOnlocalIPAddress)); } private void OnResponseReceived(HttpResponseMessage data, IPEndPoint endPoint, IPAddress localIPAddress) { var handlers = this.ResponseReceived; - if (handlers is not null) + handlers?.Invoke(this, new ResponseReceivedEventArgs(data, endPoint) { - handlers(this, new ResponseReceivedEventArgs(data, endPoint) - { - LocalIPAddress = localIPAddress - }); - } + LocalIPAddress = localIPAddress + }); } } } diff --git a/RSSDP/SsdpDevice.cs b/RSSDP/SsdpDevice.cs index 3e4261b6a9..569d733ea0 100644 --- a/RSSDP/SsdpDevice.cs +++ b/RSSDP/SsdpDevice.cs @@ -337,10 +337,7 @@ namespace Rssdp protected virtual void OnDeviceAdded(SsdpEmbeddedDevice device) { var handlers = this.DeviceAdded; - if (handlers != null) - { - handlers(this, new DeviceEventArgs(device)); - } + handlers?.Invoke(this, new DeviceEventArgs(device)); } /// <summary> @@ -352,10 +349,7 @@ namespace Rssdp protected virtual void OnDeviceRemoved(SsdpEmbeddedDevice device) { var handlers = this.DeviceRemoved; - if (handlers != null) - { - handlers(this, new DeviceEventArgs(device)); - } + handlers?.Invoke(this, new DeviceEventArgs(device)); } } } diff --git a/RSSDP/SsdpDeviceLocator.cs b/RSSDP/SsdpDeviceLocator.cs index 1afb86de40..78784f4ec2 100644 --- a/RSSDP/SsdpDeviceLocator.cs +++ b/RSSDP/SsdpDeviceLocator.cs @@ -227,13 +227,10 @@ namespace Rssdp.Infrastructure } var handlers = this.DeviceAvailable; - if (handlers is not null) + handlers?.Invoke(this, new DeviceAvailableEventArgs(device, isNewDevice) { - handlers(this, new DeviceAvailableEventArgs(device, isNewDevice) - { - RemoteIPAddress = IPAddress - }); - } + RemoteIPAddress = IPAddress + }); } /// <summary> @@ -250,10 +247,7 @@ namespace Rssdp.Infrastructure } var handlers = this.DeviceUnavailable; - if (handlers is not null) - { - handlers(this, new DeviceUnavailableEventArgs(device, expired)); - } + handlers?.Invoke(this, new DeviceUnavailableEventArgs(device, expired)); } /// <summary> diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs index d4a70a9752..cdc73d5527 100644 --- a/RSSDP/SsdpDevicePublisher.cs +++ b/RSSDP/SsdpDevicePublisher.cs @@ -527,10 +527,7 @@ namespace Rssdp.Infrastructure { var timer = _RebroadcastAliveNotificationsTimer; _RebroadcastAliveNotificationsTimer = null; - if (timer is not null) - { - timer.Dispose(); - } + timer?.Dispose(); } private TimeSpan GetMinimumNonZeroCacheLifetime() @@ -564,10 +561,7 @@ namespace Rssdp.Infrastructure private void WriteTrace(string text) { - if (LogFunction is not null) - { - LogFunction(text); - } + LogFunction?.Invoke(text); // System.Diagnostics.Debug.WriteLine(text, "SSDP Publisher"); } From 96c3bde3463ad0457d894ed532093ed28e868ba8 Mon Sep 17 00:00:00 2001 From: Stepan Goremykin <goremukin@gmail.com> Date: Sun, 8 Oct 2023 01:26:57 +0200 Subject: [PATCH 685/858] Remove redundant nullable directive --- Emby.Dlna/PlayTo/Device.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs index 8dc2184415..bb9b8b0fdc 100644 --- a/Emby.Dlna/PlayTo/Device.cs +++ b/Emby.Dlna/PlayTo/Device.cs @@ -1136,7 +1136,6 @@ namespace Emby.Dlna.PlayTo return new Device(deviceProperties, httpClientFactory, logger); } -#nullable enable private static DeviceIcon CreateIcon(XElement element) { ArgumentNullException.ThrowIfNull(element); From 994619afb2287b0ee80eab2511393a7803a3ddb6 Mon Sep 17 00:00:00 2001 From: herby2212 <12448284+herby2212@users.noreply.github.com> Date: Sun, 8 Oct 2023 13:49:35 +0200 Subject: [PATCH 686/858] fix formatting for build process --- Emby.Server.Implementations/Session/SessionManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index b5d8f6c3b7..329138feb8 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1873,7 +1873,7 @@ namespace Emby.Server.Implementations.Session _idleTimer = null; } - if(_inactiveTimer is not null) + if (_inactiveTimer is not null) { await _inactiveTimer.DisposeAsync().ConfigureAwait(false); _inactiveTimer = null; From 7fc804e4b85a851a6b78c08602b1e1f3fb35b93f Mon Sep 17 00:00:00 2001 From: Joe Rogers <1337joe@users.noreply.github.com> Date: Sun, 8 Oct 2023 14:05:55 +0200 Subject: [PATCH 687/858] Add full version tag for renovate (#10370) --- .github/workflows/repo-stale.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/repo-stale.yaml b/.github/workflows/repo-stale.yaml index 4eb0cf0996..2b11641166 100644 --- a/.github/workflows/repo-stale.yaml +++ b/.github/workflows/repo-stale.yaml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest if: ${{ contains(github.repository, 'jellyfin/') }} steps: - - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8 + - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0 with: repo-token: ${{ secrets.JF_BOT_TOKEN }} days-before-stale: 120 From c707baed8366cd44ea80713cf93e5f92971815bf Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Thu, 5 Oct 2023 23:57:11 +0200 Subject: [PATCH 688/858] Jellyfin.Drawing minor improvements Reduce duplicate/dead code --- .../Drawing/IImageProcessor.cs | 11 ---- .../Drawing/ImageProcessingOptions.cs | 3 +- .../Encoder/MediaEncoder.cs | 10 +--- .../Drawing/ImageFormatExtensions.cs | 17 ++++++ .../MediaInfo/EmbeddedImageProvider.cs | 6 -- src/Jellyfin.Drawing.Skia/SkiaEncoder.cs | 14 +---- src/Jellyfin.Drawing/ImageProcessor.cs | 56 +------------------ .../Drawing/ImageFormatExtensionsTests.cs | 13 +++++ 8 files changed, 37 insertions(+), 93 deletions(-) diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs index cdc3d52b9b..0d1e2a5a07 100644 --- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs +++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; -using System.IO; using System.Threading.Tasks; using Jellyfin.Data.Entities; using MediaBrowser.Controller.Entities; @@ -70,14 +69,6 @@ namespace MediaBrowser.Controller.Drawing string? GetImageCacheTag(User user); - /// <summary> - /// Processes the image. - /// </summary> - /// <param name="options">The options.</param> - /// <param name="toStream">To stream.</param> - /// <returns>Task.</returns> - Task ProcessImage(ImageProcessingOptions options, Stream toStream); - /// <summary> /// Processes the image. /// </summary> @@ -97,7 +88,5 @@ namespace MediaBrowser.Controller.Drawing /// <param name="options">The options.</param> /// <param name="libraryName">The library name to draw onto the collage.</param> void CreateImageCollage(ImageCollageOptions options, string? libraryName); - - bool SupportsTransparency(string path); } } diff --git a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs index 7912c5e87e..953cfe698e 100644 --- a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs +++ b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs @@ -119,7 +119,8 @@ namespace MediaBrowser.Controller.Drawing private bool IsFormatSupported(string originalImagePath) { var ext = Path.GetExtension(originalImagePath); - return SupportedOutputFormats.Any(outputFormat => string.Equals(ext, "." + outputFormat, StringComparison.OrdinalIgnoreCase)); + ext = ext.Replace(".jpeg", ".jpg", StringComparison.OrdinalIgnoreCase); + return SupportedOutputFormats.Any(outputFormat => string.Equals(ext, outputFormat.GetExtension(), StringComparison.OrdinalIgnoreCase)); } } } diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 4bff196658..26f47a18f6 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -650,15 +650,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { ArgumentException.ThrowIfNullOrEmpty(inputPath); - var outputExtension = targetFormat switch - { - ImageFormat.Bmp => ".bmp", - ImageFormat.Gif => ".gif", - ImageFormat.Jpg => ".jpg", - ImageFormat.Png => ".png", - ImageFormat.Webp => ".webp", - _ => ".jpg" - }; + var outputExtension = targetFormat?.GetExtension() ?? ".jpg"; var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + outputExtension); Directory.CreateDirectory(Path.GetDirectoryName(tempExtractPath)); diff --git a/MediaBrowser.Model/Drawing/ImageFormatExtensions.cs b/MediaBrowser.Model/Drawing/ImageFormatExtensions.cs index 68a5c25345..1bb24112ec 100644 --- a/MediaBrowser.Model/Drawing/ImageFormatExtensions.cs +++ b/MediaBrowser.Model/Drawing/ImageFormatExtensions.cs @@ -24,4 +24,21 @@ public static class ImageFormatExtensions ImageFormat.Webp => "image/webp", _ => throw new InvalidEnumArgumentException(nameof(format), (int)format, typeof(ImageFormat)) }; + + /// <summary> + /// Returns the correct extension for this <see cref="ImageFormat" />. + /// </summary> + /// <param name="format">This <see cref="ImageFormat" />.</param> + /// <exception cref="InvalidEnumArgumentException">The <paramref name="format"/> is an invalid enumeration value.</exception> + /// <returns>The correct extension for this <see cref="ImageFormat" />.</returns> + public static string GetExtension(this ImageFormat format) + => format switch + { + ImageFormat.Bmp => ".bmp", + ImageFormat.Gif => ".gif", + ImageFormat.Jpg => ".jpg", + ImageFormat.Png => ".png", + ImageFormat.Webp => ".webp", + _ => throw new InvalidEnumArgumentException(nameof(format), (int)format, typeof(ImageFormat)) + }; } diff --git a/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs index c24f4e2fcc..0bfee07fd6 100644 --- a/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs @@ -204,16 +204,10 @@ namespace MediaBrowser.Providers.MediaInfo ? Path.GetExtension(attachmentStream.FileName) : MimeTypes.ToExtension(attachmentStream.MimeType); - if (string.IsNullOrEmpty(extension)) - { - extension = ".jpg"; - } - ImageFormat format = extension switch { ".bmp" => ImageFormat.Bmp, ".gif" => ImageFormat.Gif, - ".jpg" => ImageFormat.Jpg, ".png" => ImageFormat.Png, ".webp" => ImageFormat.Webp, _ => ImageFormat.Jpg diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 416f602179..03f90da8eb 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -200,20 +200,10 @@ public class SkiaEncoder : IImageEncoder { if (!orientation.HasValue) { - return SKEncodedOrigin.TopLeft; + return SKEncodedOrigin.Default; } - return orientation.Value switch - { - ImageOrientation.TopRight => SKEncodedOrigin.TopRight, - ImageOrientation.RightTop => SKEncodedOrigin.RightTop, - ImageOrientation.RightBottom => SKEncodedOrigin.RightBottom, - ImageOrientation.LeftTop => SKEncodedOrigin.LeftTop, - ImageOrientation.LeftBottom => SKEncodedOrigin.LeftBottom, - ImageOrientation.BottomRight => SKEncodedOrigin.BottomRight, - ImageOrientation.BottomLeft => SKEncodedOrigin.BottomLeft, - _ => SKEncodedOrigin.TopLeft - }; + return (SKEncodedOrigin)orientation.Value; } /// <summary> diff --git a/src/Jellyfin.Drawing/ImageProcessor.cs b/src/Jellyfin.Drawing/ImageProcessor.cs index 4f16e294bc..65a8f4e832 100644 --- a/src/Jellyfin.Drawing/ImageProcessor.cs +++ b/src/Jellyfin.Drawing/ImageProcessor.cs @@ -107,22 +107,10 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable /// <inheritdoc /> public bool SupportsImageCollageCreation => _imageEncoder.SupportsImageCollageCreation; - /// <inheritdoc /> - public async Task ProcessImage(ImageProcessingOptions options, Stream toStream) - { - var file = await ProcessImage(options).ConfigureAwait(false); - using var fileStream = AsyncFile.OpenRead(file.Path); - await fileStream.CopyToAsync(toStream).ConfigureAwait(false); - } - /// <inheritdoc /> public IReadOnlyCollection<ImageFormat> GetSupportedImageOutputFormats() => _imageEncoder.SupportedOutputFormats; - /// <inheritdoc /> - public bool SupportsTransparency(string path) - => _transparentImageTypes.Contains(Path.GetExtension(path)); - /// <inheritdoc /> public async Task<(string Path, string? MimeType, DateTime DateModified)> ProcessImage(ImageProcessingOptions options) { @@ -224,7 +212,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable } } - return (cacheFilePath, GetMimeType(outputFormat, cacheFilePath), _fileSystem.GetLastWriteTimeUtc(cacheFilePath)); + return (cacheFilePath, outputFormat.GetMimeType(), _fileSystem.GetLastWriteTimeUtc(cacheFilePath)); } catch (Exception ex) { @@ -262,17 +250,6 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable return ImageFormat.Jpg; } - private string GetMimeType(ImageFormat format, string path) - => format switch - { - ImageFormat.Bmp => MimeTypes.GetMimeType("i.bmp"), - ImageFormat.Gif => MimeTypes.GetMimeType("i.gif"), - ImageFormat.Jpg => MimeTypes.GetMimeType("i.jpg"), - ImageFormat.Png => MimeTypes.GetMimeType("i.png"), - ImageFormat.Webp => MimeTypes.GetMimeType("i.webp"), - _ => MimeTypes.GetMimeType(path) - }; - /// <summary> /// Gets the cache file path based on a set of parameters. /// </summary> @@ -374,7 +351,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable filename.Append(",v="); filename.Append(Version); - return GetCachePath(ResizedImageCachePath, filename.ToString(), "." + format.ToString().ToLowerInvariant()); + return GetCachePath(ResizedImageCachePath, filename.ToString(), format.GetExtension()); } /// <inheritdoc /> @@ -471,35 +448,6 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable return Task.FromResult((originalImagePath, dateModified)); } - // TODO _mediaEncoder.ConvertImage is not implemented - // if (!_imageEncoder.SupportedInputFormats.Contains(inputFormat)) - // { - // try - // { - // string filename = (originalImagePath + dateModified.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("N", CultureInfo.InvariantCulture); - // - // string cacheExtension = _mediaEncoder.SupportsEncoder("libwebp") ? ".webp" : ".png"; - // var outputPath = Path.Combine(_appPaths.ImageCachePath, "converted-images", filename + cacheExtension); - // - // var file = _fileSystem.GetFileInfo(outputPath); - // if (!file.Exists) - // { - // await _mediaEncoder.ConvertImage(originalImagePath, outputPath).ConfigureAwait(false); - // dateModified = _fileSystem.GetLastWriteTimeUtc(outputPath); - // } - // else - // { - // dateModified = file.LastWriteTimeUtc; - // } - // - // originalImagePath = outputPath; - // } - // catch (Exception ex) - // { - // _logger.LogError(ex, "Image conversion failed for {Path}", originalImagePath); - // } - // } - return Task.FromResult((originalImagePath, dateModified)); } diff --git a/tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs b/tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs index a5bdb42d89..198ad5a274 100644 --- a/tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs +++ b/tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs @@ -30,4 +30,17 @@ public static class ImageFormatExtensionsTests [InlineData((ImageFormat)5)] public static void GetMimeType_Valid_ThrowsInvalidEnumArgumentException(ImageFormat format) => Assert.Throws<InvalidEnumArgumentException>(() => format.GetMimeType()); + + [Theory] + [MemberData(nameof(GetAllImageFormats))] + public static void GetExtension_Valid_Valid(ImageFormat format) + => Assert.Null(Record.Exception(() => format.GetExtension())); + + [Theory] + [InlineData((ImageFormat)int.MinValue)] + [InlineData((ImageFormat)int.MaxValue)] + [InlineData((ImageFormat)(-1))] + [InlineData((ImageFormat)5)] + public static void GetExtension_Valid_ThrowsInvalidEnumArgumentException(ImageFormat format) + => Assert.Throws<InvalidEnumArgumentException>(() => format.GetExtension()); } From 8466e53b8f0e885e75e3f3f5e9c81fceb0a61379 Mon Sep 17 00:00:00 2001 From: tellmeY18 <vysakh_b190622ec@nitc.ac.in> Date: Sat, 7 Oct 2023 21:49:16 +0000 Subject: [PATCH 689/858] Translated using Weblate (Malayalam) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ml/ --- Emby.Server.Implementations/Localization/Core/ml.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/ml.json b/Emby.Server.Implementations/Localization/Core/ml.json index 0620fbcdb0..0b50fa5298 100644 --- a/Emby.Server.Implementations/Localization/Core/ml.json +++ b/Emby.Server.Implementations/Localization/Core/ml.json @@ -121,5 +121,7 @@ "TaskOptimizeDatabaseDescription": "ഡാറ്റാബേസ് ചുരുക്കുകയും സ്വതന്ത്ര ഇടം വെട്ടിച്ചുരുക്കുകയും ചെയ്യുന്നു. ലൈബ്രറി സ്‌കാൻ ചെയ്‌തതിനുശേഷം അല്ലെങ്കിൽ ഡാറ്റാബേസ് പരിഷ്‌ക്കരണങ്ങളെ സൂചിപ്പിക്കുന്ന മറ്റ് മാറ്റങ്ങൾ ചെയ്‌തതിന് ശേഷം ഈ ടാസ്‌ക് പ്രവർത്തിപ്പിക്കുന്നത് പ്രകടനം മെച്ചപ്പെടുത്തും.", "TaskOptimizeDatabase": "ഡാറ്റാബേസ് ഒപ്റ്റിമൈസ് ചെയ്യുക", "HearingImpaired": "കേൾവി തകരാറുകൾ", - "External": "പുറമേയുള്ള" + "External": "പുറമേയുള്ള", + "TaskKeyframeExtractorDescription": "കൂടുതൽ കൃത്യമായ HLS പ്ലേലിസ്റ്റുകൾ സൃഷ്‌ടിക്കുന്നതിന് വീഡിയോ ഫയലുകളിൽ നിന്ന് കീഫ്രെയിമുകൾ എക്‌സ്‌ട്രാക്‌റ്റ് ചെയ്യുന്നു. ഈ പ്രവർത്തനം പൂർത്തിയാവാൻ കുറച്ചധികം സമയം എടുത്തേക്കാം.", + "TaskKeyframeExtractor": "കീഫ്രെയിം എക്സ്ട്രാക്റ്റർ" } From 75ee990e1d91d527974054e4130d9d27d0f7f2d6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 9 Oct 2023 08:10:21 -0600 Subject: [PATCH 690/858] Update github/codeql-action action to v2.22.1 (#10376) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 8e764b0194..4c9d68d112 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@2cb752a87e96af96708ab57187ab6372ee1973ab # v2.22.0 + uses: github/codeql-action/init@fdcae64e1484d349b3366718cdfef3d404390e85 # v2.22.1 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@2cb752a87e96af96708ab57187ab6372ee1973ab # v2.22.0 + uses: github/codeql-action/autobuild@fdcae64e1484d349b3366718cdfef3d404390e85 # v2.22.1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@2cb752a87e96af96708ab57187ab6372ee1973ab # v2.22.0 + uses: github/codeql-action/analyze@fdcae64e1484d349b3366718cdfef3d404390e85 # v2.22.1 From aa073748c00fe2b0508fde88d900143acec09e36 Mon Sep 17 00:00:00 2001 From: Nyanmisaka <nst799610810@gmail.com> Date: Mon, 9 Oct 2023 23:12:41 +0800 Subject: [PATCH 691/858] Drop experimental status of flac-in-MP4 for FFmpeg 6+ Signed-off-by: nyanmisaka <nst799610810@gmail.com> --- Jellyfin.Api/Controllers/DynamicHlsController.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 760fefbf51..44afdb6b11 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -45,6 +45,8 @@ public class DynamicHlsController : BaseJellyfinApiController private const string DefaultEventEncoderPreset = "superfast"; private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls; + private readonly Version _minFFmpegFlacInMp4 = new Version(6, 0); + private readonly ILibraryManager _libraryManager; private readonly IUserManager _userManager; private readonly IDlnaManager _dlnaManager; @@ -1705,13 +1707,14 @@ public class DynamicHlsController : BaseJellyfinApiController var audioCodec = _encodingHelper.GetAudioEncoder(state); var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container); - // dts, flac, opus and truehd are experimental in mp4 muxer + // opus, dts, truehd and flac (in FFmpeg 5 and older) are experimental in mp4 muxer var strictArgs = string.Empty; var actualOutputAudioCodec = state.ActualOutputAudioCodec; - if (string.Equals(actualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase) - || string.Equals(actualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase) + if (string.Equals(actualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase) || string.Equals(actualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase) - || string.Equals(actualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase)) + || string.Equals(actualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase) + || (string.Equals(actualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase) + && _mediaEncoder.EncoderVersion < _minFFmpegFlacInMp4)) { strictArgs = " -strict -2"; } From e8a05ad996ab35909cd5928d49c4e8c46e9985d5 Mon Sep 17 00:00:00 2001 From: herby2212 <12448284+herby2212@users.noreply.github.com> Date: Mon, 9 Oct 2023 19:15:25 +0200 Subject: [PATCH 692/858] optimize checkForInactiveStreams logic --- .../Session/SessionManager.cs | 50 ++++++++----------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 329138feb8..dc59a45239 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -625,43 +625,37 @@ namespace Emby.Server.Implementations.Session private async void CheckForInactiveSteams(object state) { - var pausedSessions = Sessions.Where(i => + var inactiveSessions = Sessions.Where(i => i.NowPlayingItem is not null && i.PlayState.IsPaused - && i.LastPausedDate is not null) - .ToList(); + && (DateTime.UtcNow - i.LastPausedDate).Value.TotalMinutes > _config.Configuration.InactiveSessionThreshold); - if (pausedSessions.Count > 0) + foreach (var session in inactiveSessions) { - var inactiveSessions = pausedSessions.Where(i => (DateTime.UtcNow - i.LastPausedDate).Value.TotalMinutes > _config.Configuration.InactiveSessionThreshold).ToList(); + _logger.LogDebug("Session {Session} has been inactive for {InactiveTime} minutes. Stopping it.", session.Id, _config.Configuration.InactiveSessionThreshold); - foreach (var session in inactiveSessions) + try { - _logger.LogDebug("Session {Session} has been inactive for {InactiveTime} minutes. Stopping it.", session.Id, _config.Configuration.InactiveSessionThreshold); - - try - { - await SendPlaystateCommand( - session.Id, - session.Id, - new PlaystateRequest() - { - Command = PlaystateCommand.Stop, - ControllingUserId = session.UserId.ToString(), - SeekPositionTicks = session.PlayState?.PositionTicks - }, - CancellationToken.None).ConfigureAwait(true); - } - catch (Exception ex) - { - _logger.LogDebug(ex, "Error calling SendPlaystateCommand for stopping inactive session {Session}.", session.Id); - } + await SendPlaystateCommand( + session.Id, + session.Id, + new PlaystateRequest() + { + Command = PlaystateCommand.Stop, + ControllingUserId = session.UserId.ToString(), + SeekPositionTicks = session.PlayState?.PositionTicks + }, + CancellationToken.None).ConfigureAwait(true); + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Error calling SendPlaystateCommand for stopping inactive session {Session}.", session.Id); } } - var playingSessions = Sessions.Where(i => i.NowPlayingItem is not null) - .ToList(); - if (playingSessions.Count == 0) + bool playingSessions = Sessions.Any(i => i.NowPlayingItem is not null); + + if (!playingSessions) { StopInactiveCheckTimer(); } From 4757ce105bedb075c6663d92bc1e93f87c4779f2 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Tue, 10 Oct 2023 00:18:50 +0200 Subject: [PATCH 693/858] Use Process.WaitForExitAsync added in .NET 5 --- .../Extensions/ProcessExtensions.cs | 60 ++----------------- .../Attachments/AttachmentExtractor.cs | 44 +++++--------- .../Encoder/MediaEncoder.cs | 22 ++++--- .../Subtitles/SubtitleEncoder.cs | 46 +++++--------- 4 files changed, 46 insertions(+), 126 deletions(-) diff --git a/MediaBrowser.Common/Extensions/ProcessExtensions.cs b/MediaBrowser.Common/Extensions/ProcessExtensions.cs index c3a7cb394e..bb8ab130df 100644 --- a/MediaBrowser.Common/Extensions/ProcessExtensions.cs +++ b/MediaBrowser.Common/Extensions/ProcessExtensions.cs @@ -15,65 +15,13 @@ namespace MediaBrowser.Common.Extensions /// </summary> /// <param name="process">The process to wait for.</param> /// <param name="timeout">The duration to wait before cancelling waiting for the task.</param> - /// <returns>True if the task exited normally, false if the timeout elapsed before the process exited.</returns> - /// <exception cref="InvalidOperationException">If <see cref="Process.EnableRaisingEvents"/> is not set to true for the process.</exception> - public static async Task<bool> WaitForExitAsync(this Process process, TimeSpan timeout) + /// <returns>A task that will complete when the process has exited, cancellation has been requested, or an error occurs.</returns> + /// <exception cref="OperationCanceledException">The timeout ended.</exception> + public static async Task WaitForExitAsync(this Process process, TimeSpan timeout) { using (var cancelTokenSource = new CancellationTokenSource(timeout)) { - return await WaitForExitAsync(process, cancelTokenSource.Token).ConfigureAwait(false); - } - } - - /// <summary> - /// Asynchronously wait for the process to exit. - /// </summary> - /// <param name="process">The process to wait for.</param> - /// <param name="cancelToken">A <see cref="CancellationToken"/> to observe while waiting for the process to exit.</param> - /// <returns>True if the task exited normally, false if cancelled before the process exited.</returns> - public static async Task<bool> WaitForExitAsync(this Process process, CancellationToken cancelToken) - { - if (!process.EnableRaisingEvents) - { - throw new InvalidOperationException("EnableRisingEvents must be enabled to async wait for a task to exit."); - } - - // Add an event handler for the process exit event - var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously); - process.Exited += (_, _) => tcs.TrySetResult(true); - - // Return immediately if the process has already exited - if (process.HasExitedSafe()) - { - return true; - } - - // Register with the cancellation token then await - using (var cancelRegistration = cancelToken.Register(() => tcs.TrySetResult(process.HasExitedSafe()))) - { - return await tcs.Task.ConfigureAwait(false); - } - } - - /// <summary> - /// Gets a value indicating whether the associated process has been terminated using - /// <see cref="Process.HasExited"/>. This is safe to call even if there is no operating system process - /// associated with the <see cref="Process"/>. - /// </summary> - /// <param name="process">The process to check the exit status for.</param> - /// <returns> - /// True if the operating system process referenced by the <see cref="Process"/> component has - /// terminated, or if there is no associated operating system process; otherwise, false. - /// </returns> - private static bool HasExitedSafe(this Process process) - { - try - { - return process.HasExited; - } - catch (InvalidOperationException) - { - return true; + await process.WaitForExitAsync(cancelTokenSource.Token).ConfigureAwait(false); } } } diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs index 0ec0c84d41..299f294b29 100644 --- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs +++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs @@ -174,22 +174,16 @@ namespace MediaBrowser.MediaEncoding.Attachments process.Start(); - var ranToCompletion = await ProcessExtensions.WaitForExitAsync(process, cancellationToken).ConfigureAwait(false); - - if (!ranToCompletion) + try { - try - { - _logger.LogWarning("Killing ffmpeg attachment extraction process"); - process.Kill(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error killing attachment extraction process"); - } + await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false); + exitCode = process.ExitCode; + } + catch (OperationCanceledException) + { + process.Kill(true); + exitCode = -1; } - - exitCode = ranToCompletion ? process.ExitCode : -1; } var failed = false; @@ -322,22 +316,16 @@ namespace MediaBrowser.MediaEncoding.Attachments process.Start(); - var ranToCompletion = await ProcessExtensions.WaitForExitAsync(process, cancellationToken).ConfigureAwait(false); - - if (!ranToCompletion) + try { - try - { - _logger.LogWarning("Killing ffmpeg attachment extraction process"); - process.Kill(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error killing attachment extraction process"); - } + await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false); + exitCode = process.ExitCode; + } + catch (OperationCanceledException) + { + process.Kill(true); + exitCode = -1; } - - exitCode = ranToCompletion ? process.ExitCode : -1; } var failed = false; diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 4bff196658..0eaf9748f6 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -760,11 +760,15 @@ namespace MediaBrowser.MediaEncoding.Encoder timeoutMs = enableHdrExtraction ? DefaultHdrImageExtractionTimeout : DefaultSdrImageExtractionTimeout; } - ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMilliseconds(timeoutMs)).ConfigureAwait(false); - - if (!ranToCompletion) + try { - StopProcess(processWrapper, 1000); + await process.WaitForExitAsync(TimeSpan.FromMilliseconds(timeoutMs)).ConfigureAwait(false); + ranToCompletion = true; + } + catch (OperationCanceledException) + { + process.Kill(true); + ranToCompletion = false; } } finally @@ -999,7 +1003,7 @@ namespace MediaBrowser.MediaEncoding.Encoder return true; } - private class ProcessWrapper : IDisposable + private sealed class ProcessWrapper : IDisposable { private readonly MediaEncoder _mediaEncoder; @@ -1042,13 +1046,7 @@ namespace MediaBrowser.MediaEncoding.Encoder _mediaEncoder._runningProcesses.Remove(this); } - try - { - process.Dispose(); - } - catch - { - } + process.Dispose(); } public void Dispose() diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index a41e0b7e98..21fa4468ed 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -420,23 +420,16 @@ namespace MediaBrowser.MediaEncoding.Subtitles throw; } - var ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false); - - if (!ranToCompletion) + try { - try - { - _logger.LogInformation("Killing ffmpeg subtitle conversion process"); - - process.Kill(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error killing subtitle conversion process"); - } + await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false); + exitCode = process.ExitCode; + } + catch (OperationCanceledException) + { + process.Kill(true); + exitCode = -1; } - - exitCode = ranToCompletion ? process.ExitCode : -1; } var failed = false; @@ -574,23 +567,16 @@ namespace MediaBrowser.MediaEncoding.Subtitles throw; } - var ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false); - - if (!ranToCompletion) + try { - try - { - _logger.LogWarning("Killing ffmpeg subtitle extraction process"); - - process.Kill(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error killing subtitle extraction process"); - } + await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false); + exitCode = process.ExitCode; + } + catch (OperationCanceledException) + { + process.Kill(true); + exitCode = -1; } - - exitCode = ranToCompletion ? process.ExitCode : -1; } var failed = false; From 305405c9a1ac4a37ac9e8eda44899c33cf76eda3 Mon Sep 17 00:00:00 2001 From: scampower3 <81431263+scampower3@users.noreply.github.com> Date: Tue, 10 Oct 2023 19:12:09 +0800 Subject: [PATCH 694/858] Combine Title and Overview for multi-episodes files for NFO file (#10080) --- .../Parsers/EpisodeNfoParser.cs | 41 ++++++++++++++++++- .../Parsers/EpisodeNfoProviderTests.cs | 6 +-- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs index d2f349ad7a..432b89c313 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs @@ -1,6 +1,7 @@ using System; using System.Globalization; using System.IO; +using System.Text; using System.Threading; using System.Xml; using MediaBrowser.Common.Configuration; @@ -81,7 +82,10 @@ namespace MediaBrowser.XbmcMetadata.Parsers } // Extract the last episode number from nfo + // Retrieves all title and plot tags from the rest of the nfo and concatenates them with the first episode // This is needed because XBMC metadata uses multiple episodedetails blocks instead of episodenumberend tag + var name = new StringBuilder(item.Item.Name); + var overview = new StringBuilder(item.Item.Overview); while ((index = xmlFile.IndexOf(srch, StringComparison.OrdinalIgnoreCase)) != -1) { xml = xmlFile.Substring(0, index + srch.Length); @@ -92,12 +96,44 @@ namespace MediaBrowser.XbmcMetadata.Parsers { reader.MoveToContent(); - if (reader.ReadToDescendant("episode") && int.TryParse(reader.ReadElementContentAsString(), out var num)) + while (!reader.EOF && reader.ReadState == ReadState.Interactive) { - item.Item.IndexNumberEnd = Math.Max(num, item.Item.IndexNumberEnd ?? num); + cancellationToken.ThrowIfCancellationRequested(); + + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.Name) + { + case "name": + case "title": + case "localtitle": + name.Append(" / ").Append(reader.ReadElementContentAsString()); + break; + case "episode": + { + if (int.TryParse(reader.ReadElementContentAsString(), out var num)) + { + item.Item.IndexNumberEnd = Math.Max(num, item.Item.IndexNumberEnd ?? num); + } + + break; + } + + case "biography": + case "plot": + case "review": + overview.Append(" / ").Append(reader.ReadElementContentAsString()); + break; + } + } + + reader.Read(); } } } + + item.Item.Name = name.ToString(); + item.Item.Overview = overview.ToString(); } catch (XmlException) { @@ -172,6 +208,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers break; } + case "displayafterseason": case "airsafter_season": { var val = reader.ReadElementContentAsString(); diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs index f63bc0e1bc..c0d06116b5 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Threading; using Jellyfin.Data.Enums; @@ -114,11 +114,11 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers _parser.Fetch(result, "Test Data/Rising.nfo", CancellationToken.None); var item = result.Item; - Assert.Equal("Rising (1)", item.Name); + Assert.Equal("Rising (1) / Rising (2)", item.Name); Assert.Equal(1, item.IndexNumber); Assert.Equal(2, item.IndexNumberEnd); Assert.Equal(1, item.ParentIndexNumber); - Assert.Equal("A new Stargate team embarks on a dangerous mission to a distant galaxy, where they discover a mythical lost city -- and a deadly new enemy.", item.Overview); + Assert.Equal("A new Stargate team embarks on a dangerous mission to a distant galaxy, where they discover a mythical lost city -- and a deadly new enemy. / Sheppard tries to convince Weir to mount a rescue mission to free Colonel Sumner, Teyla, and the others captured by the Wraith.", item.Overview); Assert.Equal(new DateTime(2004, 7, 16), item.PremiereDate); Assert.Equal(2004, item.ProductionYear); } From d15f6908b0cec93d9a91e458f3616a5224b68d48 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Tue, 10 Oct 2023 13:29:16 +0200 Subject: [PATCH 695/858] Empty Guids shouldn't make it into the refresh queue ``` System.ArgumentException: Guid can't be empty (Parameter 'id') at Emby.Server.Implementations.Library.LibraryManager.GetItemById(Guid id) in /home/loma/dev/jellyfin/Emby.Server.Implementations/Library/LibraryManager.cs:line 1224 at MediaBrowser.Providers.Manager.ProviderManager.StartProcessingRefreshQueue() in /home/loma/dev/jellyfin/MediaBrowser.Providers/Manager/ProviderManager.cs:line 983 ``` --- MediaBrowser.Providers/Manager/ProviderManager.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index f3211ba450..d0bb34d52a 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -943,6 +943,12 @@ namespace MediaBrowser.Providers.Manager /// <inheritdoc/> public void QueueRefresh(Guid itemId, MetadataRefreshOptions options, RefreshPriority priority) { + ArgumentNullException.ThrowIfNull(itemId); + if (itemId.Equals(default)) + { + throw new ArgumentException("Guid can't be empty", nameof(itemId)); + } + if (_disposed) { return; From 502c25750670db92f75e2027febd4e5b6cd6c34b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 10 Oct 2023 13:07:13 +0000 Subject: [PATCH 696/858] Update dependency Microsoft.AspNetCore.Authorization to v7.0.12 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 8cf3eae2a5..55b03cdcee 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -25,7 +25,7 @@ <PackageVersion Include="libse" Version="3.6.13" /> <PackageVersion Include="LrcParser" Version="2023.524.0" /> <PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" /> - <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.11" /> + <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.12" /> <PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" /> <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.11" /> <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" /> From 74f61fbd79ef2e2ad4a986f5f886581b2827de07 Mon Sep 17 00:00:00 2001 From: lonebyte <61915324+lonebyte@users.noreply.github.com> Date: Tue, 10 Oct 2023 22:48:52 +0200 Subject: [PATCH 697/858] Fix HLS playback of m4a files with mjpeg stream (#10069) --- Jellyfin.Api/Controllers/DynamicHlsController.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 7bf366e5d0..42c94c29d3 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1721,14 +1721,17 @@ public class DynamicHlsController : BaseJellyfinApiController if (!state.IsOutputVideo) { - if (EncodingHelper.IsCopyCodec(audioCodec)) - { - return "-acodec copy" + bitStreamArgs + strictArgs; - } - var audioTranscodeParams = string.Empty; - audioTranscodeParams += "-acodec " + audioCodec + bitStreamArgs + strictArgs; + // -vn to drop any video streams + audioTranscodeParams += "-vn"; + + if (EncodingHelper.IsCopyCodec(audioCodec)) + { + return audioTranscodeParams + " -acodec copy" + bitStreamArgs + strictArgs; + } + + audioTranscodeParams += " -acodec " + audioCodec + bitStreamArgs + strictArgs; var audioBitrate = state.OutputAudioBitrate; var audioChannels = state.OutputAudioChannels; @@ -1756,7 +1759,6 @@ public class DynamicHlsController : BaseJellyfinApiController audioTranscodeParams += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture); } - audioTranscodeParams += " -vn"; return audioTranscodeParams; } From 2920611ffc206d845563637c4a865bf3f02d1374 Mon Sep 17 00:00:00 2001 From: Cody Robibero <cody@robibe.ro> Date: Sat, 13 May 2023 12:44:31 -0600 Subject: [PATCH 698/858] Convert string MediaType to enum MediaType --- Emby.Dlna/ContentDirectory/ControlHandler.cs | 2 +- Emby.Dlna/Didl/DidlBuilder.cs | 24 +++++++------- Emby.Dlna/PlayTo/PlayToController.cs | 7 ++-- Emby.Dlna/PlayTo/uBaseObject.cs | 13 ++++---- .../Data/SqliteItemRepository.cs | 16 +++------- .../Library/MediaSourceManager.cs | 15 +++++---- .../Library/Resolvers/PlaylistResolver.cs | 1 + .../Library/UserViewManager.cs | 2 +- .../Validators/CollectionPostScanTask.cs | 2 +- .../Playlists/PlaylistManager.cs | 15 +++++---- .../ScheduledTasks/Tasks/ChapterImagesTask.cs | 1 + Jellyfin.Api/Controllers/ArtistsController.cs | 4 +-- Jellyfin.Api/Controllers/FilterController.cs | 2 +- Jellyfin.Api/Controllers/ItemsController.cs | 6 ++-- .../Controllers/PlaylistsController.cs | 3 +- Jellyfin.Api/Controllers/SearchController.cs | 2 +- Jellyfin.Api/Controllers/SessionController.cs | 2 +- .../Controllers/SuggestionsController.cs | 2 +- .../Controllers/TrailersController.cs | 2 +- Jellyfin.Api/Controllers/YearsController.cs | 6 ++-- Jellyfin.Api/Helpers/MediaInfoHelper.cs | 2 +- Jellyfin.Api/Helpers/StreamingHelpers.cs | 3 +- .../Models/PlaylistDtos/CreatePlaylistDto.cs | 3 +- .../SessionDtos/ClientCapabilitiesDto.cs | 3 +- Jellyfin.Data/Enums/MediaType.cs | 32 +++++++++++++++++++ .../Consumers/Session/PlaybackStartLogger.cs | 7 ++-- .../Consumers/Session/PlaybackStopLogger.cs | 7 ++-- .../Entities/Audio/Audio.cs | 2 +- MediaBrowser.Controller/Entities/BaseItem.cs | 2 +- MediaBrowser.Controller/Entities/Book.cs | 2 +- .../Entities/InternalItemsQuery.cs | 4 +-- MediaBrowser.Controller/Entities/Photo.cs | 3 +- .../Entities/UserViewBuilder.cs | 2 +- MediaBrowser.Controller/Entities/Video.cs | 3 +- .../LiveTv/LiveTvChannel.cs | 2 +- MediaBrowser.Controller/Playlists/Playlist.cs | 14 ++++---- .../Session/SessionInfo.cs | 5 +-- .../Parsers/PlaylistXmlParser.cs | 8 ++++- .../Savers/PlaylistXmlSaver.cs | 5 +-- MediaBrowser.Model/Dlna/DeviceProfile.cs | 8 +++-- MediaBrowser.Model/Dto/BaseItemDto.cs | 2 +- MediaBrowser.Model/Entities/MediaType.cs | 28 ---------------- .../Playlists/PlaylistCreationRequest.cs | 3 +- MediaBrowser.Model/Search/SearchHint.cs | 4 +-- MediaBrowser.Model/Search/SearchQuery.cs | 4 +-- .../Session/ClientCapabilities.cs | 5 +-- .../MediaInfo/SubtitleScheduledTask.cs | 2 +- 47 files changed, 159 insertions(+), 133 deletions(-) create mode 100644 Jellyfin.Data/Enums/MediaType.cs delete mode 100644 MediaBrowser.Model/Entities/MediaType.cs diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs index abd594a3a1..12cd7e2ef7 100644 --- a/Emby.Dlna/ContentDirectory/ControlHandler.cs +++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs @@ -494,7 +494,7 @@ namespace Emby.Dlna.ContentDirectory { var folder = (Folder)item; - string[] mediaTypes = Array.Empty<string>(); + MediaType[] mediaTypes = Array.Empty<MediaType>(); bool? isFolder = null; switch (search.SearchType) diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs index 5ed982876d..9f152df132 100644 --- a/Emby.Dlna/Didl/DidlBuilder.cs +++ b/Emby.Dlna/Didl/DidlBuilder.cs @@ -174,13 +174,14 @@ namespace Emby.Dlna.Didl if (item is IHasMediaSources) { - if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) + switch (item.MediaType) { - AddAudioResource(writer, item, deviceId, filter, streamInfo); - } - else if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) - { - AddVideoResource(writer, item, deviceId, filter, streamInfo); + case MediaType.Audio: + AddAudioResource(writer, item, deviceId, filter, streamInfo); + break; + case MediaType.Video: + AddVideoResource(writer, item, deviceId, filter, streamInfo); + break; } } @@ -821,15 +822,15 @@ namespace Emby.Dlna.Didl writer.WriteString(classType ?? "object.container.storageFolder"); } - else if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) + else if (item.MediaType == MediaType.Audio) { writer.WriteString("object.item.audioItem.musicTrack"); } - else if (string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase)) + else if (item.MediaType == MediaType.Photo) { writer.WriteString("object.item.imageItem.photo"); } - else if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) + else if (item.MediaType == MediaType.Video) { if (!_profile.RequiresPlainVideoItems && item is Movie) { @@ -1006,8 +1007,7 @@ namespace Emby.Dlna.Didl if (!_profile.EnableAlbumArtInDidl) { - if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) - || string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) + if (item.MediaType == MediaType.Audio || item.MediaType == MediaType.Video) { if (!stubType.HasValue) { @@ -1016,7 +1016,7 @@ namespace Emby.Dlna.Didl } } - if (!_profile.EnableSingleAlbumArtLimit || string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase)) + if (!_profile.EnableSingleAlbumArtLimit || item.MediaType == MediaType.Photo) { AddImageResElement(item, writer, 4096, 4096, "jpg", "JPEG_LRG"); AddImageResElement(item, writer, 1024, 768, "jpg", "JPEG_MED"); diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs index b1ad15cdc9..9e3d1d4d6d 100644 --- a/Emby.Dlna/PlayTo/PlayToController.cs +++ b/Emby.Dlna/PlayTo/PlayToController.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using Emby.Dlna.Didl; using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using Jellyfin.Data.Events; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Drawing; @@ -577,7 +578,7 @@ namespace Emby.Dlna.PlayTo 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)) + if (item.MediaType == MediaType.Video) { return new PlaylistItem { @@ -597,7 +598,7 @@ namespace Emby.Dlna.PlayTo }; } - if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) + if (item.MediaType == MediaType.Audio) { return new PlaylistItem { @@ -615,7 +616,7 @@ namespace Emby.Dlna.PlayTo }; } - if (string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase)) + if (item.MediaType == MediaType.Photo) { return PlaylistItemFactory.Create((Photo)item, profile); } diff --git a/Emby.Dlna/PlayTo/uBaseObject.cs b/Emby.Dlna/PlayTo/uBaseObject.cs index 2e0f2063be..a8f451405c 100644 --- a/Emby.Dlna/PlayTo/uBaseObject.cs +++ b/Emby.Dlna/PlayTo/uBaseObject.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using Jellyfin.Data.Enums; namespace Emby.Dlna.PlayTo { @@ -33,19 +34,19 @@ namespace Emby.Dlna.PlayTo { var classType = UpnpClass ?? string.Empty; - if (classType.IndexOf(MediaBrowser.Model.Entities.MediaType.Audio, StringComparison.Ordinal) != -1) + if (classType.Contains("Audio", StringComparison.Ordinal)) { - return MediaBrowser.Model.Entities.MediaType.Audio; + return "Audio"; } - if (classType.IndexOf(MediaBrowser.Model.Entities.MediaType.Video, StringComparison.Ordinal) != -1) + if (classType.Contains("Video", StringComparison.Ordinal)) { - return MediaBrowser.Model.Entities.MediaType.Video; + return "Video"; } - if (classType.IndexOf("image", StringComparison.Ordinal) != -1) + if (classType.Contains("image", StringComparison.Ordinal)) { - return MediaBrowser.Model.Entities.MediaType.Photo; + return "Photo"; } return null; diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 77cf4089be..798521d224 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -722,7 +722,7 @@ namespace Emby.Server.Implementations.Data saveItemStatement.TryBind("@IsLocked", item.IsLocked); saveItemStatement.TryBind("@Name", item.Name); saveItemStatement.TryBind("@OfficialRating", item.OfficialRating); - saveItemStatement.TryBind("@MediaType", item.MediaType); + saveItemStatement.TryBind("@MediaType", item.MediaType.ToString()); saveItemStatement.TryBind("@Overview", item.Overview); saveItemStatement.TryBind("@ParentIndexNumber", item.ParentIndexNumber); saveItemStatement.TryBind("@PremiereDate", item.PremiereDate); @@ -3109,11 +3109,6 @@ namespace Emby.Server.Implementations.Data return true; } - private bool IsValidMediaType(string value) - { - return IsAlphaNumeric(value); - } - private bool IsValidPersonType(string value) { return IsAlphaNumeric(value); @@ -4124,15 +4119,14 @@ namespace Emby.Server.Implementations.Data } } - var queryMediaTypes = query.MediaTypes.Where(IsValidMediaType).ToArray(); - if (queryMediaTypes.Length == 1) + if (query.MediaTypes.Length == 1) { whereClauses.Add("MediaType=@MediaTypes"); - statement?.TryBind("@MediaTypes", queryMediaTypes[0]); + statement?.TryBind("@MediaTypes", query.MediaTypes[0].ToString()); } - else if (queryMediaTypes.Length > 1) + else if (query.MediaTypes.Length > 1) { - var val = string.Join(',', queryMediaTypes.Select(i => "'" + i + "'")); + var val = string.Join(',', query.MediaTypes.Select(i => $"'{i}'")); whereClauses.Add("MediaType in (" + val + ")"); } diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index c9a26a30f5..3d0f9f02c0 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -11,6 +11,7 @@ using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using EasyCaching.Core.Configurations; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Extensions.Json; @@ -186,11 +187,11 @@ namespace Emby.Server.Implementations.Library { SetDefaultAudioAndSubtitleStreamIndexes(item, source, user); - if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) + if (item.MediaType == MediaType.Audio) { source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding); } - else if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) + else if (item.MediaType == MediaType.Video) { source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding); source.SupportsDirectStream = user.HasPermission(PermissionKind.EnablePlaybackRemuxing); @@ -334,11 +335,11 @@ namespace Emby.Server.Implementations.Library { SetDefaultAudioAndSubtitleStreamIndexes(item, source, user); - if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) + if (item.MediaType == MediaType.Audio) { source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding); } - else if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) + else if (item.MediaType == MediaType.Video) { source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding); source.SupportsDirectStream = user.HasPermission(PermissionKind.EnablePlaybackRemuxing); @@ -417,9 +418,9 @@ namespace Emby.Server.Implementations.Library public void SetDefaultAudioAndSubtitleStreamIndexes(BaseItem item, MediaSourceInfo source, User user) { // Item would only be null if the app didn't supply ItemId as part of the live stream open request - var mediaType = item is null ? MediaType.Video : item.MediaType; + var mediaType = item?.MediaType ?? MediaType.Video; - if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) + if (mediaType == MediaType.Video) { var userData = item is null ? new UserItemData() : _userDataManager.GetUserData(user, item); @@ -428,7 +429,7 @@ namespace Emby.Server.Implementations.Library SetDefaultAudioStreamIndex(source, userData, user, allowRememberingSelection); SetDefaultSubtitleStreamIndex(source, userData, user, allowRememberingSelection); } - else if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) + else if (mediaType == MediaType.Audio) { var audio = source.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio); diff --git a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs index 5d569009d3..8e34a42b38 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs @@ -5,6 +5,7 @@ using System; using System.IO; using System.Linq; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Playlists; diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs index 2c3dc18574..0343db68b2 100644 --- a/Emby.Server.Implementations/Library/UserViewManager.cs +++ b/Emby.Server.Implementations/Library/UserViewManager.cs @@ -317,7 +317,7 @@ namespace Emby.Server.Implementations.Library } } - var mediaTypes = new List<string>(); + var mediaTypes = new List<MediaType>(); if (includeItemTypes.Length == 0) { diff --git a/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs index df45793c3a..89f64ee4f0 100644 --- a/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs +++ b/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs @@ -63,7 +63,7 @@ namespace Emby.Server.Implementations.Library.Validators { var movies = _libraryManager.GetItemList(new InternalItemsQuery { - MediaTypes = new string[] { MediaType.Video }, + MediaTypes = new[] { MediaType.Video }, IncludeItemTypes = new[] { BaseItemKind.Movie }, IsVirtualItem = false, OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }, diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 649c499247..c783f2b01f 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -74,7 +75,7 @@ namespace Emby.Server.Implementations.Playlists throw new ArgumentException(nameof(parentFolder)); } - if (string.IsNullOrEmpty(options.MediaType)) + if (options.MediaType is null || options.MediaType == MediaType.Unknown) { foreach (var itemId in options.ItemIdList) { @@ -84,7 +85,7 @@ namespace Emby.Server.Implementations.Playlists throw new ArgumentException("No item exists with the supplied Id"); } - if (!string.IsNullOrEmpty(item.MediaType)) + if (item.MediaType == MediaType.Unknown) { options.MediaType = item.MediaType; } @@ -102,20 +103,20 @@ namespace Emby.Server.Implementations.Playlists { options.MediaType = folder.GetRecursiveChildren(i => !i.IsFolder && i.SupportsAddingToPlaylist) .Select(i => i.MediaType) - .FirstOrDefault(i => !string.IsNullOrEmpty(i)); + .FirstOrDefault(i => i != MediaType.Unknown); } } - if (!string.IsNullOrEmpty(options.MediaType)) + if (options.MediaType is null || options.MediaType == MediaType.Unknown) { break; } } } - if (string.IsNullOrEmpty(options.MediaType)) + if (options.MediaType is null || options.MediaType == MediaType.Unknown) { - options.MediaType = "Audio"; + options.MediaType = MediaType.Audio; } var user = _userManager.GetUserById(options.UserId); @@ -168,7 +169,7 @@ namespace Emby.Server.Implementations.Playlists return path; } - private List<BaseItem> GetPlaylistItems(IEnumerable<Guid> itemIds, string playlistMediaType, User user, DtoOptions options) + private List<BaseItem> GetPlaylistItems(IEnumerable<Guid> itemIds, MediaType playlistMediaType, User user, DtoOptions options) { var items = itemIds.Select(i => _libraryManager.GetItemById(i)).Where(i => i is not null); diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs index 6ad6c4cbd6..1d72ceae9c 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Dto; diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs index c9d2f67f92..1c994ac8be 100644 --- a/Jellyfin.Api/Controllers/ArtistsController.cs +++ b/Jellyfin.Api/Controllers/ArtistsController.cs @@ -95,7 +95,7 @@ public class ArtistsController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, @@ -299,7 +299,7 @@ public class ArtistsController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs index d51a5325f5..baeb8b81a0 100644 --- a/Jellyfin.Api/Controllers/FilterController.cs +++ b/Jellyfin.Api/Controllers/FilterController.cs @@ -50,7 +50,7 @@ public class FilterController : BaseJellyfinApiController [FromQuery] Guid? userId, [FromQuery] Guid? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes) + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes) { userId = RequestHelpers.GetUserId(User, userId); var user = userId.Value.Equals(default) diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 80128536da..884613afae 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -195,7 +195,7 @@ public class ItemsController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, [FromQuery] bool? isPlayed, @@ -652,7 +652,7 @@ public class ItemsController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, [FromQuery] bool? isPlayed, @@ -812,7 +812,7 @@ public class ItemsController : BaseJellyfinApiController [FromQuery] string? searchTerm, [FromQuery] Guid? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index 8d2a738d4a..c4c89ccde0 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -8,6 +8,7 @@ using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.PlaylistDtos; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Playlists; @@ -75,7 +76,7 @@ public class PlaylistsController : BaseJellyfinApiController [FromQuery, ParameterObsolete] string? name, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder)), ParameterObsolete] IReadOnlyList<Guid> ids, [FromQuery, ParameterObsolete] Guid? userId, - [FromQuery, ParameterObsolete] string? mediaType, + [FromQuery, ParameterObsolete] MediaType? mediaType, [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] CreatePlaylistDto? createPlaylistRequest) { if (ids.Count == 0) diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs index 387b3ea5a6..5b45941657 100644 --- a/Jellyfin.Api/Controllers/SearchController.cs +++ b/Jellyfin.Api/Controllers/SearchController.cs @@ -86,7 +86,7 @@ public class SearchController : BaseJellyfinApiController [FromQuery, Required] string searchTerm, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes, [FromQuery] Guid? parentId, [FromQuery] bool? isMovie, [FromQuery] bool? isSeries, diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index e93456de66..e20cf034dc 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -393,7 +393,7 @@ public class SessionController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task<ActionResult> PostCapabilities( [FromQuery] string? id, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] playableMediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] playableMediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] GeneralCommandType[] supportedCommands, [FromQuery] bool supportsMediaControl = false, [FromQuery] bool supportsSync = false, diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs index 5b808f257c..675757fc51 100644 --- a/Jellyfin.Api/Controllers/SuggestionsController.cs +++ b/Jellyfin.Api/Controllers/SuggestionsController.cs @@ -56,7 +56,7 @@ public class SuggestionsController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<QueryResult<BaseItemDto>> GetSuggestions( [FromRoute, Required] Guid userId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaType, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaType, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] type, [FromQuery] int? startIndex, [FromQuery] int? limit, diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs index b5b6406206..9f1e0e837a 100644 --- a/Jellyfin.Api/Controllers/TrailersController.cs +++ b/Jellyfin.Api/Controllers/TrailersController.cs @@ -160,7 +160,7 @@ public class TrailersController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, [FromQuery] bool? isPlayed, diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs index 74370db50b..200ecea094 100644 --- a/Jellyfin.Api/Controllers/YearsController.cs +++ b/Jellyfin.Api/Controllers/YearsController.cs @@ -76,7 +76,7 @@ public class YearsController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, @@ -191,7 +191,7 @@ public class YearsController : BaseJellyfinApiController return _dtoService.GetBaseItemDto(item, dtoOptions); } - private bool FilterItem(BaseItem f, IReadOnlyCollection<BaseItemKind> excludeItemTypes, IReadOnlyCollection<BaseItemKind> includeItemTypes, IReadOnlyCollection<string> mediaTypes) + private bool FilterItem(BaseItem f, IReadOnlyCollection<BaseItemKind> excludeItemTypes, IReadOnlyCollection<BaseItemKind> includeItemTypes, IReadOnlyCollection<MediaType> mediaTypes) { var baseItemKind = f.GetBaseItemKind(); // Exclude item types @@ -207,7 +207,7 @@ public class YearsController : BaseJellyfinApiController } // Include MediaTypes - if (mediaTypes.Count > 0 && !mediaTypes.Contains(f.MediaType ?? string.Empty, StringComparison.OrdinalIgnoreCase)) + if (mediaTypes.Count > 0 && !mediaTypes.Contains(f.MediaType)) { return false; } diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs index a36028cfeb..321987ca74 100644 --- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs +++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs @@ -243,7 +243,7 @@ public class MediaInfoHelper } // Beginning of Playback Determination - var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) + var streamInfo = item.MediaType == MediaType.Audio ? streamBuilder.GetOptimalAudioStream(options) : streamBuilder.GetOptimalVideoStream(options); diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index a653c57952..ed2358dd8e 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Extensions; using Jellyfin.Api.Models.StreamingDtos; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; @@ -128,7 +129,7 @@ public static class StreamingHelpers var item = libraryManager.GetItemById(streamingRequest.Id); - state.IsInputVideo = string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase); + state.IsInputVideo = item.MediaType == MediaType.Video; MediaSourceInfo? mediaSource = null; if (string.IsNullOrWhiteSpace(streamingRequest.LiveStreamId)) diff --git a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs index 1fba32c5b8..bdc4888719 100644 --- a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs +++ b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text.Json.Serialization; +using Jellyfin.Data.Enums; using Jellyfin.Extensions.Json.Converters; namespace Jellyfin.Api.Models.PlaylistDtos; @@ -29,5 +30,5 @@ public class CreatePlaylistDto /// <summary> /// Gets or sets the media type. /// </summary> - public string? MediaType { get; set; } + public MediaType? MediaType { get; set; } } diff --git a/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs b/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs index b88be33b22..b021771a0f 100644 --- a/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs +++ b/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text.Json.Serialization; +using Jellyfin.Data.Enums; using Jellyfin.Extensions.Json.Converters; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Session; @@ -16,7 +17,7 @@ public class ClientCapabilitiesDto /// Gets or sets the list of playable media types. /// </summary> [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] - public IReadOnlyList<string> PlayableMediaTypes { get; set; } = Array.Empty<string>(); + public IReadOnlyList<MediaType> PlayableMediaTypes { get; set; } = Array.Empty<MediaType>(); /// <summary> /// Gets or sets the list of supported commands. diff --git a/Jellyfin.Data/Enums/MediaType.cs b/Jellyfin.Data/Enums/MediaType.cs new file mode 100644 index 0000000000..b014ff3664 --- /dev/null +++ b/Jellyfin.Data/Enums/MediaType.cs @@ -0,0 +1,32 @@ +namespace Jellyfin.Data.Enums; + +/// <summary> +/// Media types. +/// </summary> +public enum MediaType +{ + /// <summary> + /// Unknown media type. + /// </summary> + Unknown = 0, + + /// <summary> + /// Video media. + /// </summary> + Video = 1, + + /// <summary> + /// Audio media. + /// </summary> + Audio = 2, + + /// <summary> + /// Photo media. + /// </summary> + Photo = 3, + + /// <summary> + /// Book media. + /// </summary> + Book = 4 +} diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs index 27726a57a6..8a33383e3a 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs @@ -2,6 +2,7 @@ using System.Globalization; using System.Threading.Tasks; using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Events; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Activity; @@ -89,14 +90,14 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session return name; } - private static string GetPlaybackNotificationType(string mediaType) + private static string GetPlaybackNotificationType(MediaType mediaType) { - if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) + if (mediaType == MediaType.Audio) { return NotificationType.AudioPlayback.ToString(); } - if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) + if (mediaType == MediaType.Video) { return NotificationType.VideoPlayback.ToString(); } diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs index 6b16477aa7..4c2effc2e3 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs @@ -2,6 +2,7 @@ using System.Globalization; using System.Threading.Tasks; using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Events; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Activity; @@ -97,14 +98,14 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session return name; } - private static string? GetPlaybackStoppedNotificationType(string mediaType) + private static string? GetPlaybackStoppedNotificationType(MediaType mediaType) { - if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) + if (mediaType == MediaType.Audio) { return NotificationType.AudioPlaybackStopped.ToString(); } - if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) + if (mediaType == MediaType.Video) { return NotificationType.VideoPlaybackStopped.ToString(); } diff --git a/MediaBrowser.Controller/Entities/Audio/Audio.cs b/MediaBrowser.Controller/Entities/Audio/Audio.cs index c7216a3209..243d2f04f2 100644 --- a/MediaBrowser.Controller/Entities/Audio/Audio.cs +++ b/MediaBrowser.Controller/Entities/Audio/Audio.cs @@ -63,7 +63,7 @@ namespace MediaBrowser.Controller.Entities.Audio /// </summary> /// <value>The type of the media.</value> [JsonIgnore] - public override string MediaType => Model.Entities.MediaType.Audio; + public override MediaType MediaType => MediaType.Audio; public override double GetDefaultPrimaryImageAspectRatio() { diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 9f3e8eec96..f83065a01b 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -422,7 +422,7 @@ namespace MediaBrowser.Controller.Entities /// </summary> /// <value>The type of the media.</value> [JsonIgnore] - public virtual string MediaType => null; + public virtual MediaType MediaType => MediaType.Unknown; [JsonIgnore] public virtual string[] PhysicalLocations diff --git a/MediaBrowser.Controller/Entities/Book.cs b/MediaBrowser.Controller/Entities/Book.cs index d75beb06da..63f9180fd6 100644 --- a/MediaBrowser.Controller/Entities/Book.cs +++ b/MediaBrowser.Controller/Entities/Book.cs @@ -18,7 +18,7 @@ namespace MediaBrowser.Controller.Entities } [JsonIgnore] - public override string MediaType => Model.Entities.MediaType.Book; + public override MediaType MediaType => MediaType.Book; public override bool SupportsPlayedStatus => true; diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index a51299284b..2de974349f 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -36,7 +36,7 @@ namespace MediaBrowser.Controller.Entities ImageTypes = Array.Empty<ImageType>(); IncludeItemTypes = Array.Empty<BaseItemKind>(); ItemIds = Array.Empty<Guid>(); - MediaTypes = Array.Empty<string>(); + MediaTypes = Array.Empty<MediaType>(); MinSimilarityScore = 20; OfficialRatings = Array.Empty<string>(); OrderBy = Array.Empty<(string, SortOrder)>(); @@ -86,7 +86,7 @@ namespace MediaBrowser.Controller.Entities public bool? IncludeItemsByName { get; set; } - public string[] MediaTypes { get; set; } + public MediaType[] MediaTypes { get; set; } public BaseItemKind[] IncludeItemTypes { get; set; } diff --git a/MediaBrowser.Controller/Entities/Photo.cs b/MediaBrowser.Controller/Entities/Photo.cs index ba6ce189ac..cb9feacd38 100644 --- a/MediaBrowser.Controller/Entities/Photo.cs +++ b/MediaBrowser.Controller/Entities/Photo.cs @@ -3,6 +3,7 @@ #pragma warning disable CS1591 using System.Text.Json.Serialization; +using Jellyfin.Data.Enums; using MediaBrowser.Model.Drawing; namespace MediaBrowser.Controller.Entities @@ -13,7 +14,7 @@ namespace MediaBrowser.Controller.Entities public override bool SupportsLocalMetadata => false; [JsonIgnore] - public override string MediaType => Model.Entities.MediaType.Photo; + public override MediaType MediaType => MediaType.Photo; [JsonIgnore] public override Folder LatestItemsIndexContainer => AlbumEntity; diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs index c276ab463a..5f187e64e1 100644 --- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -476,7 +476,7 @@ namespace MediaBrowser.Controller.Entities public static bool Filter(BaseItem item, User user, InternalItemsQuery query, IUserDataManager userDataManager, ILibraryManager libraryManager) { - if (query.MediaTypes.Length > 0 && !query.MediaTypes.Contains(item.MediaType ?? string.Empty, StringComparison.OrdinalIgnoreCase)) + if (query.MediaTypes.Length > 0 && !query.MediaTypes.Contains(item.MediaType)) { return false; } diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs index 9f685b7e2e..be2eb4d288 100644 --- a/MediaBrowser.Controller/Entities/Video.cs +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; @@ -256,7 +257,7 @@ namespace MediaBrowser.Controller.Entities /// </summary> /// <value>The type of the media.</value> [JsonIgnore] - public override string MediaType => Model.Entities.MediaType.Video; + public override MediaType MediaType => MediaType.Video; public override List<string> GetUserDataKeys() { diff --git a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs index f11e3c8f68..3c2cf8e3d2 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs @@ -44,7 +44,7 @@ namespace MediaBrowser.Controller.LiveTv public override LocationType LocationType => LocationType.Remote; [JsonIgnore] - public override string MediaType => ChannelType == ChannelType.Radio ? Model.Entities.MediaType.Audio : Model.Entities.MediaType.Video; + public override MediaType MediaType => ChannelType == ChannelType.Radio ? MediaType.Audio : MediaType.Video; [JsonIgnore] public bool IsMovie { get; set; } diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs index 498df5ab06..ca032e7f6e 100644 --- a/MediaBrowser.Controller/Playlists/Playlist.cs +++ b/MediaBrowser.Controller/Playlists/Playlist.cs @@ -69,7 +69,7 @@ namespace MediaBrowser.Controller.Playlists public override bool SupportsInheritedParentImages => false; [JsonIgnore] - public override bool SupportsPlayedStatus => string.Equals(MediaType, "Video", StringComparison.OrdinalIgnoreCase); + public override bool SupportsPlayedStatus => MediaType == Jellyfin.Data.Enums.MediaType.Video; [JsonIgnore] public override bool AlwaysScanInternalMetadataPath => true; @@ -80,10 +80,10 @@ namespace MediaBrowser.Controller.Playlists [JsonIgnore] public override bool IsPreSorted => true; - public string PlaylistMediaType { get; set; } + public MediaType PlaylistMediaType { get; set; } [JsonIgnore] - public override string MediaType => PlaylistMediaType; + public override MediaType MediaType => PlaylistMediaType; [JsonIgnore] private bool IsSharedItem @@ -107,9 +107,9 @@ namespace MediaBrowser.Controller.Playlists return System.IO.Path.HasExtension(path) && !Directory.Exists(path); } - public void SetMediaType(string value) + public void SetMediaType(MediaType? value) { - PlaylistMediaType = value; + PlaylistMediaType = value ?? MediaType.Unknown; } public override double GetDefaultPrimaryImageAspectRatio() @@ -167,7 +167,7 @@ namespace MediaBrowser.Controller.Playlists return base.GetChildren(user, true, query); } - public static List<BaseItem> GetPlaylistItems(string playlistMediaType, IEnumerable<BaseItem> inputItems, User user, DtoOptions options) + public static List<BaseItem> GetPlaylistItems(MediaType playlistMediaType, IEnumerable<BaseItem> inputItems, User user, DtoOptions options) { if (user is not null) { @@ -185,7 +185,7 @@ namespace MediaBrowser.Controller.Playlists return list; } - private static IEnumerable<BaseItem> GetPlaylistItems(BaseItem item, User user, string mediaType, DtoOptions options) + private static IEnumerable<BaseItem> GetPlaylistItems(BaseItem item, User user, MediaType mediaType, DtoOptions options) { if (item is MusicGenre musicGenre) { diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs index 25bf23d61b..07ad529b48 100644 --- a/MediaBrowser.Controller/Session/SessionInfo.cs +++ b/MediaBrowser.Controller/Session/SessionInfo.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Session; @@ -60,13 +61,13 @@ namespace MediaBrowser.Controller.Session /// Gets the playable media types. /// </summary> /// <value>The playable media types.</value> - public IReadOnlyList<string> PlayableMediaTypes + public IReadOnlyList<MediaType> PlayableMediaTypes { get { if (Capabilities is null) { - return Array.Empty<string>(); + return Array.Empty<MediaType>(); } return Capabilities.PlayableMediaTypes; diff --git a/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs index 879a3616bd..e0277870d1 100644 --- a/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs @@ -1,5 +1,7 @@ +using System; using System.Collections.Generic; using System.Xml; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Playlists; @@ -31,7 +33,11 @@ namespace MediaBrowser.LocalMetadata.Parsers switch (reader.Name) { case "PlaylistMediaType": - item.PlaylistMediaType = reader.ReadNormalizedString(); + if (Enum.TryParse<MediaType>(reader.ReadNormalizedString(), out var mediaType)) + { + item.PlaylistMediaType = mediaType; + } + break; case "PlaylistItems": diff --git a/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs index 847add07f7..3f018cae9b 100644 --- a/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs +++ b/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs @@ -1,6 +1,7 @@ using System.IO; using System.Threading.Tasks; using System.Xml; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -48,12 +49,12 @@ namespace MediaBrowser.LocalMetadata.Savers { var game = (Playlist)item; - if (string.IsNullOrEmpty(game.PlaylistMediaType)) + if (game.PlaylistMediaType == MediaType.Unknown) { return Task.CompletedTask; } - return writer.WriteElementStringAsync(null, "PlaylistMediaType", null, game.PlaylistMediaType); + return writer.WriteElementStringAsync(null, "PlaylistMediaType", null, game.PlaylistMediaType.ToString()); } /// <inheritdoc /> diff --git a/MediaBrowser.Model/Dlna/DeviceProfile.cs b/MediaBrowser.Model/Dlna/DeviceProfile.cs index 07bb002eae..71d0896a70 100644 --- a/MediaBrowser.Model/Dlna/DeviceProfile.cs +++ b/MediaBrowser.Model/Dlna/DeviceProfile.cs @@ -1,6 +1,7 @@ #pragma warning disable CA1819 // Properties should not return arrays using System; using System.ComponentModel; +using System.Linq; using System.Xml.Serialization; using Jellyfin.Data.Enums; using Jellyfin.Extensions; @@ -227,9 +228,12 @@ namespace MediaBrowser.Model.Dlna /// The GetSupportedMediaTypes. /// </summary> /// <returns>The .</returns> - public string[] GetSupportedMediaTypes() + public MediaType[] GetSupportedMediaTypes() { - return ContainerProfile.SplitValue(SupportedMediaTypes); + return ContainerProfile.SplitValue(SupportedMediaTypes) + .Select(m => Enum.TryParse<MediaType>(m, out var parsed) ? parsed : MediaType.Unknown) + .Where(m => m != MediaType.Unknown) + .ToArray(); } /// <summary> diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs index 8fab1ca6d6..edbf2d9d4d 100644 --- a/MediaBrowser.Model/Dto/BaseItemDto.cs +++ b/MediaBrowser.Model/Dto/BaseItemDto.cs @@ -584,7 +584,7 @@ namespace MediaBrowser.Model.Dto /// Gets or sets the type of the media. /// </summary> /// <value>The type of the media.</value> - public string MediaType { get; set; } + public MediaType MediaType { get; set; } /// <summary> /// Gets or sets the end date. diff --git a/MediaBrowser.Model/Entities/MediaType.cs b/MediaBrowser.Model/Entities/MediaType.cs deleted file mode 100644 index dd2ae810be..0000000000 --- a/MediaBrowser.Model/Entities/MediaType.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace MediaBrowser.Model.Entities -{ - /// <summary> - /// Class MediaType. - /// </summary> - public static class MediaType - { - /// <summary> - /// The video. - /// </summary> - public const string Video = "Video"; - - /// <summary> - /// The audio. - /// </summary> - public const string Audio = "Audio"; - - /// <summary> - /// The photo. - /// </summary> - public const string Photo = "Photo"; - - /// <summary> - /// The book. - /// </summary> - public const string Book = "Book"; - } -} diff --git a/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs b/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs index 8472697164..62d496d047 100644 --- a/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs +++ b/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Jellyfin.Data.Enums; using MediaBrowser.Model.Entities; namespace MediaBrowser.Model.Playlists; @@ -22,7 +23,7 @@ public class PlaylistCreationRequest /// <summary> /// Gets or sets the media type. /// </summary> - public string? MediaType { get; set; } + public MediaType? MediaType { get; set; } /// <summary> /// Gets or sets the user id. diff --git a/MediaBrowser.Model/Search/SearchHint.cs b/MediaBrowser.Model/Search/SearchHint.cs index 3fa7f3d565..fd911dbede 100644 --- a/MediaBrowser.Model/Search/SearchHint.cs +++ b/MediaBrowser.Model/Search/SearchHint.cs @@ -16,7 +16,7 @@ namespace MediaBrowser.Model.Search { Name = string.Empty; MatchedTerm = string.Empty; - MediaType = string.Empty; + MediaType = Jellyfin.Data.Enums.MediaType.Unknown; Artists = Array.Empty<string>(); } @@ -115,7 +115,7 @@ namespace MediaBrowser.Model.Search /// Gets or sets the type of the media. /// </summary> /// <value>The type of the media.</value> - public string MediaType { get; set; } + public MediaType MediaType { get; set; } /// <summary> /// Gets or sets the start date. diff --git a/MediaBrowser.Model/Search/SearchQuery.cs b/MediaBrowser.Model/Search/SearchQuery.cs index 1caed827f3..b91fd86571 100644 --- a/MediaBrowser.Model/Search/SearchQuery.cs +++ b/MediaBrowser.Model/Search/SearchQuery.cs @@ -16,7 +16,7 @@ namespace MediaBrowser.Model.Search IncludePeople = true; IncludeStudios = true; - MediaTypes = Array.Empty<string>(); + MediaTypes = Array.Empty<MediaType>(); IncludeItemTypes = Array.Empty<BaseItemKind>(); ExcludeItemTypes = Array.Empty<BaseItemKind>(); } @@ -55,7 +55,7 @@ namespace MediaBrowser.Model.Search public bool IncludeArtists { get; set; } - public string[] MediaTypes { get; set; } + public MediaType[] MediaTypes { get; set; } public BaseItemKind[] IncludeItemTypes { get; set; } diff --git a/MediaBrowser.Model/Session/ClientCapabilities.cs b/MediaBrowser.Model/Session/ClientCapabilities.cs index d692906c64..7fefce9cd5 100644 --- a/MediaBrowser.Model/Session/ClientCapabilities.cs +++ b/MediaBrowser.Model/Session/ClientCapabilities.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Jellyfin.Data.Enums; using MediaBrowser.Model.Dlna; namespace MediaBrowser.Model.Session @@ -11,12 +12,12 @@ namespace MediaBrowser.Model.Session { public ClientCapabilities() { - PlayableMediaTypes = Array.Empty<string>(); + PlayableMediaTypes = Array.Empty<MediaType>(); SupportedCommands = Array.Empty<GeneralCommandType>(); SupportsPersistentIdentifier = true; } - public IReadOnlyList<string> PlayableMediaTypes { get; set; } + public IReadOnlyList<MediaType> PlayableMediaTypes { get; set; } public IReadOnlyList<GeneralCommandType> SupportedCommands { get; set; } diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs index f21939d2a5..6eb75891aa 100644 --- a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs +++ b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs @@ -97,7 +97,7 @@ namespace MediaBrowser.Providers.MediaInfo { var query = new InternalItemsQuery { - MediaTypes = new string[] { MediaType.Video }, + MediaTypes = new[] { MediaType.Video }, IsVirtualItem = false, IncludeItemTypes = types, DtoOptions = new DtoOptions(true), From a88e13a677623d479cc32a6e436e0a193a64b78e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 10 Oct 2023 15:58:06 -0600 Subject: [PATCH 699/858] Update dotnet monorepo to v7.0.12 (#10382) * Update dotnet monorepo to v7.0.12 * update sdk --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Cody Robibero <cody@robibe.ro> --- Directory.Packages.props | 16 ++++++++-------- deployment/Dockerfile.centos.amd64 | 2 +- deployment/Dockerfile.fedora.amd64 | 2 +- deployment/Dockerfile.ubuntu.amd64 | 2 +- deployment/Dockerfile.ubuntu.arm64 | 2 +- deployment/Dockerfile.ubuntu.armhf | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 55b03cdcee..33736482c8 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -27,13 +27,13 @@ <PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" /> <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.12" /> <PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" /> - <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.11" /> + <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.12" /> <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" /> - <PackageVersion Include="Microsoft.Data.Sqlite" Version="7.0.11" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.11" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.11" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.11" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.11" /> + <PackageVersion Include="Microsoft.Data.Sqlite" Version="7.0.12" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.12" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.12" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.12" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.12" /> <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" /> @@ -42,8 +42,8 @@ <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" /> - <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.11" /> - <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.11" /> + <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.12" /> + <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.12" /> <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Http" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" /> diff --git a/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64 index 28cf585e98..6c2086bee3 100644 --- a/deployment/Dockerfile.centos.amd64 +++ b/deployment/Dockerfile.centos.amd64 @@ -13,7 +13,7 @@ RUN yum update -yq \ && yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget # Install DotNET SDK -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/61f29db0-10a5-4816-8fd8-ca2f71beaea3/e15fb7288eb5bc0053b91ea7b0bfd580/dotnet-sdk-7.0.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c4b5aad8-a416-436b-927c-3ebd5a9793ad/38efd1b64c8edc7c5f13699dd0be54e1/dotnet-sdk-7.0.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.fedora.amd64 b/deployment/Dockerfile.fedora.amd64 index 0e8166f46c..16002f7905 100644 --- a/deployment/Dockerfile.fedora.amd64 +++ b/deployment/Dockerfile.fedora.amd64 @@ -12,7 +12,7 @@ RUN dnf update -yq \ && dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd wget make # Install DotNET SDK -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/61f29db0-10a5-4816-8fd8-ca2f71beaea3/e15fb7288eb5bc0053b91ea7b0bfd580/dotnet-sdk-7.0.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c4b5aad8-a416-436b-927c-3ebd5a9793ad/38efd1b64c8edc7c5f13699dd0be54e1/dotnet-sdk-7.0.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64 index 04c748d096..2befb4b55b 100644 --- a/deployment/Dockerfile.ubuntu.amd64 +++ b/deployment/Dockerfile.ubuntu.amd64 @@ -17,7 +17,7 @@ RUN apt-get update -yqq \ libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/61f29db0-10a5-4816-8fd8-ca2f71beaea3/e15fb7288eb5bc0053b91ea7b0bfd580/dotnet-sdk-7.0.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c4b5aad8-a416-436b-927c-3ebd5a9793ad/38efd1b64c8edc7c5f13699dd0be54e1/dotnet-sdk-7.0.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.arm64 b/deployment/Dockerfile.ubuntu.arm64 index 5bc197679b..497a7a3ddd 100644 --- a/deployment/Dockerfile.ubuntu.arm64 +++ b/deployment/Dockerfile.ubuntu.arm64 @@ -16,7 +16,7 @@ RUN apt-get update -yqq \ mmv build-essential lsb-release # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/61f29db0-10a5-4816-8fd8-ca2f71beaea3/e15fb7288eb5bc0053b91ea7b0bfd580/dotnet-sdk-7.0.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c4b5aad8-a416-436b-927c-3ebd5a9793ad/38efd1b64c8edc7c5f13699dd0be54e1/dotnet-sdk-7.0.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.armhf b/deployment/Dockerfile.ubuntu.armhf index fab869a6b8..fae7b209fa 100644 --- a/deployment/Dockerfile.ubuntu.armhf +++ b/deployment/Dockerfile.ubuntu.armhf @@ -16,7 +16,7 @@ RUN apt-get update -yqq \ mmv build-essential lsb-release # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/61f29db0-10a5-4816-8fd8-ca2f71beaea3/e15fb7288eb5bc0053b91ea7b0bfd580/dotnet-sdk-7.0.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c4b5aad8-a416-436b-927c-3ebd5a9793ad/38efd1b64c8edc7c5f13699dd0be54e1/dotnet-sdk-7.0.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet From dc27d8f9cd3b6cf280ba8e5e2520db387f651fb4 Mon Sep 17 00:00:00 2001 From: Tim Eisele <Shadowghost@users.noreply.github.com> Date: Wed, 11 Oct 2023 00:02:37 +0200 Subject: [PATCH 700/858] Refactor URI overrides (#10051) --- Emby.Dlna/Main/DlnaEntryPoint.cs | 9 +- .../ApplicationHost.cs | 6 +- .../EntryPoints/UdpServerEntryPoint.cs | 29 ++- .../Net/SocketFactory.cs | 33 ++- Emby.Server.Implementations/Udp/UdpServer.cs | 11 +- .../Extensions/NetworkExtensions.cs | 13 +- Jellyfin.Networking/Manager/NetworkManager.cs | 244 +++++++++++------- .../ApiServiceCollectionExtensions.cs | 2 +- .../Extensions/WebHostBuilderExtensions.cs | 3 +- MediaBrowser.Model/Net/IPData.cs | 5 + .../Net/PublishedServerUriOverride.cs | 42 +++ RSSDP/SsdpCommunicationsServer.cs | 128 +++++---- RSSDP/SsdpDeviceLocator.cs | 42 ++- RSSDP/SsdpDevicePublisher.cs | 89 ++++--- .../NetworkExtensionsTests.cs | 1 - .../NetworkManagerTests.cs | 10 +- .../NetworkParseTests.cs | 39 +-- .../ParseNetworkTests.cs | 5 +- 18 files changed, 433 insertions(+), 278 deletions(-) create mode 100644 MediaBrowser.Model/Net/PublishedServerUriOverride.cs diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index 1a4bec398c..83b0ef3164 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -201,9 +201,10 @@ namespace Emby.Dlna.Main { if (_communicationsServer is null) { - var enableMultiSocketBinding = OperatingSystem.IsWindows() || OperatingSystem.IsLinux(); - - _communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding) + _communicationsServer = new SsdpCommunicationsServer( + _socketFactory, + _networkManager, + _logger) { IsShared = true }; @@ -213,7 +214,7 @@ namespace Emby.Dlna.Main } catch (Exception ex) { - _logger.LogError(ex, "Error starting ssdp handlers"); + _logger.LogError(ex, "Error starting SSDP handlers"); } } diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 3253ea0267..c247178ee9 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -450,7 +450,7 @@ namespace Emby.Server.Implementations ConfigurationManager.AddParts(GetExports<IConfigurationFactory>()); - NetManager = new NetworkManager(ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>()); + NetManager = new NetworkManager(ConfigurationManager, _startupConfig, LoggerFactory.CreateLogger<NetworkManager>()); // Initialize runtime stat collection if (ConfigurationManager.Configuration.EnableMetrics) @@ -913,7 +913,7 @@ namespace Emby.Server.Implementations /// <inheritdoc/> public string GetSmartApiUrl(HttpRequest request) { - // Return the host in the HTTP request as the API url + // Return the host in the HTTP request as the API URL if not configured otherwise if (ConfigurationManager.GetNetworkConfiguration().EnablePublishedServerUriByRequest) { int? requestPort = request.Host.Port; @@ -948,7 +948,7 @@ namespace Emby.Server.Implementations public string GetApiUrlForLocalAccess(IPAddress ipAddress = null, bool allowHttps = true) { // With an empty source, the port will be null - var smart = NetManager.GetBindAddress(ipAddress, out _, true); + var smart = NetManager.GetBindAddress(ipAddress, out _, false); var scheme = !allowHttps ? Uri.UriSchemeHttp : null; int? port = !allowHttps ? HttpPort : null; return GetLocalApiUrl(smart, scheme, port); diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs index 54191649da..2c03f9ffda 100644 --- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs +++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs @@ -18,7 +18,7 @@ using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.EntryPoints { /// <summary> - /// Class UdpServerEntryPoint. + /// Class responsible for registering all UDP broadcast endpoints and their handlers. /// </summary> public sealed class UdpServerEntryPoint : IServerEntryPoint { @@ -35,7 +35,6 @@ namespace Emby.Server.Implementations.EntryPoints private readonly IConfiguration _config; private readonly IConfigurationManager _configurationManager; private readonly INetworkManager _networkManager; - private readonly bool _enableMultiSocketBinding; /// <summary> /// The UDP server. @@ -65,7 +64,6 @@ namespace Emby.Server.Implementations.EntryPoints _configurationManager = configurationManager; _networkManager = networkManager; _udpServers = new List<UdpServer>(); - _enableMultiSocketBinding = OperatingSystem.IsWindows() || OperatingSystem.IsLinux(); } /// <inheritdoc /> @@ -80,14 +78,16 @@ namespace Emby.Server.Implementations.EntryPoints try { - if (_enableMultiSocketBinding) + // Linux needs to bind to the broadcast addresses to get broadcast traffic + // Windows receives broadcast fine when binding to just the interface, it is unable to bind to broadcast addresses + if (OperatingSystem.IsLinux()) { - // Add global broadcast socket + // Add global broadcast listener var server = new UdpServer(_logger, _appHost, _config, IPAddress.Broadcast, PortNumber); server.Start(_cancellationTokenSource.Token); _udpServers.Add(server); - // Add bind address specific broadcast sockets + // Add bind address specific broadcast listeners // IPv6 is currently unsupported var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork); foreach (var intf in validInterfaces) @@ -102,9 +102,18 @@ namespace Emby.Server.Implementations.EntryPoints } else { - var server = new UdpServer(_logger, _appHost, _config, IPAddress.Any, PortNumber); - server.Start(_cancellationTokenSource.Token); - _udpServers.Add(server); + // Add bind address specific broadcast listeners + // IPv6 is currently unsupported + var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork); + foreach (var intf in validInterfaces) + { + var intfAddress = intf.Address; + _logger.LogDebug("Binding UDP server to {Address} on port {PortNumber}", intfAddress, PortNumber); + + var server = new UdpServer(_logger, _appHost, _config, intfAddress, PortNumber); + server.Start(_cancellationTokenSource.Token); + _udpServers.Add(server); + } } } catch (SocketException ex) @@ -119,7 +128,7 @@ namespace Emby.Server.Implementations.EntryPoints { if (_disposed) { - throw new ObjectDisposedException(this.GetType().Name); + throw new ObjectDisposedException(GetType().Name); } } diff --git a/Emby.Server.Implementations/Net/SocketFactory.cs b/Emby.Server.Implementations/Net/SocketFactory.cs index 51e92953df..505984cfb2 100644 --- a/Emby.Server.Implementations/Net/SocketFactory.cs +++ b/Emby.Server.Implementations/Net/SocketFactory.cs @@ -1,12 +1,15 @@ -#pragma warning disable CS1591 - using System; +using System.Linq; using System.Net; +using System.Net.NetworkInformation; using System.Net.Sockets; using MediaBrowser.Model.Net; namespace Emby.Server.Implementations.Net { + /// <summary> + /// Factory class to create different kinds of sockets. + /// </summary> public class SocketFactory : ISocketFactory { /// <inheritdoc /> @@ -38,7 +41,8 @@ namespace Emby.Server.Implementations.Net /// <inheritdoc /> public Socket CreateSsdpUdpSocket(IPData bindInterface, int localPort) { - ArgumentNullException.ThrowIfNull(bindInterface.Address); + var interfaceAddress = bindInterface.Address; + ArgumentNullException.ThrowIfNull(interfaceAddress); if (localPort < 0) { @@ -49,7 +53,7 @@ namespace Emby.Server.Implementations.Net try { socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); - socket.Bind(new IPEndPoint(bindInterface.Address, localPort)); + socket.Bind(new IPEndPoint(interfaceAddress, localPort)); return socket; } @@ -82,16 +86,25 @@ namespace Emby.Server.Implementations.Net try { - var interfaceIndex = bindInterface.Index; - var interfaceIndexSwapped = (int)IPAddress.HostToNetworkOrder(interfaceIndex); - socket.MulticastLoopback = false; socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.PacketInformation, true); socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, multicastTimeToLive); - socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastInterface, interfaceIndexSwapped); - socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress, interfaceIndex)); - socket.Bind(new IPEndPoint(multicastAddress, localPort)); + + if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) + { + socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress)); + socket.Bind(new IPEndPoint(multicastAddress, localPort)); + } + else + { + // Only create socket if interface supports multicast + var interfaceIndex = bindInterface.Index; + var interfaceIndexSwapped = IPAddress.HostToNetworkOrder(interfaceIndex); + + socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress, interfaceIndex)); + socket.Bind(new IPEndPoint(bindIPAddress, localPort)); + } return socket; } diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs index a3bbd6df0c..10376ed76c 100644 --- a/Emby.Server.Implementations/Udp/UdpServer.cs +++ b/Emby.Server.Implementations/Udp/UdpServer.cs @@ -52,7 +52,10 @@ namespace Emby.Server.Implementations.Udp _endpoint = new IPEndPoint(bindAddress, port); - _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp) + { + MulticastLoopback = false, + }; _udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); } @@ -74,6 +77,7 @@ namespace Emby.Server.Implementations.Udp try { + _logger.LogDebug("Sending AutoDiscovery response"); await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint, cancellationToken).ConfigureAwait(false); } catch (SocketException ex) @@ -99,7 +103,8 @@ namespace Emby.Server.Implementations.Udp { try { - var result = await _udpSocket.ReceiveFromAsync(_receiveBuffer, SocketFlags.None, _endpoint, cancellationToken).ConfigureAwait(false); + var endpoint = (EndPoint)new IPEndPoint(IPAddress.Any, 0); + var result = await _udpSocket.ReceiveFromAsync(_receiveBuffer, endpoint, cancellationToken).ConfigureAwait(false); var text = Encoding.UTF8.GetString(_receiveBuffer, 0, result.ReceivedBytes); if (text.Contains("who is JellyfinServer?", StringComparison.OrdinalIgnoreCase)) { @@ -112,7 +117,7 @@ namespace Emby.Server.Implementations.Udp } catch (OperationCanceledException) { - // Don't throw + _logger.LogDebug("Broadcast socket operation cancelled"); } } } diff --git a/Jellyfin.Networking/Extensions/NetworkExtensions.cs b/Jellyfin.Networking/Extensions/NetworkExtensions.cs index e45fa3bcb7..2d921a4826 100644 --- a/Jellyfin.Networking/Extensions/NetworkExtensions.cs +++ b/Jellyfin.Networking/Extensions/NetworkExtensions.cs @@ -231,12 +231,12 @@ public static partial class NetworkExtensions } else if (address.AddressFamily == AddressFamily.InterNetwork) { - result = new IPNetwork(address, Network.MinimumIPv4PrefixSize); + result = address.Equals(IPAddress.Any) ? Network.IPv4Any : new IPNetwork(address, Network.MinimumIPv4PrefixSize); return true; } else if (address.AddressFamily == AddressFamily.InterNetworkV6) { - result = new IPNetwork(address, Network.MinimumIPv6PrefixSize); + result = address.Equals(IPAddress.IPv6Any) ? Network.IPv6Any : new IPNetwork(address, Network.MinimumIPv6PrefixSize); return true; } } @@ -284,12 +284,15 @@ public static partial class NetworkExtensions if (hosts.Count <= 2) { + var firstPart = hosts[0]; + // Is hostname or hostname:port - if (FqdnGeneratedRegex().IsMatch(hosts[0])) + if (FqdnGeneratedRegex().IsMatch(firstPart)) { try { - addresses = Dns.GetHostAddresses(hosts[0]); + // .NET automatically filters only supported returned addresses based on OS support. + addresses = Dns.GetHostAddresses(firstPart); return true; } catch (SocketException) @@ -299,7 +302,7 @@ public static partial class NetworkExtensions } // Is an IPv4 or IPv4:port - if (IPAddress.TryParse(hosts[0].AsSpan().LeftPart('/'), out var address)) + if (IPAddress.TryParse(firstPart.AsSpan().LeftPart('/'), out var address)) { if (((address.AddressFamily == AddressFamily.InterNetwork) && (!isIPv4Enabled && isIPv6Enabled)) || ((address.AddressFamily == AddressFamily.InterNetworkV6) && (isIPv4Enabled && !isIPv6Enabled))) diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index f20e285263..9c59500d77 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -15,7 +15,9 @@ using MediaBrowser.Common.Net; using MediaBrowser.Model.Net; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; +using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; namespace Jellyfin.Networking.Manager { @@ -33,12 +35,14 @@ namespace Jellyfin.Networking.Manager private readonly IConfigurationManager _configurationManager; + private readonly IConfiguration _startupConfig; + private readonly object _networkEventLock; /// <summary> /// Holds the published server URLs and the IPs to use them on. /// </summary> - private IReadOnlyDictionary<IPData, string> _publishedServerUrls; + private IReadOnlyList<PublishedServerUriOverride> _publishedServerUrls; private IReadOnlyList<IPNetwork> _remoteAddressFilter; @@ -76,20 +80,22 @@ namespace Jellyfin.Networking.Manager /// <summary> /// Initializes a new instance of the <see cref="NetworkManager"/> class. /// </summary> - /// <param name="configurationManager">IServerConfigurationManager instance.</param> + /// <param name="configurationManager">The <see cref="IConfigurationManager"/> instance.</param> + /// <param name="startupConfig">The <see cref="IConfiguration"/> instance holding startup parameters.</param> /// <param name="logger">Logger to use for messages.</param> #pragma warning disable CS8618 // Non-nullable field is uninitialized. : Values are set in UpdateSettings function. Compiler doesn't yet recognise this. - public NetworkManager(IConfigurationManager configurationManager, ILogger<NetworkManager> logger) + public NetworkManager(IConfigurationManager configurationManager, IConfiguration startupConfig, ILogger<NetworkManager> logger) { ArgumentNullException.ThrowIfNull(logger); ArgumentNullException.ThrowIfNull(configurationManager); _logger = logger; _configurationManager = configurationManager; + _startupConfig = startupConfig; _initLock = new(); _interfaces = new List<IPData>(); _macAddresses = new List<PhysicalAddress>(); - _publishedServerUrls = new Dictionary<IPData, string>(); + _publishedServerUrls = new List<PublishedServerUriOverride>(); _networkEventLock = new object(); _remoteAddressFilter = new List<IPNetwork>(); @@ -130,7 +136,7 @@ namespace Jellyfin.Networking.Manager /// <summary> /// Gets the Published server override list. /// </summary> - public IReadOnlyDictionary<IPData, string> PublishedServerUrls => _publishedServerUrls; + public IReadOnlyList<PublishedServerUriOverride> PublishedServerUrls => _publishedServerUrls; /// <inheritdoc/> public void Dispose() @@ -170,7 +176,6 @@ namespace Jellyfin.Networking.Manager { if (!_eventfire) { - _logger.LogDebug("Network Address Change Event."); // As network events tend to fire one after the other only fire once every second. _eventfire = true; OnNetworkChange(); @@ -193,11 +198,12 @@ namespace Jellyfin.Networking.Manager } else { - InitialiseInterfaces(); - InitialiseLan(networkConfig); + InitializeInterfaces(); + InitializeLan(networkConfig); EnforceBindSettings(networkConfig); } + PrintNetworkInformation(networkConfig); NetworkChanged?.Invoke(this, EventArgs.Empty); } finally @@ -210,7 +216,7 @@ namespace Jellyfin.Networking.Manager /// Generate a list of all the interface ip addresses and submasks where that are in the active/unknown state. /// Generate a list of all active mac addresses that aren't loopback addresses. /// </summary> - private void InitialiseInterfaces() + private void InitializeInterfaces() { lock (_initLock) { @@ -222,7 +228,7 @@ namespace Jellyfin.Networking.Manager try { var nics = NetworkInterface.GetAllNetworkInterfaces() - .Where(i => i.SupportsMulticast && i.OperationalStatus == OperationalStatus.Up); + .Where(i => i.OperationalStatus == OperationalStatus.Up); foreach (NetworkInterface adapter in nics) { @@ -242,34 +248,36 @@ namespace Jellyfin.Networking.Manager { if (IsIPv4Enabled && info.Address.AddressFamily == AddressFamily.InterNetwork) { - var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name); - interfaceObject.Index = ipProperties.GetIPv4Properties().Index; - interfaceObject.Name = adapter.Name; + var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name) + { + Index = ipProperties.GetIPv4Properties().Index, + Name = adapter.Name, + SupportsMulticast = adapter.SupportsMulticast + }; interfaces.Add(interfaceObject); } else if (IsIPv6Enabled && info.Address.AddressFamily == AddressFamily.InterNetworkV6) { - var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name); - interfaceObject.Index = ipProperties.GetIPv6Properties().Index; - interfaceObject.Name = adapter.Name; + var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name) + { + Index = ipProperties.GetIPv6Properties().Index, + Name = adapter.Name, + SupportsMulticast = adapter.SupportsMulticast + }; interfaces.Add(interfaceObject); } } } -#pragma warning disable CA1031 // Do not catch general exception types catch (Exception ex) -#pragma warning restore CA1031 // Do not catch general exception types { // Ignore error, and attempt to continue. _logger.LogError(ex, "Error encountered parsing interfaces."); } } } -#pragma warning disable CA1031 // Do not catch general exception types catch (Exception ex) -#pragma warning restore CA1031 // Do not catch general exception types { _logger.LogError(ex, "Error obtaining interfaces."); } @@ -279,14 +287,14 @@ namespace Jellyfin.Networking.Manager { _logger.LogWarning("No interface information available. Using loopback interface(s)."); - if (IsIPv4Enabled && !IsIPv6Enabled) + if (IsIPv4Enabled) { - interfaces.Add(new IPData(IPAddress.Loopback, new IPNetwork(IPAddress.Loopback, 8), "lo")); + interfaces.Add(new IPData(IPAddress.Loopback, Network.IPv4RFC5735Loopback, "lo")); } - if (!IsIPv4Enabled && IsIPv6Enabled) + if (IsIPv6Enabled) { - interfaces.Add(new IPData(IPAddress.IPv6Loopback, new IPNetwork(IPAddress.IPv6Loopback, 128), "lo")); + interfaces.Add(new IPData(IPAddress.IPv6Loopback, Network.IPv6RFC4291Loopback, "lo")); } } @@ -299,9 +307,9 @@ namespace Jellyfin.Networking.Manager } /// <summary> - /// Initialises internal LAN cache. + /// Initializes internal LAN cache. /// </summary> - private void InitialiseLan(NetworkConfiguration config) + private void InitializeLan(NetworkConfiguration config) { lock (_initLock) { @@ -341,10 +349,6 @@ namespace Jellyfin.Networking.Manager _excludedSubnets = NetworkExtensions.TryParseToSubnets(subnets, out var excludedSubnets, true) ? excludedSubnets : new List<IPNetwork>(); - - _logger.LogInformation("Defined LAN addresses: {0}", _lanSubnets.Select(s => s.Prefix + "/" + s.PrefixLength)); - _logger.LogInformation("Defined LAN exclusions: {0}", _excludedSubnets.Select(s => s.Prefix + "/" + s.PrefixLength)); - _logger.LogInformation("Using LAN addresses: {0}", _lanSubnets.Where(s => !_excludedSubnets.Contains(s)).Select(s => s.Prefix + "/" + s.PrefixLength)); } } @@ -369,12 +373,12 @@ namespace Jellyfin.Networking.Manager .ToHashSet(); interfaces = interfaces.Where(x => bindAddresses.Contains(x.Address)).ToList(); - if (bindAddresses.Contains(IPAddress.Loopback)) + if (bindAddresses.Contains(IPAddress.Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.Loopback))) { interfaces.Add(new IPData(IPAddress.Loopback, Network.IPv4RFC5735Loopback, "lo")); } - if (bindAddresses.Contains(IPAddress.IPv6Loopback)) + if (bindAddresses.Contains(IPAddress.IPv6Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.IPv6Loopback))) { interfaces.Add(new IPData(IPAddress.IPv6Loopback, Network.IPv6RFC4291Loopback, "lo")); } @@ -409,15 +413,14 @@ namespace Jellyfin.Networking.Manager interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetworkV6); } - _logger.LogInformation("Using bind addresses: {0}", interfaces.OrderByDescending(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => x.Address)); _interfaces = interfaces; } } /// <summary> - /// Initialises the remote address values. + /// Initializes the remote address values. /// </summary> - private void InitialiseRemote(NetworkConfiguration config) + private void InitializeRemote(NetworkConfiguration config) { lock (_initLock) { @@ -455,13 +458,33 @@ namespace Jellyfin.Networking.Manager /// format is subnet=ipaddress|host|uri /// when subnet = 0.0.0.0, any external address matches. /// </summary> - private void InitialiseOverrides(NetworkConfiguration config) + private void InitializeOverrides(NetworkConfiguration config) { lock (_initLock) { - var publishedServerUrls = new Dictionary<IPData, string>(); - var overrides = config.PublishedServerUriBySubnet; + var publishedServerUrls = new List<PublishedServerUriOverride>(); + // Prefer startup configuration. + var startupOverrideKey = _startupConfig[AddressOverrideKey]; + if (!string.IsNullOrEmpty(startupOverrideKey)) + { + publishedServerUrls.Add( + new PublishedServerUriOverride( + new IPData(IPAddress.Any, Network.IPv4Any), + startupOverrideKey, + true, + true)); + publishedServerUrls.Add( + new PublishedServerUriOverride( + new IPData(IPAddress.IPv6Any, Network.IPv6Any), + startupOverrideKey, + true, + true)); + _publishedServerUrls = publishedServerUrls; + return; + } + + var overrides = config.PublishedServerUriBySubnet; foreach (var entry in overrides) { var parts = entry.Split('='); @@ -475,31 +498,70 @@ namespace Jellyfin.Networking.Manager var identifier = parts[0]; if (string.Equals(identifier, "all", StringComparison.OrdinalIgnoreCase)) { - publishedServerUrls[new IPData(IPAddress.Broadcast, null)] = replacement; + // Drop any other overrides in case an "all" override exists + publishedServerUrls.Clear(); + publishedServerUrls.Add( + new PublishedServerUriOverride( + new IPData(IPAddress.Any, Network.IPv4Any), + replacement, + true, + true)); + publishedServerUrls.Add( + new PublishedServerUriOverride( + new IPData(IPAddress.IPv6Any, Network.IPv6Any), + replacement, + true, + true)); + break; } else if (string.Equals(identifier, "external", StringComparison.OrdinalIgnoreCase)) { - publishedServerUrls[new IPData(IPAddress.Any, Network.IPv4Any)] = replacement; - publishedServerUrls[new IPData(IPAddress.IPv6Any, Network.IPv6Any)] = replacement; + publishedServerUrls.Add( + new PublishedServerUriOverride( + new IPData(IPAddress.Any, Network.IPv4Any), + replacement, + false, + true)); + publishedServerUrls.Add( + new PublishedServerUriOverride( + new IPData(IPAddress.IPv6Any, Network.IPv6Any), + replacement, + false, + true)); } else if (string.Equals(identifier, "internal", StringComparison.OrdinalIgnoreCase)) { foreach (var lan in _lanSubnets) { var lanPrefix = lan.Prefix; - publishedServerUrls[new IPData(lanPrefix, new IPNetwork(lanPrefix, lan.PrefixLength))] = replacement; + publishedServerUrls.Add( + new PublishedServerUriOverride( + new IPData(lanPrefix, new IPNetwork(lanPrefix, lan.PrefixLength)), + replacement, + true, + false)); } } else if (NetworkExtensions.TryParseToSubnet(identifier, out var result) && result is not null) { var data = new IPData(result.Prefix, result); - publishedServerUrls[data] = replacement; + publishedServerUrls.Add( + new PublishedServerUriOverride( + data, + replacement, + true, + true)); } else if (TryParseInterface(identifier, out var ifaces)) { foreach (var iface in ifaces) { - publishedServerUrls[iface] = replacement; + publishedServerUrls.Add( + new PublishedServerUriOverride( + iface, + replacement, + true, + true)); } } else @@ -521,7 +583,7 @@ namespace Jellyfin.Networking.Manager } /// <summary> - /// Reloads all settings and re-initialises the instance. + /// Reloads all settings and re-Initializes the instance. /// </summary> /// <param name="configuration">The <see cref="NetworkConfiguration"/> to use.</param> public void UpdateSettings(object configuration) @@ -531,12 +593,12 @@ namespace Jellyfin.Networking.Manager var config = (NetworkConfiguration)configuration; HappyEyeballs.HttpClientExtension.UseIPv6 = config.EnableIPv6; - InitialiseLan(config); - InitialiseRemote(config); + InitializeLan(config); + InitializeRemote(config); if (string.IsNullOrEmpty(MockNetworkSettings)) { - InitialiseInterfaces(); + InitializeInterfaces(); } else // Used in testing only. { @@ -552,8 +614,10 @@ namespace Jellyfin.Networking.Manager var index = int.Parse(parts[1], CultureInfo.InvariantCulture); if (address.AddressFamily == AddressFamily.InterNetwork || address.AddressFamily == AddressFamily.InterNetworkV6) { - var data = new IPData(address, subnet, parts[2]); - data.Index = index; + var data = new IPData(address, subnet, parts[2]) + { + Index = index + }; interfaces.Add(data); } } @@ -567,7 +631,9 @@ namespace Jellyfin.Networking.Manager } EnforceBindSettings(config); - InitialiseOverrides(config); + InitializeOverrides(config); + + PrintNetworkInformation(config, false); } /// <summary> @@ -672,20 +738,13 @@ namespace Jellyfin.Networking.Manager /// <inheritdoc/> public IReadOnlyList<IPData> GetAllBindInterfaces(bool individualInterfaces = false) { - if (_interfaces.Count != 0) + if (_interfaces.Count > 0 || individualInterfaces) { return _interfaces; } // No bind address and no exclusions, so listen on all interfaces. var result = new List<IPData>(); - - if (individualInterfaces) - { - result.AddRange(_interfaces); - return result; - } - if (IsIPv4Enabled && IsIPv6Enabled) { // Kestrel source code shows it uses Sockets.DualMode - so this also covers IPAddress.Any by default @@ -892,31 +951,34 @@ namespace Jellyfin.Networking.Manager bindPreference = string.Empty; int? port = null; - var validPublishedServerUrls = _publishedServerUrls.Where(x => x.Key.Address.Equals(IPAddress.Any) - || x.Key.Address.Equals(IPAddress.IPv6Any) - || x.Key.Subnet.Contains(source)) - .DistinctBy(x => x.Key) - .OrderBy(x => x.Key.Address.Equals(IPAddress.Any) - || x.Key.Address.Equals(IPAddress.IPv6Any)) + // Only consider subnets including the source IP, prefering specific overrides + List<PublishedServerUriOverride> validPublishedServerUrls; + if (!isInExternalSubnet) + { + // Only use matching internal subnets + // Prefer more specific (bigger subnet prefix) overrides + validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsInternalOverride && x.Data.Subnet.Contains(source)) + .OrderByDescending(x => x.Data.Subnet.PrefixLength) .ToList(); + } + else + { + // Only use matching external subnets + // Prefer more specific (bigger subnet prefix) overrides + validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsExternalOverride && x.Data.Subnet.Contains(source)) + .OrderByDescending(x => x.Data.Subnet.PrefixLength) + .ToList(); + } - // Check for user override. foreach (var data in validPublishedServerUrls) { - if (isInExternalSubnet && (data.Key.Address.Equals(IPAddress.Any) || data.Key.Address.Equals(IPAddress.IPv6Any))) - { - // External. - bindPreference = data.Value; - break; - } - - // Get address interface. - var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Key.Subnet.Contains(x.Address)); + // Get interface matching override subnet + var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Data.Subnet.Contains(x.Address)); if (intf?.Address is not null) { - // Match IP address. - bindPreference = data.Value; + // If matching interface is found, use override + bindPreference = data.OverrideUri; break; } } @@ -927,7 +989,7 @@ namespace Jellyfin.Networking.Manager return false; } - // Has it got a port defined? + // Handle override specifying port var parts = bindPreference.Split(':'); if (parts.Length > 1) { @@ -935,18 +997,12 @@ namespace Jellyfin.Networking.Manager { bindPreference = parts[0]; port = p; + _logger.LogDebug("{Source}: Matching bind address override found: {Address}:{Port}", source, bindPreference, port); + return true; } } - if (port is not null) - { - _logger.LogDebug("{Source}: Matching bind address override found: {Address}:{Port}", source, bindPreference, port); - } - else - { - _logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference); - } - + _logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference); return true; } @@ -1053,5 +1109,19 @@ namespace Jellyfin.Networking.Manager _logger.LogDebug("{Source}: Using first external interface as bind address: {Result}", source, result); return true; } + + private void PrintNetworkInformation(NetworkConfiguration config, bool debug = true) + { + var logLevel = debug ? LogLevel.Debug : LogLevel.Information; + if (_logger.IsEnabled(logLevel)) + { + _logger.Log(logLevel, "Defined LAN addresses: {0}", _lanSubnets.Select(s => s.Prefix + "/" + s.PrefixLength)); + _logger.Log(logLevel, "Defined LAN exclusions: {0}", _excludedSubnets.Select(s => s.Prefix + "/" + s.PrefixLength)); + _logger.Log(logLevel, "Using LAN addresses: {0}", _lanSubnets.Where(s => !_excludedSubnets.Contains(s)).Select(s => s.Prefix + "/" + s.PrefixLength)); + _logger.Log(logLevel, "Using bind addresses: {0}", _interfaces.OrderByDescending(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => x.Address)); + _logger.Log(logLevel, "Remote IP filter is {0}", config.IsRemoteIPFilterBlacklist ? "Blocklist" : "Allowlist"); + _logger.Log(logLevel, "Filter list: {0}", _remoteAddressFilter.Select(s => s.Prefix + "/" + s.PrefixLength)); + } + } } } diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 89dbbdd2fe..cb1680558f 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -282,7 +282,7 @@ namespace Jellyfin.Server.Extensions AddIPAddress(config, options, subnet.Prefix, subnet.PrefixLength); } } - else if (NetworkExtensions.TryParseHost(allowedProxies[i], out var addresses)) + else if (NetworkExtensions.TryParseHost(allowedProxies[i], out var addresses, config.EnableIPv4, config.EnableIPv6)) { foreach (var address in addresses) { diff --git a/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs b/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs index 3cb791b571..c9d5b54def 100644 --- a/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs @@ -3,7 +3,6 @@ using System.IO; using System.Net; using Jellyfin.Server.Helpers; using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Net; using MediaBrowser.Controller.Extensions; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; @@ -36,7 +35,7 @@ public static class WebHostBuilderExtensions return builder .UseKestrel((builderContext, options) => { - var addresses = appHost.NetManager.GetAllBindInterfaces(); + var addresses = appHost.NetManager.GetAllBindInterfaces(true); bool flagged = false; foreach (var netAdd in addresses) diff --git a/MediaBrowser.Model/Net/IPData.cs b/MediaBrowser.Model/Net/IPData.cs index 985b16c6e4..e9fcd67975 100644 --- a/MediaBrowser.Model/Net/IPData.cs +++ b/MediaBrowser.Model/Net/IPData.cs @@ -47,6 +47,11 @@ public class IPData /// </summary> public int Index { get; set; } + /// <summary> + /// Gets or sets a value indicating whether the network supports multicast. + /// </summary> + public bool SupportsMulticast { get; set; } = false; + /// <summary> /// Gets or sets the interface name. /// </summary> diff --git a/MediaBrowser.Model/Net/PublishedServerUriOverride.cs b/MediaBrowser.Model/Net/PublishedServerUriOverride.cs new file mode 100644 index 0000000000..476d1ba382 --- /dev/null +++ b/MediaBrowser.Model/Net/PublishedServerUriOverride.cs @@ -0,0 +1,42 @@ +namespace MediaBrowser.Model.Net; + +/// <summary> +/// Class holding information for a published server URI override. +/// </summary> +public class PublishedServerUriOverride +{ + /// <summary> + /// Initializes a new instance of the <see cref="PublishedServerUriOverride"/> class. + /// </summary> + /// <param name="data">The <see cref="IPData"/>.</param> + /// <param name="overrideUri">The override.</param> + /// <param name="internalOverride">A value indicating whether the override is for internal requests.</param> + /// <param name="externalOverride">A value indicating whether the override is for external requests.</param> + public PublishedServerUriOverride(IPData data, string overrideUri, bool internalOverride, bool externalOverride) + { + Data = data; + OverrideUri = overrideUri; + IsInternalOverride = internalOverride; + IsExternalOverride = externalOverride; + } + + /// <summary> + /// Gets or sets the object's IP address. + /// </summary> + public IPData Data { get; set; } + + /// <summary> + /// Gets or sets the override URI. + /// </summary> + public string OverrideUri { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether the override should be applied to internal requests. + /// </summary> + public bool IsInternalOverride { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether the override should be applied to external requests. + /// </summary> + public bool IsExternalOverride { get; set; } +} diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs index 0dce6c3bfa..a3f30c174f 100644 --- a/RSSDP/SsdpCommunicationsServer.cs +++ b/RSSDP/SsdpCommunicationsServer.cs @@ -32,10 +32,10 @@ namespace Rssdp.Infrastructure * port to use, we will default to 0 which allows the underlying system to auto-assign a free port. */ - private object _BroadcastListenSocketSynchroniser = new object(); + private object _BroadcastListenSocketSynchroniser = new(); private List<Socket> _MulticastListenSockets; - private object _SendSocketSynchroniser = new object(); + private object _SendSocketSynchroniser = new(); private List<Socket> _sendSockets; private HttpRequestParser _RequestParser; @@ -48,7 +48,6 @@ namespace Rssdp.Infrastructure private int _MulticastTtl; private bool _IsShared; - private readonly bool _enableMultiSocketBinding; /// <summary> /// Raised when a HTTPU request message is received by a socket (unicast or multicast). @@ -64,9 +63,11 @@ namespace Rssdp.Infrastructure /// Minimum constructor. /// </summary> /// <exception cref="ArgumentNullException">The <paramref name="socketFactory"/> argument is null.</exception> - public SsdpCommunicationsServer(ISocketFactory socketFactory, - INetworkManager networkManager, ILogger logger, bool enableMultiSocketBinding) - : this(socketFactory, 0, SsdpConstants.SsdpDefaultMulticastTimeToLive, networkManager, logger, enableMultiSocketBinding) + public SsdpCommunicationsServer( + ISocketFactory socketFactory, + INetworkManager networkManager, + ILogger logger) + : this(socketFactory, 0, SsdpConstants.SsdpDefaultMulticastTimeToLive, networkManager, logger) { } @@ -76,7 +77,12 @@ namespace Rssdp.Infrastructure /// </summary> /// <exception cref="ArgumentNullException">The <paramref name="socketFactory"/> argument is null.</exception> /// <exception cref="ArgumentOutOfRangeException">The <paramref name="multicastTimeToLive"/> argument is less than or equal to zero.</exception> - public SsdpCommunicationsServer(ISocketFactory socketFactory, int localPort, int multicastTimeToLive, INetworkManager networkManager, ILogger logger, bool enableMultiSocketBinding) + public SsdpCommunicationsServer( + ISocketFactory socketFactory, + int localPort, + int multicastTimeToLive, + INetworkManager networkManager, + ILogger logger) { if (socketFactory is null) { @@ -88,19 +94,18 @@ namespace Rssdp.Infrastructure throw new ArgumentOutOfRangeException(nameof(multicastTimeToLive), "multicastTimeToLive must be greater than zero."); } - _BroadcastListenSocketSynchroniser = new object(); - _SendSocketSynchroniser = new object(); + _BroadcastListenSocketSynchroniser = new(); + _SendSocketSynchroniser = new(); _LocalPort = localPort; _SocketFactory = socketFactory; - _RequestParser = new HttpRequestParser(); - _ResponseParser = new HttpResponseParser(); + _RequestParser = new(); + _ResponseParser = new(); _MulticastTtl = multicastTimeToLive; _networkManager = networkManager; _logger = logger; - _enableMultiSocketBinding = enableMultiSocketBinding; } /// <summary> @@ -335,7 +340,7 @@ namespace Rssdp.Infrastructure { sockets = sockets.ToList(); - var tasks = sockets.Where(s => (fromlocalIPAddress is null || fromlocalIPAddress.Equals(((IPEndPoint)s.LocalEndPoint).Address))) + var tasks = sockets.Where(s => fromlocalIPAddress is null || fromlocalIPAddress.Equals(((IPEndPoint)s.LocalEndPoint).Address)) .Select(s => SendFromSocket(s, messageData, destination, cancellationToken)); return Task.WhenAll(tasks); } @@ -347,33 +352,26 @@ namespace Rssdp.Infrastructure { var sockets = new List<Socket>(); var multicastGroupAddress = IPAddress.Parse(SsdpConstants.MulticastLocalAdminAddress); - if (_enableMultiSocketBinding) - { - // IPv6 is currently unsupported - var validInterfaces = _networkManager.GetInternalBindAddresses() - .Where(x => x.Address is not null) - .Where(x => x.AddressFamily == AddressFamily.InterNetwork) - .DistinctBy(x => x.Index); - foreach (var intf in validInterfaces) - { - try - { - var socket = _SocketFactory.CreateUdpMulticastSocket(multicastGroupAddress, intf, _MulticastTtl, SsdpConstants.MulticastPort); - _ = ListenToSocketInternal(socket); - sockets.Add(socket); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error in CreateMulticastSocketsAndListen. IP address: {0}", intf.Address); - } - } - } - else + // IPv6 is currently unsupported + var validInterfaces = _networkManager.GetInternalBindAddresses() + .Where(x => x.Address is not null) + .Where(x => x.SupportsMulticast) + .Where(x => x.AddressFamily == AddressFamily.InterNetwork) + .DistinctBy(x => x.Index); + + foreach (var intf in validInterfaces) { - var socket = _SocketFactory.CreateUdpMulticastSocket(multicastGroupAddress, new IPData(IPAddress.Any, null), _MulticastTtl, SsdpConstants.MulticastPort); - _ = ListenToSocketInternal(socket); - sockets.Add(socket); + try + { + var socket = _SocketFactory.CreateUdpMulticastSocket(multicastGroupAddress, intf, _MulticastTtl, SsdpConstants.MulticastPort); + _ = ListenToSocketInternal(socket); + sockets.Add(socket); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to create SSDP UDP multicast socket for {0} on interface {1} (index {2})", intf.Address, intf.Name, intf.Index); + } } return sockets; @@ -382,34 +380,32 @@ namespace Rssdp.Infrastructure private List<Socket> CreateSendSockets() { var sockets = new List<Socket>(); - if (_enableMultiSocketBinding) - { - // IPv6 is currently unsupported - var validInterfaces = _networkManager.GetInternalBindAddresses() - .Where(x => x.Address is not null) - .Where(x => x.AddressFamily == AddressFamily.InterNetwork); - foreach (var intf in validInterfaces) + // IPv6 is currently unsupported + var validInterfaces = _networkManager.GetInternalBindAddresses() + .Where(x => x.Address is not null) + .Where(x => x.SupportsMulticast) + .Where(x => x.AddressFamily == AddressFamily.InterNetwork); + + if (OperatingSystem.IsMacOS()) + { + // Manually remove loopback on macOS due to https://github.com/dotnet/runtime/issues/24340 + validInterfaces = validInterfaces.Where(x => !x.Address.Equals(IPAddress.Loopback)); + } + + foreach (var intf in validInterfaces) + { + try { - try - { - var socket = _SocketFactory.CreateSsdpUdpSocket(intf, _LocalPort); - _ = ListenToSocketInternal(socket); - sockets.Add(socket); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error in CreateSsdpUdpSocket. IPAddress: {0}", intf.Address); - } + var socket = _SocketFactory.CreateSsdpUdpSocket(intf, _LocalPort); + _ = ListenToSocketInternal(socket); + sockets.Add(socket); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to create SSDP UDP sender socket for {0} on interface {1} (index {2})", intf.Address, intf.Name, intf.Index); } } - else - { - var socket = _SocketFactory.CreateSsdpUdpSocket(new IPData(IPAddress.Any, null), _LocalPort); - _ = ListenToSocketInternal(socket); - sockets.Add(socket); - } - return sockets; } @@ -423,7 +419,7 @@ namespace Rssdp.Infrastructure { try { - var result = await socket.ReceiveMessageFromAsync(receiveBuffer, SocketFlags.None, new IPEndPoint(IPAddress.Any, 0), CancellationToken.None).ConfigureAwait(false);; + var result = await socket.ReceiveMessageFromAsync(receiveBuffer, new IPEndPoint(IPAddress.Any, _LocalPort), CancellationToken.None).ConfigureAwait(false);; if (result.ReceivedBytes > 0) { @@ -431,7 +427,7 @@ namespace Rssdp.Infrastructure var localEndpointAdapter = _networkManager.GetAllBindInterfaces().First(a => a.Index == result.PacketInformation.Interface); ProcessMessage( - UTF8Encoding.UTF8.GetString(receiveBuffer, 0, result.ReceivedBytes), + Encoding.UTF8.GetString(receiveBuffer, 0, result.ReceivedBytes), remoteEndpoint, localEndpointAdapter.Address); } @@ -511,7 +507,7 @@ namespace Rssdp.Infrastructure return; } - var handlers = this.RequestReceived; + var handlers = RequestReceived; if (handlers is not null) { handlers(this, new RequestReceivedEventArgs(data, remoteEndPoint, receivedOnlocalIPAddress)); @@ -520,7 +516,7 @@ namespace Rssdp.Infrastructure private void OnResponseReceived(HttpResponseMessage data, IPEndPoint endPoint, IPAddress localIPAddress) { - var handlers = this.ResponseReceived; + var handlers = ResponseReceived; if (handlers is not null) { handlers(this, new ResponseReceivedEventArgs(data, endPoint) diff --git a/RSSDP/SsdpDeviceLocator.cs b/RSSDP/SsdpDeviceLocator.cs index 59f4c5070b..82b09c4b45 100644 --- a/RSSDP/SsdpDeviceLocator.cs +++ b/RSSDP/SsdpDeviceLocator.cs @@ -17,7 +17,7 @@ namespace Rssdp.Infrastructure private ISsdpCommunicationsServer _CommunicationsServer; private Timer _BroadcastTimer; - private object _timerLock = new object(); + private object _timerLock = new(); private string _OSName; @@ -221,12 +221,12 @@ namespace Rssdp.Infrastructure /// <seealso cref="DeviceAvailable"/> protected virtual void OnDeviceAvailable(DiscoveredSsdpDevice device, bool isNewDevice, IPAddress IPAddress) { - if (this.IsDisposed) + if (IsDisposed) { return; } - var handlers = this.DeviceAvailable; + var handlers = DeviceAvailable; if (handlers is not null) { handlers(this, new DeviceAvailableEventArgs(device, isNewDevice) @@ -244,12 +244,12 @@ namespace Rssdp.Infrastructure /// <seealso cref="DeviceUnavailable"/> protected virtual void OnDeviceUnavailable(DiscoveredSsdpDevice device, bool expired) { - if (this.IsDisposed) + if (IsDisposed) { return; } - var handlers = this.DeviceUnavailable; + var handlers = DeviceUnavailable; if (handlers is not null) { handlers(this, new DeviceUnavailableEventArgs(device, expired)); @@ -291,8 +291,8 @@ namespace Rssdp.Infrastructure _CommunicationsServer = null; if (commsServer is not null) { - commsServer.ResponseReceived -= this.CommsServer_ResponseReceived; - commsServer.RequestReceived -= this.CommsServer_RequestReceived; + commsServer.ResponseReceived -= CommsServer_ResponseReceived; + commsServer.RequestReceived -= CommsServer_RequestReceived; } } } @@ -341,7 +341,7 @@ namespace Rssdp.Infrastructure var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - values["HOST"] = "239.255.255.250:1900"; + values["HOST"] = string.Format(CultureInfo.InvariantCulture, "{0}:{1}", SsdpConstants.MulticastLocalAdminAddress, SsdpConstants.MulticastPort); values["USER-AGENT"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion); values["MAN"] = "\"ssdp:discover\""; @@ -382,17 +382,17 @@ namespace Rssdp.Infrastructure private void ProcessNotificationMessage(HttpRequestMessage message, IPAddress IPAddress) { - if (String.Compare(message.Method.Method, "Notify", StringComparison.OrdinalIgnoreCase) != 0) + if (string.Compare(message.Method.Method, "Notify", StringComparison.OrdinalIgnoreCase) != 0) { return; } var notificationType = GetFirstHeaderStringValue("NTS", message); - if (String.Compare(notificationType, SsdpConstants.SsdpKeepAliveNotification, StringComparison.OrdinalIgnoreCase) == 0) + if (string.Compare(notificationType, SsdpConstants.SsdpKeepAliveNotification, StringComparison.OrdinalIgnoreCase) == 0) { ProcessAliveNotification(message, IPAddress); } - else if (String.Compare(notificationType, SsdpConstants.SsdpByeByeNotification, StringComparison.OrdinalIgnoreCase) == 0) + else if (string.Compare(notificationType, SsdpConstants.SsdpByeByeNotification, StringComparison.OrdinalIgnoreCase) == 0) { ProcessByeByeNotification(message); } @@ -420,7 +420,7 @@ namespace Rssdp.Infrastructure private void ProcessByeByeNotification(HttpRequestMessage message) { var notficationType = GetFirstHeaderStringValue("NT", message); - if (!String.IsNullOrEmpty(notficationType)) + if (!string.IsNullOrEmpty(notficationType)) { var usn = GetFirstHeaderStringValue("USN", message); @@ -447,10 +447,9 @@ namespace Rssdp.Infrastructure private string GetFirstHeaderStringValue(string headerName, HttpResponseMessage message) { string retVal = null; - IEnumerable<string> values; if (message.Headers.Contains(headerName)) { - message.Headers.TryGetValues(headerName, out values); + message.Headers.TryGetValues(headerName, out var values); if (values is not null) { retVal = values.FirstOrDefault(); @@ -463,10 +462,9 @@ namespace Rssdp.Infrastructure private string GetFirstHeaderStringValue(string headerName, HttpRequestMessage message) { string retVal = null; - IEnumerable<string> values; if (message.Headers.Contains(headerName)) { - message.Headers.TryGetValues(headerName, out values); + message.Headers.TryGetValues(headerName, out var values); if (values is not null) { retVal = values.FirstOrDefault(); @@ -479,10 +477,9 @@ namespace Rssdp.Infrastructure private Uri GetFirstHeaderUriValue(string headerName, HttpRequestMessage request) { string value = null; - IEnumerable<string> values; if (request.Headers.Contains(headerName)) { - request.Headers.TryGetValues(headerName, out values); + request.Headers.TryGetValues(headerName, out var values); if (values is not null) { value = values.FirstOrDefault(); @@ -496,10 +493,9 @@ namespace Rssdp.Infrastructure private Uri GetFirstHeaderUriValue(string headerName, HttpResponseMessage response) { string value = null; - IEnumerable<string> values; if (response.Headers.Contains(headerName)) { - response.Headers.TryGetValues(headerName, out values); + response.Headers.TryGetValues(headerName, out var values); if (values is not null) { value = values.FirstOrDefault(); @@ -529,7 +525,7 @@ namespace Rssdp.Infrastructure foreach (var device in expiredDevices) { - if (this.IsDisposed) + if (IsDisposed) { return; } @@ -543,7 +539,7 @@ namespace Rssdp.Infrastructure // problems. foreach (var expiredUsn in (from expiredDevice in expiredDevices select expiredDevice.Usn).Distinct()) { - if (this.IsDisposed) + if (IsDisposed) { return; } @@ -560,7 +556,7 @@ namespace Rssdp.Infrastructure existingDevices = FindExistingDeviceNotifications(_Devices, deviceUsn); foreach (var existingDevice in existingDevices) { - if (this.IsDisposed) + if (IsDisposed) { return true; } diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs index 950e6fec86..65ae658a45 100644 --- a/RSSDP/SsdpDevicePublisher.cs +++ b/RSSDP/SsdpDevicePublisher.cs @@ -206,9 +206,9 @@ namespace Rssdp.Infrastructure IPAddress receivedOnlocalIPAddress, CancellationToken cancellationToken) { - if (String.IsNullOrEmpty(searchTarget)) + if (string.IsNullOrEmpty(searchTarget)) { - WriteTrace(String.Format(CultureInfo.InvariantCulture, "Invalid search request received From {0}, Target is null/empty.", remoteEndPoint.ToString())); + WriteTrace(string.Format(CultureInfo.InvariantCulture, "Invalid search request received From {0}, Target is null/empty.", remoteEndPoint.ToString())); return; } @@ -232,7 +232,7 @@ namespace Rssdp.Infrastructure // return; } - if (!Int32.TryParse(mx, out var maxWaitInterval) || maxWaitInterval <= 0) + if (!int.TryParse(mx, out var maxWaitInterval) || maxWaitInterval <= 0) { return; } @@ -243,27 +243,27 @@ namespace Rssdp.Infrastructure } // Do not block synchronously as that may tie up a threadpool thread for several seconds. - Task.Delay(_Random.Next(16, (maxWaitInterval * 1000))).ContinueWith((parentTask) => + Task.Delay(_Random.Next(16, maxWaitInterval * 1000)).ContinueWith((parentTask) => { // Copying devices to local array here to avoid threading issues/enumerator exceptions. IEnumerable<SsdpDevice> devices = null; lock (_Devices) { - if (String.Compare(SsdpConstants.SsdpDiscoverAllSTHeader, searchTarget, StringComparison.OrdinalIgnoreCase) == 0) + if (string.Compare(SsdpConstants.SsdpDiscoverAllSTHeader, searchTarget, StringComparison.OrdinalIgnoreCase) == 0) { devices = GetAllDevicesAsFlatEnumerable().ToArray(); } - else if (String.Compare(SsdpConstants.UpnpDeviceTypeRootDevice, searchTarget, StringComparison.OrdinalIgnoreCase) == 0 || (this.SupportPnpRootDevice && String.Compare(SsdpConstants.PnpDeviceTypeRootDevice, searchTarget, StringComparison.OrdinalIgnoreCase) == 0)) + else if (string.Compare(SsdpConstants.UpnpDeviceTypeRootDevice, searchTarget, StringComparison.OrdinalIgnoreCase) == 0 || (SupportPnpRootDevice && String.Compare(SsdpConstants.PnpDeviceTypeRootDevice, searchTarget, StringComparison.OrdinalIgnoreCase) == 0)) { devices = _Devices.ToArray(); } else if (searchTarget.Trim().StartsWith("uuid:", StringComparison.OrdinalIgnoreCase)) { - devices = GetAllDevicesAsFlatEnumerable().Where(d => String.Compare(d.Uuid, searchTarget.Substring(5), StringComparison.OrdinalIgnoreCase) == 0).ToArray(); + devices = GetAllDevicesAsFlatEnumerable().Where(d => string.Compare(d.Uuid, searchTarget.Substring(5), StringComparison.OrdinalIgnoreCase) == 0).ToArray(); } else if (searchTarget.StartsWith("urn:", StringComparison.OrdinalIgnoreCase)) { - devices = GetAllDevicesAsFlatEnumerable().Where(d => String.Compare(d.FullDeviceType, searchTarget, StringComparison.OrdinalIgnoreCase) == 0).ToArray(); + devices = GetAllDevicesAsFlatEnumerable().Where(d => string.Compare(d.FullDeviceType, searchTarget, StringComparison.OrdinalIgnoreCase) == 0).ToArray(); } } @@ -299,7 +299,7 @@ namespace Rssdp.Infrastructure if (isRootDevice) { SendSearchResponse(SsdpConstants.UpnpDeviceTypeRootDevice, device, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), endPoint, receivedOnlocalIPAddress, cancellationToken); - if (this.SupportPnpRootDevice) + if (SupportPnpRootDevice) { SendSearchResponse(SsdpConstants.PnpDeviceTypeRootDevice, device, GetUsn(device.Udn, SsdpConstants.PnpDeviceTypeRootDevice), endPoint, receivedOnlocalIPAddress, cancellationToken); } @@ -312,7 +312,7 @@ namespace Rssdp.Infrastructure private string GetUsn(string udn, string fullDeviceType) { - return String.Format(CultureInfo.InvariantCulture, "{0}::{1}", udn, fullDeviceType); + return string.Format(CultureInfo.InvariantCulture, "{0}::{1}", udn, fullDeviceType); } private async void SendSearchResponse( @@ -326,16 +326,17 @@ namespace Rssdp.Infrastructure const string header = "HTTP/1.1 200 OK"; var rootDevice = device.ToRootDevice(); - var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - - values["EXT"] = ""; - values["DATE"] = DateTime.UtcNow.ToString("r"); - values["HOST"] = "239.255.255.250:1900"; - values["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds; - values["ST"] = searchTarget; - values["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion); - values["USN"] = uniqueServiceName; - values["LOCATION"] = rootDevice.Location.ToString(); + var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) + { + ["EXT"] = "", + ["DATE"] = DateTime.UtcNow.ToString("r"), + ["HOST"] = string.Format(CultureInfo.InvariantCulture, "{0}:{1}", SsdpConstants.MulticastLocalAdminAddress, SsdpConstants.MulticastPort), + ["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds, + ["ST"] = searchTarget, + ["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion), + ["USN"] = uniqueServiceName, + ["LOCATION"] = rootDevice.Location.ToString() + }; var message = BuildMessage(header, values); @@ -439,7 +440,7 @@ namespace Rssdp.Infrastructure if (isRoot) { SendAliveNotification(device, SsdpConstants.UpnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), cancellationToken); - if (this.SupportPnpRootDevice) + if (SupportPnpRootDevice) { SendAliveNotification(device, SsdpConstants.PnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.PnpDeviceTypeRootDevice), cancellationToken); } @@ -460,17 +461,18 @@ namespace Rssdp.Infrastructure const string header = "NOTIFY * HTTP/1.1"; - var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - - // If needed later for non-server devices, these headers will need to be dynamic - values["HOST"] = "239.255.255.250:1900"; - values["DATE"] = DateTime.UtcNow.ToString("r"); - values["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds; - values["LOCATION"] = rootDevice.Location.ToString(); - values["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion); - values["NTS"] = "ssdp:alive"; - values["NT"] = notificationType; - values["USN"] = uniqueServiceName; + var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) + { + // If needed later for non-server devices, these headers will need to be dynamic + ["HOST"] = string.Format(CultureInfo.InvariantCulture, "{0}:{1}", SsdpConstants.MulticastLocalAdminAddress, SsdpConstants.MulticastPort), + ["DATE"] = DateTime.UtcNow.ToString("r"), + ["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds, + ["LOCATION"] = rootDevice.Location.ToString(), + ["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion), + ["NTS"] = "ssdp:alive", + ["NT"] = notificationType, + ["USN"] = uniqueServiceName + }; var message = BuildMessage(header, values); @@ -485,7 +487,7 @@ namespace Rssdp.Infrastructure if (isRoot) { tasks.Add(SendByeByeNotification(device, SsdpConstants.UpnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), cancellationToken)); - if (this.SupportPnpRootDevice) + if (SupportPnpRootDevice) { tasks.Add(SendByeByeNotification(device, "pnp:rootdevice", GetUsn(device.Udn, "pnp:rootdevice"), cancellationToken)); } @@ -506,20 +508,21 @@ namespace Rssdp.Infrastructure { const string header = "NOTIFY * HTTP/1.1"; - var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - - // If needed later for non-server devices, these headers will need to be dynamic - values["HOST"] = "239.255.255.250:1900"; - values["DATE"] = DateTime.UtcNow.ToString("r"); - values["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion); - values["NTS"] = "ssdp:byebye"; - values["NT"] = notificationType; - values["USN"] = uniqueServiceName; + var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) + { + // If needed later for non-server devices, these headers will need to be dynamic + ["HOST"] = string.Format(CultureInfo.InvariantCulture, "{0}:{1}", SsdpConstants.MulticastLocalAdminAddress, SsdpConstants.MulticastPort), + ["DATE"] = DateTime.UtcNow.ToString("r"), + ["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion), + ["NTS"] = "ssdp:byebye", + ["NT"] = notificationType, + ["USN"] = uniqueServiceName + }; var message = BuildMessage(header, values); var sendCount = IsDisposed ? 1 : 3; - WriteTrace(String.Format(CultureInfo.InvariantCulture, "Sent byebye notification"), device); + WriteTrace(string.Format(CultureInfo.InvariantCulture, "Sent byebye notification"), device); return _CommsServer.SendMulticastMessage(message, sendCount, _sendOnlyMatchedHost ? device.ToRootDevice().Address : null, cancellationToken); } diff --git a/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs b/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs index 2ff7c7de4f..072e0a8c53 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs @@ -16,7 +16,6 @@ namespace Jellyfin.Networking.Tests [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")] diff --git a/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs b/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs index 0b07a3c53f..2302f90b8d 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs @@ -1,7 +1,9 @@ using System.Net; using Jellyfin.Networking.Configuration; using Jellyfin.Networking.Manager; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; +using Moq; using Xunit; namespace Jellyfin.Networking.Tests @@ -28,7 +30,8 @@ namespace Jellyfin.Networking.Tests LocalNetworkSubnets = network.Split(',') }; - using var networkManager = new NetworkManager(NetworkParseTests.GetMockConfig(conf), new NullLogger<NetworkManager>()); + var startupConf = new Mock<IConfiguration>(); + using var networkManager = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>()); Assert.True(networkManager.IsInLocalNetwork(ip)); } @@ -56,9 +59,10 @@ namespace Jellyfin.Networking.Tests LocalNetworkSubnets = network.Split(',') }; - using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), new NullLogger<NetworkManager>()); + var startupConf = new Mock<IConfiguration>(); + using var networkManager = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>()); - Assert.False(nm.IsInLocalNetwork(ip)); + Assert.False(networkManager.IsInLocalNetwork(ip)); } } } diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs index 731cbbafbc..022b8a3d04 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs @@ -7,6 +7,7 @@ using Jellyfin.Networking.Extensions; using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Model.Net; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; @@ -54,7 +55,8 @@ namespace Jellyfin.Networking.Tests }; NetworkManager.MockNetworkSettings = interfaces; - using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); + var startupConf = new Mock<IConfiguration>(); + using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>()); NetworkManager.MockNetworkSettings = string.Empty; Assert.Equal(value, "[" + string.Join(",", nm.GetInternalBindAddresses().Select(x => x.Address + "/" + x.Subnet.PrefixLength)) + "]"); @@ -200,7 +202,8 @@ namespace Jellyfin.Networking.Tests }; 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>()); + var startupConf = new Mock<IConfiguration>(); + using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>()); NetworkManager.MockNetworkSettings = string.Empty; // Check to see if DNS resolution is working. If not, skip test. @@ -229,24 +232,24 @@ namespace Jellyfin.Networking.Tests [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")] + [InlineData("8.8.8.8", "192.168.1.0/24", "eth16,eth11", false, "all=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")] + [InlineData("10.10.10.10", "192.168.1.0/24", "eth16", false, "external=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")] + [InlineData("192.168.1.1", "192.168.1.0/24", "", false, "external=http://helloworld.com", "eth16")] // User on external network, internal binding only - so assumption 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")] + [InlineData("jellyfin.org", "192.168.1.0/24", "eth16", false, "external=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")] + [InlineData("jellyfin.org", "192.168.1.0/24", "", false, "external=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 assumed to be internal, no binding - so result is the 1st matching interface. + [InlineData("", "192.168.1.0/24", "", false, "all=http://helloworld.com", "eth16")] - // User is internal, no binding - so result is the 1st internal, which is then overridden. + // User is internal, no binding - so result is the 1st internal interface, 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) { @@ -264,7 +267,8 @@ namespace Jellyfin.Networking.Tests }; 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>()); + var startupConf = new Mock<IConfiguration>(); + using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>()); NetworkManager.MockNetworkSettings = string.Empty; if (nm.TryParseInterface(result, out IReadOnlyList<IPData>? resultObj) && resultObj is not null) @@ -293,7 +297,9 @@ namespace Jellyfin.Networking.Tests RemoteIPFilter = addresses.Split(','), IsRemoteIPFilterBlacklist = false }; - using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); + + var startupConf = new Mock<IConfiguration>(); + using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>()); Assert.NotEqual(nm.HasRemoteAccess(IPAddress.Parse(remoteIP)), denied); } @@ -314,7 +320,8 @@ namespace Jellyfin.Networking.Tests IsRemoteIPFilterBlacklist = true }; - using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); + var startupConf = new Mock<IConfiguration>(); + using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>()); Assert.NotEqual(nm.HasRemoteAccess(IPAddress.Parse(remoteIP)), denied); } @@ -334,7 +341,8 @@ namespace Jellyfin.Networking.Tests }; NetworkManager.MockNetworkSettings = interfaces; - using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); + var startupConf = new Mock<IConfiguration>(); + using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>()); var interfaceToUse = nm.GetBindAddress(string.Empty, out _); @@ -358,7 +366,8 @@ namespace Jellyfin.Networking.Tests }; NetworkManager.MockNetworkSettings = interfaces; - using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); + var startupConf = new Mock<IConfiguration>(); + using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>()); var interfaceToUse = nm.GetBindAddress(source, out _); diff --git a/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs index 49516ccccd..2881020375 100644 --- a/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs +++ b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs @@ -7,6 +7,7 @@ using Jellyfin.Server.Extensions; using MediaBrowser.Common.Configuration; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; @@ -119,8 +120,8 @@ namespace Jellyfin.Server.Tests EnableIPv6 = true, EnableIPv4 = true, }; - - return new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); + var startupConf = new Mock<IConfiguration>(); + return new NetworkManager(GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>()); } } } From 26571a8c51b9670f198e58175463e1d3db5441ee Mon Sep 17 00:00:00 2001 From: Bond-009 <bond.009@outlook.com> Date: Wed, 11 Oct 2023 01:51:15 +0200 Subject: [PATCH 701/858] Deprecate CanLaunchWebBrowser (#10381) It's been a while since I removed this feature from server not sure why it's in the api anyway. The macOS and Windows app have this functionality --- Emby.Server.Implementations/SystemManager.cs | 5 ----- MediaBrowser.Model/System/SystemInfo.cs | 3 ++- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/Emby.Server.Implementations/SystemManager.cs b/Emby.Server.Implementations/SystemManager.cs index 5e9c424e9a..af66b62e3e 100644 --- a/Emby.Server.Implementations/SystemManager.cs +++ b/Emby.Server.Implementations/SystemManager.cs @@ -46,10 +46,6 @@ public class SystemManager : ISystemManager _installationManager = installationManager; } - private bool CanLaunchWebBrowser => Environment.UserInteractive - && !_startupOptions.IsService - && (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS()); - /// <inheritdoc /> public SystemInfo GetSystemInfo(HttpRequest request) { @@ -67,7 +63,6 @@ public class SystemManager : ISystemManager ItemsByNamePath = _applicationPaths.InternalMetadataPath, InternalMetadataPath = _applicationPaths.InternalMetadataPath, CachePath = _applicationPaths.CachePath, - CanLaunchWebBrowser = CanLaunchWebBrowser, TranscodingTempPath = _configurationManager.GetTranscodePath(), ServerName = _applicationHost.FriendlyName, LocalAddress = _applicationHost.GetSmartApiUrl(request), diff --git a/MediaBrowser.Model/System/SystemInfo.cs b/MediaBrowser.Model/System/SystemInfo.cs index bd0099af70..502fc38abc 100644 --- a/MediaBrowser.Model/System/SystemInfo.cs +++ b/MediaBrowser.Model/System/SystemInfo.cs @@ -84,7 +84,8 @@ namespace MediaBrowser.Model.System [Obsolete("This is always true")] public bool CanSelfRestart { get; set; } = true; - public bool CanLaunchWebBrowser { get; set; } + [Obsolete("This is always false")] + public bool CanLaunchWebBrowser { get; set; } = false; /// <summary> /// Gets or sets the program data path. From 35d63ec540ced98e9f1eebf9a786714abc9ec82e Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Wed, 11 Oct 2023 13:43:43 +0200 Subject: [PATCH 702/858] Fix regression --- Jellyfin.Api/Helpers/StreamingHelpers.cs | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index a653c57952..11f6bcf6bf 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -421,13 +421,12 @@ public static class StreamingHelpers /// <param name="state">The state.</param> /// <param name="mediaSource">The mediaSource.</param> /// <returns>System.String.</returns> - private static string? GetOutputFileExtension(StreamState state, MediaSourceInfo? mediaSource) + private static string GetOutputFileExtension(StreamState state, MediaSourceInfo? mediaSource) { - var ext = Path.GetExtension(state.RequestedUrl.AsSpan()); - - if (ext.IsEmpty) + var ext = Path.GetExtension(state.RequestedUrl); + if (!string.IsNullOrEmpty(ext)) { - return null; + return ext; } // Try to infer based on the desired video codec @@ -463,10 +462,9 @@ public static class StreamingHelpers return ".asf"; } } - - // Try to infer based on the desired audio codec - if (!state.IsVideoRequest) + else { + // Try to infer based on the desired audio codec var audioCodec = state.Request.AudioCodec; if (string.Equals("aac", audioCodec, StringComparison.OrdinalIgnoreCase)) @@ -497,7 +495,7 @@ public static class StreamingHelpers return '.' + (idx == -1 ? mediaSource.Container : mediaSource.Container[..idx]).Trim(); } - return null; + throw new InvalidOperationException("Failed to find an appropriate file extension"); } /// <summary> @@ -509,12 +507,12 @@ public static class StreamingHelpers /// <param name="deviceId">The device id.</param> /// <param name="playSessionId">The play session id.</param> /// <returns>The complete file path, including the folder, for the transcoding file.</returns> - private static string GetOutputFilePath(StreamState state, string? outputFileExtension, IServerConfigurationManager serverConfigurationManager, string? deviceId, string? playSessionId) + private static string GetOutputFilePath(StreamState state, string outputFileExtension, IServerConfigurationManager serverConfigurationManager, string? deviceId, string? playSessionId) { var data = $"{state.MediaPath}-{state.UserAgent}-{deviceId!}-{playSessionId!}"; var filename = data.GetMD5().ToString("N", CultureInfo.InvariantCulture); - var ext = outputFileExtension?.ToLowerInvariant(); + var ext = outputFileExtension.ToLowerInvariant(); var folder = serverConfigurationManager.GetTranscodePath(); return Path.Combine(folder, filename + ext); From 2b1454530b2a29db2769c3de4c025d457200303f Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Wed, 11 Oct 2023 10:33:00 -0400 Subject: [PATCH 703/858] Add DLNA service collection extensions --- .../DlnaServiceCollectionExtensions.cs | 52 +++++++++++++++++++ .../ApplicationHost.cs | 8 --- Jellyfin.Server/Startup.cs | 20 +------ 3 files changed, 54 insertions(+), 26 deletions(-) create mode 100644 Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs diff --git a/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs b/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs new file mode 100644 index 0000000000..3a47acf10a --- /dev/null +++ b/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs @@ -0,0 +1,52 @@ +using System; +using System.Globalization; +using System.Net; +using System.Net.Http; +using System.Text; +using Emby.Dlna.Ssdp; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Dlna; +using MediaBrowser.Model.Dlna; +using Microsoft.Extensions.DependencyInjection; + +namespace Emby.Dlna.Extensions; + +/// <summary> +/// Extension methods for adding DLNA services. +/// </summary> +public static class DlnaServiceCollectionExtensions +{ + /// <summary> + /// Adds DLNA services to the provided <see cref="IServiceCollection"/>. + /// </summary> + /// <param name="services">The <see cref="IServiceCollection"/>.</param> + /// <param name="applicationHost">the.</param> + public static void AddDlnaServices( + this IServiceCollection services, + IServerApplicationHost applicationHost) + { + services.AddHttpClient(NamedClient.Dlna, c => + { + c.DefaultRequestHeaders.UserAgent.ParseAdd( + string.Format( + CultureInfo.InvariantCulture, + "{0}/{1} UPnP/1.0 {2}/{3}", + Environment.OSVersion.Platform, + Environment.OSVersion, + applicationHost.Name, + applicationHost.ApplicationVersionString)); + + c.DefaultRequestHeaders.Add("CPFN.UPNP.ORG", applicationHost.FriendlyName); // Required for UPnP DeviceArchitecture v2.0 + c.DefaultRequestHeaders.Add("FriendlyName.DLNA.ORG", applicationHost.FriendlyName); // REVIEW: where does this come from? + }) + .ConfigurePrimaryHttpMessageHandler(_ => new SocketsHttpHandler + { + AutomaticDecompression = DecompressionMethods.All, + RequestHeaderEncodingSelector = (_, _) => Encoding.UTF8 + }); + + services.AddSingleton<IDlnaManager, DlnaManager>(); + services.AddSingleton<IDeviceDiscovery, DeviceDiscovery>(); + } +} diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index c247178ee9..c9bf7f085e 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -13,9 +13,7 @@ using System.Net; using System.Reflection; using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; -using Emby.Dlna; using Emby.Dlna.Main; -using Emby.Dlna.Ssdp; using Emby.Naming.Common; using Emby.Photos; using Emby.Server.Implementations.Channels; @@ -58,7 +56,6 @@ using MediaBrowser.Controller.Chapters; using MediaBrowser.Controller.ClientEvent; using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; @@ -82,7 +79,6 @@ using MediaBrowser.LocalMetadata.Savers; using MediaBrowser.MediaEncoding.BdInfo; using MediaBrowser.MediaEncoding.Subtitles; using MediaBrowser.Model.Cryptography; -using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; @@ -563,8 +559,6 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton<ISessionManager, SessionManager>(); - serviceCollection.AddSingleton<IDlnaManager, DlnaManager>(); - serviceCollection.AddSingleton<ICollectionManager, CollectionManager>(); serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>(); @@ -576,8 +570,6 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton<IUserViewManager, UserViewManager>(); - serviceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>(); - serviceCollection.AddSingleton<IChapterManager, ChapterManager>(); serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>(); diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index b759b6bca5..2acddb243d 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -1,10 +1,10 @@ using System; -using System.Globalization; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Net.Mime; using System.Text; +using Emby.Dlna.Extensions; using Jellyfin.Api.Middleware; using Jellyfin.MediaEncoding.Hls.Extensions; using Jellyfin.Networking.Configuration; @@ -27,7 +27,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting; -using Microsoft.VisualBasic; using Prometheus; namespace Jellyfin.Server @@ -120,26 +119,11 @@ namespace Jellyfin.Server }) .ConfigurePrimaryHttpMessageHandler(defaultHttpClientHandlerDelegate); - services.AddHttpClient(NamedClient.Dlna, c => - { - c.DefaultRequestHeaders.UserAgent.ParseAdd( - string.Format( - CultureInfo.InvariantCulture, - "{0}/{1} UPnP/1.0 {2}/{3}", - Environment.OSVersion.Platform, - Environment.OSVersion, - _serverApplicationHost.Name, - _serverApplicationHost.ApplicationVersionString)); - - c.DefaultRequestHeaders.Add("CPFN.UPNP.ORG", _serverApplicationHost.FriendlyName); // Required for UPnP DeviceArchitecture v2.0 - c.DefaultRequestHeaders.Add("FriendlyName.DLNA.ORG", _serverApplicationHost.FriendlyName); // REVIEW: where does this come from? - }) - .ConfigurePrimaryHttpMessageHandler(defaultHttpClientHandlerDelegate); - services.AddHealthChecks() .AddCheck<DbContextFactoryHealthCheck<JellyfinDbContext>>(nameof(JellyfinDbContext)); services.AddHlsPlaylistGenerator(); + services.AddDlnaServices(_serverApplicationHost); } /// <summary> From 584dc05c3c58891f95a0498615f114047144f5c8 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Wed, 11 Oct 2023 16:38:45 +0200 Subject: [PATCH 704/858] Enable CodeAnalysisTreatWarningsAsErrors for MediaBrowser.Common --- MediaBrowser.Common/MediaBrowser.Common.csproj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index 3f1a098e45..7015d991fd 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -38,10 +38,6 @@ <SymbolPackageFormat>snupkg</SymbolPackageFormat> </PropertyGroup> - <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> - <CodeAnalysisTreatWarningsAsErrors>false</CodeAnalysisTreatWarningsAsErrors> - </PropertyGroup> - <PropertyGroup Condition=" '$(Stability)'=='Unstable'"> <!-- Include all symbols in the main nupkg until Azure Artifact Feed starts supporting ingesting NuGet symbol packages. --> <AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder> From effc3d488c2e0df04189b18b8e47905b1f641f24 Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Wed, 11 Oct 2023 11:05:14 -0400 Subject: [PATCH 705/858] Use DI for ContentDirectoryService --- .../DlnaServiceCollectionExtensions.cs | 2 ++ Emby.Dlna/Main/DlnaEntryPoint.cs | 23 +------------------ .../Controllers/DlnaServerController.cs | 5 ++-- 3 files changed, 6 insertions(+), 24 deletions(-) diff --git a/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs b/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs index 3a47acf10a..c28a5618ce 100644 --- a/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs +++ b/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs @@ -3,6 +3,7 @@ using System.Globalization; using System.Net; using System.Net.Http; using System.Text; +using Emby.Dlna.ContentDirectory; using Emby.Dlna.Ssdp; using MediaBrowser.Common.Net; using MediaBrowser.Controller; @@ -48,5 +49,6 @@ public static class DlnaServiceCollectionExtensions services.AddSingleton<IDlnaManager, DlnaManager>(); services.AddSingleton<IDeviceDiscovery, DeviceDiscovery>(); + services.AddSingleton<IContentDirectory, ContentDirectoryService>(); } } diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index 83b0ef3164..b2d2f36986 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -23,7 +23,6 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Session; -using MediaBrowser.Controller.TV; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Net; @@ -76,9 +75,7 @@ namespace Emby.Dlna.Main IDeviceDiscovery deviceDiscovery, IMediaEncoder mediaEncoder, ISocketFactory socketFactory, - INetworkManager networkManager, - IUserViewManager userViewManager, - ITVSeriesManager tvSeriesManager) + INetworkManager networkManager) { _config = config; _appHost = appHost; @@ -97,21 +94,6 @@ namespace Emby.Dlna.Main _networkManager = networkManager; _logger = loggerFactory.CreateLogger<DlnaEntryPoint>(); - ContentDirectory = new ContentDirectory.ContentDirectoryService( - dlnaManager, - userDataManager, - imageProcessor, - libraryManager, - config, - userManager, - loggerFactory.CreateLogger<ContentDirectory.ContentDirectoryService>(), - httpClientFactory, - localizationManager, - mediaSourceManager, - userViewManager, - mediaEncoder, - tvSeriesManager); - ConnectionManager = new ConnectionManager.ConnectionManagerService( dlnaManager, config, @@ -140,8 +122,6 @@ namespace Emby.Dlna.Main /// </summary> public static bool Enabled { get; private set; } - public IContentDirectory ContentDirectory { get; private set; } - public IConnectionManager ConnectionManager { get; private set; } public IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; } @@ -453,7 +433,6 @@ namespace Emby.Dlna.Main _communicationsServer = null; } - ContentDirectory = null; ConnectionManager = null; MediaReceiverRegistrar = null; Current = null; diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs index 95b296fae9..178ba6d6e1 100644 --- a/Jellyfin.Api/Controllers/DlnaServerController.cs +++ b/Jellyfin.Api/Controllers/DlnaServerController.cs @@ -33,10 +33,11 @@ public class DlnaServerController : BaseJellyfinApiController /// Initializes a new instance of the <see cref="DlnaServerController"/> class. /// </summary> /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param> - public DlnaServerController(IDlnaManager dlnaManager) + /// <param name="contentDirectory">Instance of the <see cref="IContentDirectory"/> interface.</param> + public DlnaServerController(IDlnaManager dlnaManager, IContentDirectory contentDirectory) { _dlnaManager = dlnaManager; - _contentDirectory = DlnaEntryPoint.Current.ContentDirectory; + _contentDirectory = contentDirectory; _connectionManager = DlnaEntryPoint.Current.ConnectionManager; _mediaReceiverRegistrar = DlnaEntryPoint.Current.MediaReceiverRegistrar; } From e0b089a37573a02898e179e738d44039df9432bf Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Wed, 11 Oct 2023 11:08:19 -0400 Subject: [PATCH 706/858] Use DI for ConnectionManagerService --- Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs | 2 ++ Emby.Dlna/Main/DlnaEntryPoint.cs | 9 --------- Jellyfin.Api/Controllers/DlnaServerController.cs | 8 ++++++-- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs b/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs index c28a5618ce..f8ed00aeaf 100644 --- a/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs +++ b/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs @@ -3,6 +3,7 @@ using System.Globalization; using System.Net; using System.Net.Http; using System.Text; +using Emby.Dlna.ConnectionManager; using Emby.Dlna.ContentDirectory; using Emby.Dlna.Ssdp; using MediaBrowser.Common.Net; @@ -50,5 +51,6 @@ public static class DlnaServiceCollectionExtensions services.AddSingleton<IDlnaManager, DlnaManager>(); services.AddSingleton<IDeviceDiscovery, DeviceDiscovery>(); services.AddSingleton<IContentDirectory, ContentDirectoryService>(); + services.AddSingleton<IConnectionManager, ConnectionManagerService>(); } } diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index b2d2f36986..a22143ae7b 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -94,12 +94,6 @@ namespace Emby.Dlna.Main _networkManager = networkManager; _logger = loggerFactory.CreateLogger<DlnaEntryPoint>(); - ConnectionManager = new ConnectionManager.ConnectionManagerService( - dlnaManager, - config, - loggerFactory.CreateLogger<ConnectionManager.ConnectionManagerService>(), - httpClientFactory); - MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrarService( loggerFactory.CreateLogger<MediaReceiverRegistrar.MediaReceiverRegistrarService>(), httpClientFactory, @@ -122,8 +116,6 @@ namespace Emby.Dlna.Main /// </summary> public static bool Enabled { get; private set; } - public IConnectionManager ConnectionManager { get; private set; } - public IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; } public async Task RunAsync() @@ -433,7 +425,6 @@ namespace Emby.Dlna.Main _communicationsServer = null; } - ConnectionManager = null; MediaReceiverRegistrar = null; Current = null; diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs index 178ba6d6e1..2d52097c04 100644 --- a/Jellyfin.Api/Controllers/DlnaServerController.cs +++ b/Jellyfin.Api/Controllers/DlnaServerController.cs @@ -34,11 +34,15 @@ public class DlnaServerController : BaseJellyfinApiController /// </summary> /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param> /// <param name="contentDirectory">Instance of the <see cref="IContentDirectory"/> interface.</param> - public DlnaServerController(IDlnaManager dlnaManager, IContentDirectory contentDirectory) + /// <param name="connectionManager">Instance of the <see cref="IConnectionManager"/> interface.</param> + public DlnaServerController( + IDlnaManager dlnaManager, + IContentDirectory contentDirectory, + IConnectionManager connectionManager) { _dlnaManager = dlnaManager; _contentDirectory = contentDirectory; - _connectionManager = DlnaEntryPoint.Current.ConnectionManager; + _connectionManager = connectionManager; _mediaReceiverRegistrar = DlnaEntryPoint.Current.MediaReceiverRegistrar; } From 010cf2340aca8f21d90fd9c8c8653b9b8d7208b2 Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Wed, 11 Oct 2023 11:12:33 -0400 Subject: [PATCH 707/858] Use DI for MediaReceiverRegistrarService --- .../Extensions/DlnaServiceCollectionExtensions.cs | 2 ++ Emby.Dlna/Main/DlnaEntryPoint.cs | 13 ------------- Jellyfin.Api/Controllers/DlnaServerController.cs | 7 ++++--- 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs b/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs index f8ed00aeaf..8361cc7e78 100644 --- a/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs +++ b/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs @@ -5,6 +5,7 @@ using System.Net.Http; using System.Text; using Emby.Dlna.ConnectionManager; using Emby.Dlna.ContentDirectory; +using Emby.Dlna.MediaReceiverRegistrar; using Emby.Dlna.Ssdp; using MediaBrowser.Common.Net; using MediaBrowser.Controller; @@ -52,5 +53,6 @@ public static class DlnaServiceCollectionExtensions services.AddSingleton<IDeviceDiscovery, DeviceDiscovery>(); services.AddSingleton<IContentDirectory, ContentDirectoryService>(); services.AddSingleton<IConnectionManager, ConnectionManagerService>(); + services.AddSingleton<IMediaReceiverRegistrar, MediaReceiverRegistrarService>(); } } diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index a22143ae7b..ea996a4424 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -94,12 +94,6 @@ namespace Emby.Dlna.Main _networkManager = networkManager; _logger = loggerFactory.CreateLogger<DlnaEntryPoint>(); - MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrarService( - loggerFactory.CreateLogger<MediaReceiverRegistrar.MediaReceiverRegistrarService>(), - httpClientFactory, - config); - Current = this; - var netConfig = config.GetConfiguration<NetworkConfiguration>(NetworkConfigurationStore.StoreKey); _disabled = appHost.ListenWithHttps && netConfig.RequireHttps; @@ -109,15 +103,11 @@ namespace Emby.Dlna.Main } } - public static DlnaEntryPoint Current { get; private set; } - /// <summary> /// Gets a value indicating whether the dlna server is enabled. /// </summary> public static bool Enabled { get; private set; } - public IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; } - public async Task RunAsync() { await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false); @@ -425,9 +415,6 @@ namespace Emby.Dlna.Main _communicationsServer = null; } - MediaReceiverRegistrar = null; - Current = null; - _disposed = true; } } diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs index 2d52097c04..42576934b3 100644 --- a/Jellyfin.Api/Controllers/DlnaServerController.cs +++ b/Jellyfin.Api/Controllers/DlnaServerController.cs @@ -5,7 +5,6 @@ using System.IO; using System.Net.Mime; using System.Threading.Tasks; using Emby.Dlna; -using Emby.Dlna.Main; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using MediaBrowser.Controller.Dlna; @@ -35,15 +34,17 @@ public class DlnaServerController : BaseJellyfinApiController /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param> /// <param name="contentDirectory">Instance of the <see cref="IContentDirectory"/> interface.</param> /// <param name="connectionManager">Instance of the <see cref="IConnectionManager"/> interface.</param> + /// <param name="mediaReceiverRegistrar">Instance of the <see cref="IMediaReceiverRegistrar"/> interface.</param> public DlnaServerController( IDlnaManager dlnaManager, IContentDirectory contentDirectory, - IConnectionManager connectionManager) + IConnectionManager connectionManager, + IMediaReceiverRegistrar mediaReceiverRegistrar) { _dlnaManager = dlnaManager; _contentDirectory = contentDirectory; _connectionManager = connectionManager; - _mediaReceiverRegistrar = DlnaEntryPoint.Current.MediaReceiverRegistrar; + _mediaReceiverRegistrar = mediaReceiverRegistrar; } /// <summary> From 2e1b8ea62d92263d087cbc8996886cdecc4b1705 Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Wed, 11 Oct 2023 11:37:28 -0400 Subject: [PATCH 708/858] Remove DlnaEntryPoint.Enabled --- Emby.Dlna/Main/DlnaEntryPoint.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index ea996a4424..06c39ecca3 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -103,11 +103,6 @@ namespace Emby.Dlna.Main } } - /// <summary> - /// Gets a value indicating whether the dlna server is enabled. - /// </summary> - public static bool Enabled { get; private set; } - public async Task RunAsync() { await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false); @@ -134,8 +129,6 @@ namespace Emby.Dlna.Main private void ReloadComponents() { var options = _config.GetDlnaConfiguration(); - Enabled = options.EnableServer; - StartSsdpHandler(); if (options.EnableServer) From 5b51645381b1b657aa64a37efffe34c76042ab51 Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Wed, 11 Oct 2023 11:49:22 -0400 Subject: [PATCH 709/858] Don't manually dispose DeviceDiscovery The lifetime of DeviceDiscovery is managed by DI --- Emby.Dlna/Main/DlnaEntryPoint.cs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index 06c39ecca3..82faa45b39 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -188,19 +188,6 @@ namespace Emby.Dlna.Main } } - private void DisposeDeviceDiscovery() - { - try - { - _logger.LogInformation("Disposing DeviceDiscovery"); - ((DeviceDiscovery)_deviceDiscovery).Dispose(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error stopping device discovery"); - } - } - public void StartDevicePublisher(Configuration.DlnaOptions options) { if (_publisher is not null) @@ -399,7 +386,6 @@ namespace Emby.Dlna.Main DisposeDevicePublisher(); DisposePlayToManager(); - DisposeDeviceDiscovery(); if (_communicationsServer is not null) { From d7748cfa0476280cce9dba34b4512cc58760c8bb Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Wed, 11 Oct 2023 18:32:57 +0200 Subject: [PATCH 710/858] Multiple Stream changes * Remove useless MemoryStream in DlnaHttpClient * Use HttpContent.ReadFromJsonAsync extension * Call ConfigureAwait for IAsyncDisposable * Use HttpContent.CopyToAsync where possible --- Emby.Dlna/PlayTo/DlnaHttpClient.cs | 53 ++++++++-------- .../Channels/ChannelManager.cs | 7 ++- .../Emby.Server.Implementations.csproj | 2 - .../Library/LiveStreamHelper.cs | 18 ++++-- .../Library/MediaSourceManager.cs | 15 +++-- .../LiveTv/EmbyTV/EmbyTV.cs | 14 +++-- .../LiveTv/Listings/SchedulesDirect.cs | 25 +++----- .../TunerHosts/HdHomerun/HdHomerunHost.cs | 61 +++++++++---------- .../Localization/LocalizationManager.cs | 35 ++++++----- .../Plugins/PluginManager.cs | 18 ++++-- .../Updates/InstallationManager.cs | 5 +- Jellyfin.Api/Jellyfin.Api.csproj | 2 - .../ClientEvent/ClientEventLogger.cs | 9 ++- .../Plugins/AudioDb/AudioDbAlbumProvider.cs | 15 ++--- .../Plugins/AudioDb/AudioDbArtistProvider.cs | 21 +++---- .../Plugins/Omdb/OmdbItemProvider.cs | 35 +++++------ .../AuthHelper.cs | 11 +--- .../Controllers/BrandingControllerTests.cs | 4 +- .../Controllers/DashboardControllerTests.cs | 7 +-- .../Controllers/DlnaControllerTests.cs | 12 +--- .../Controllers/ItemsControllerTests.cs | 5 +- .../Controllers/StartupControllerTests.cs | 9 +-- .../Controllers/UserControllerTests.cs | 9 +-- .../Controllers/UserLibraryControllerTests.cs | 13 ++-- .../OpenApiSpecTests.cs | 4 +- 25 files changed, 195 insertions(+), 214 deletions(-) diff --git a/Emby.Dlna/PlayTo/DlnaHttpClient.cs b/Emby.Dlna/PlayTo/DlnaHttpClient.cs index 220aa1a8dc..255c51f19a 100644 --- a/Emby.Dlna/PlayTo/DlnaHttpClient.cs +++ b/Emby.Dlna/PlayTo/DlnaHttpClient.cs @@ -55,41 +55,42 @@ namespace Emby.Dlna.PlayTo var client = _httpClientFactory.CreateClient(NamedClient.Dlna); using var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false); response.EnsureSuccessStatusCode(); - await using MemoryStream ms = new MemoryStream(); - await response.Content.CopyToAsync(ms, cancellationToken).ConfigureAwait(false); - ms.Position = 0; - try + Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + await using (stream.ConfigureAwait(false)) { - return await XDocument.LoadAsync( - ms, - LoadOptions.None, - cancellationToken).ConfigureAwait(false); - } - catch (XmlException) - { - // try correcting the Xml response with common errors - ms.Position = 0; - using StreamReader sr = new StreamReader(ms); - var xmlString = await sr.ReadToEndAsync(cancellationToken).ConfigureAwait(false); - - // find and replace unescaped ampersands (&) - xmlString = EscapeAmpersandRegex().Replace(xmlString, "&"); - try { - // retry reading Xml - using var xmlReader = new StringReader(xmlString); return await XDocument.LoadAsync( - xmlReader, + stream, LoadOptions.None, cancellationToken).ConfigureAwait(false); } - catch (XmlException ex) + catch (XmlException) { - _logger.LogError(ex, "Failed to parse response"); - _logger.LogDebug("Malformed response: {Content}\n", xmlString); + // try correcting the Xml response with common errors + stream.Position = 0; + using StreamReader sr = new StreamReader(stream); + var xmlString = await sr.ReadToEndAsync(cancellationToken).ConfigureAwait(false); - return null; + // find and replace unescaped ampersands (&) + xmlString = EscapeAmpersandRegex().Replace(xmlString, "&"); + + try + { + // retry reading Xml + using var xmlReader = new StringReader(xmlString); + return await XDocument.LoadAsync( + xmlReader, + LoadOptions.None, + cancellationToken).ConfigureAwait(false); + } + catch (XmlException ex) + { + _logger.LogError(ex, "Failed to parse response"); + _logger.LogDebug("Malformed response: {Content}\n", xmlString); + + return null; + } } } } diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs index 961e225e9e..3036cbcf7c 100644 --- a/Emby.Server.Implementations/Channels/ChannelManager.cs +++ b/Emby.Server.Implementations/Channels/ChannelManager.cs @@ -371,8 +371,11 @@ namespace Emby.Server.Implementations.Channels Directory.CreateDirectory(Path.GetDirectoryName(path)); - await using FileStream createStream = File.Create(path); - await JsonSerializer.SerializeAsync(createStream, mediaSources, _jsonOptions).ConfigureAwait(false); + FileStream createStream = File.Create(path); + await using (createStream.ConfigureAwait(false)) + { + await JsonSerializer.SerializeAsync(createStream, mediaSources, _jsonOptions).ConfigureAwait(false); + } } /// <inheritdoc /> diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 3aab0a5e9d..80263c1394 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -43,8 +43,6 @@ <TargetFramework>net7.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> - <!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 --> - <NoWarn>AD0001</NoWarn> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> diff --git a/Emby.Server.Implementations/Library/LiveStreamHelper.cs b/Emby.Server.Implementations/Library/LiveStreamHelper.cs index 936a08da81..59d705acef 100644 --- a/Emby.Server.Implementations/Library/LiveStreamHelper.cs +++ b/Emby.Server.Implementations/Library/LiveStreamHelper.cs @@ -48,15 +48,20 @@ namespace Emby.Server.Implementations.Library if (!string.IsNullOrEmpty(cacheKey)) { + FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath); try { - await using FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath); mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); // _logger.LogDebug("Found cached media info"); } - catch + catch (Exception ex) { + _logger.LogError(ex, "Error deserializing mediainfo cache"); + } + finally + { + await jsonStream.DisposeAsync().ConfigureAwait(false); } } @@ -84,10 +89,13 @@ namespace Emby.Server.Implementations.Library if (cacheFilePath is not null) { Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath)); - await using FileStream createStream = AsyncFile.OpenWrite(cacheFilePath); - await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false); + FileStream createStream = AsyncFile.OpenWrite(cacheFilePath); + await using (createStream.ConfigureAwait(false)) + { + await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false); + } - // _logger.LogDebug("Saved media info to {0}", cacheFilePath); + _logger.LogDebug("Saved media info to {0}", cacheFilePath); } } diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index c9a26a30f5..91469dba99 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -625,17 +625,19 @@ namespace Emby.Server.Implementations.Library if (!string.IsNullOrEmpty(cacheKey)) { + FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath); try { - await using FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath); mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); - - // _logger.LogDebug("Found cached media info"); } catch (Exception ex) { _logger.LogDebug(ex, "_jsonSerializer.DeserializeFromFile threw an exception."); } + finally + { + await jsonStream.DisposeAsync().ConfigureAwait(false); + } } if (mediaInfo is null) @@ -664,8 +666,11 @@ namespace Emby.Server.Implementations.Library if (cacheFilePath is not null) { Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath)); - await using FileStream createStream = File.Create(cacheFilePath); - await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false); + FileStream createStream = File.Create(cacheFilePath); + await using (createStream.ConfigureAwait(false)) + { + await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false); + } // _logger.LogDebug("Saved media info to {0}", cacheFilePath); } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index b9d0f170ac..74b62ca3f2 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -1851,7 +1851,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return; } - await using (var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None)) + var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None); + await using (stream.ConfigureAwait(false)) { var settings = new XmlWriterSettings { @@ -1860,7 +1861,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV Async = true }; - await using (var writer = XmlWriter.Create(stream, settings)) + var writer = XmlWriter.Create(stream, settings); + await using (writer.ConfigureAwait(false)) { await writer.WriteStartDocumentAsync(true).ConfigureAwait(false); await writer.WriteStartElementAsync(null, "tvshow", null).ConfigureAwait(false); @@ -1914,7 +1916,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return; } - await using (var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None)) + var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None); + await using (stream.ConfigureAwait(false)) { var settings = new XmlWriterSettings { @@ -1927,7 +1930,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV var isSeriesEpisode = timer.IsProgramSeries; - await using (var writer = XmlWriter.Create(stream, settings)) + var writer = XmlWriter.Create(stream, settings); + await using (writer.ConfigureAwait(false)) { await writer.WriteStartDocumentAsync(true).ConfigureAwait(false); @@ -1965,7 +1969,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } else { - await writer.WriteStartElementAsync(null, "movie", null); + await writer.WriteStartElementAsync(null, "movie", null).ConfigureAwait(false); if (!string.IsNullOrWhiteSpace(item.Name)) { diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index 7645c6c52d..6b0520ad0f 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -106,8 +106,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings options.Content = JsonContent.Create(requestList, options: _jsonOptions); options.Headers.TryAddWithoutValidation("token", token); using var response = await Send(options, true, info, cancellationToken).ConfigureAwait(false); - await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - var dailySchedules = await JsonSerializer.DeserializeAsync<IReadOnlyList<DayDto>>(responseStream, _jsonOptions, cancellationToken).ConfigureAwait(false); + var dailySchedules = await response.Content.ReadFromJsonAsync<IReadOnlyList<DayDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false); if (dailySchedules is null) { return Array.Empty<ProgramInfo>(); @@ -122,8 +121,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings programRequestOptions.Content = JsonContent.Create(programIds, options: _jsonOptions); using var innerResponse = await Send(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false); - await using var innerResponseStream = await innerResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - var programDetails = await JsonSerializer.DeserializeAsync<IReadOnlyList<ProgramDetailsDto>>(innerResponseStream, _jsonOptions, cancellationToken).ConfigureAwait(false); + var programDetails = await innerResponse.Content.ReadFromJsonAsync<IReadOnlyList<ProgramDetailsDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false); if (programDetails is null) { return Array.Empty<ProgramInfo>(); @@ -482,8 +480,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings try { using var innerResponse2 = await Send(message, true, info, cancellationToken).ConfigureAwait(false); - await using var response = await innerResponse2.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - return await JsonSerializer.DeserializeAsync<IReadOnlyList<ShowImagesDto>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false); + return await innerResponse2.Content.ReadFromJsonAsync<IReadOnlyList<ShowImagesDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { @@ -510,10 +507,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings try { using var httpResponse = await Send(options, false, info, cancellationToken).ConfigureAwait(false); - await using var response = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - - var root = await JsonSerializer.DeserializeAsync<IReadOnlyList<HeadendsDto>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false); - + var root = await httpResponse.Content.ReadFromJsonAsync<IReadOnlyList<HeadendsDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false); if (root is not null) { foreach (HeadendsDto headend in root) @@ -649,8 +643,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false); response.EnsureSuccessStatusCode(); - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - var root = await JsonSerializer.DeserializeAsync<TokenDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false); + var root = await response.Content.ReadFromJsonAsync<TokenDto>(_jsonOptions, cancellationToken).ConfigureAwait(false); if (string.Equals(root?.Message, "OK", StringComparison.Ordinal)) { _logger.LogInformation("Authenticated with Schedules Direct token: {Token}", root.Token); @@ -691,10 +684,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings { using var httpResponse = await Send(options, false, null, cancellationToken).ConfigureAwait(false); httpResponse.EnsureSuccessStatusCode(); - await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - using var response = httpResponse.Content; - var root = await JsonSerializer.DeserializeAsync<LineupsDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false); - + var root = await httpResponse.Content.ReadFromJsonAsync<LineupsDto>(_jsonOptions, cancellationToken).ConfigureAwait(false); return root?.Lineups.Any(i => string.Equals(info.ListingsId, i.Lineup, StringComparison.OrdinalIgnoreCase)) ?? false; } catch (HttpRequestException ex) @@ -748,8 +738,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings options.Headers.TryAddWithoutValidation("token", token); using var httpResponse = await Send(options, true, info, cancellationToken).ConfigureAwait(false); - await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - var root = await JsonSerializer.DeserializeAsync<ChannelDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false); + var root = await httpResponse.Content.ReadFromJsonAsync<ChannelDto>(_jsonOptions, cancellationToken).ConfigureAwait(false); if (root is null) { return new List<ChannelInfo>(); diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index 7e588f6812..4f96dde445 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -9,6 +9,7 @@ using System.IO; using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Http.Json; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -76,13 +77,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL ?? model.BaseURL + "/lineup.json", HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - var lineup = await JsonSerializer.DeserializeAsync<List<Channels>>(stream, _jsonOptions, cancellationToken) - .ConfigureAwait(false) ?? new List<Channels>(); - + var lineup = await response.Content.ReadFromJsonAsync<IEnumerable<Channels>>(_jsonOptions, cancellationToken).ConfigureAwait(false) ?? Enumerable.Empty<Channels>(); if (info.ImportFavoritesOnly) { - lineup = lineup.Where(i => i.Favorite).ToList(); + lineup = lineup.Where(i => i.Favorite); } return lineup.Where(i => !i.DRM).ToList(); @@ -129,9 +127,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun .GetAsync(GetApiUrl(info) + "/discover.json", HttpCompletionOption.ResponseHeadersRead, cancellationToken) .ConfigureAwait(false); response.EnsureSuccessStatusCode(); - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - var discoverResponse = await JsonSerializer.DeserializeAsync<DiscoverResponse>(stream, _jsonOptions, cancellationToken) - .ConfigureAwait(false); + var discoverResponse = await response.Content.ReadFromJsonAsync<DiscoverResponse>(_jsonOptions, cancellationToken).ConfigureAwait(false); if (!string.IsNullOrEmpty(cacheKey)) { @@ -175,34 +171,37 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun using var response = await _httpClientFactory.CreateClient(NamedClient.Default) .GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/tuners.html", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken) .ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - using var sr = new StreamReader(stream, System.Text.Encoding.UTF8); var tuners = new List<LiveTvTunerInfo>(); - await foreach (var line in sr.ReadAllLinesAsync().ConfigureAwait(false)) + var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + await using (stream.ConfigureAwait(false)) { - string stripedLine = StripXML(line); - if (stripedLine.Contains("Channel", StringComparison.Ordinal)) + using var sr = new StreamReader(stream, System.Text.Encoding.UTF8); + await foreach (var line in sr.ReadAllLinesAsync().ConfigureAwait(false)) { - LiveTvTunerStatus status; - var index = stripedLine.IndexOf("Channel", StringComparison.OrdinalIgnoreCase); - var name = stripedLine.Substring(0, index - 1); - var currentChannel = stripedLine.Substring(index + 7); - if (string.Equals(currentChannel, "none", StringComparison.Ordinal)) + string stripedLine = StripXML(line); + if (stripedLine.Contains("Channel", StringComparison.Ordinal)) { - status = LiveTvTunerStatus.LiveTv; - } - else - { - status = LiveTvTunerStatus.Available; - } + LiveTvTunerStatus status; + var index = stripedLine.IndexOf("Channel", StringComparison.OrdinalIgnoreCase); + var name = stripedLine.Substring(0, index - 1); + var currentChannel = stripedLine.Substring(index + 7); + if (string.Equals(currentChannel, "none", StringComparison.Ordinal)) + { + status = LiveTvTunerStatus.LiveTv; + } + else + { + status = LiveTvTunerStatus.Available; + } - tuners.Add(new LiveTvTunerInfo - { - Name = name, - SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber, - ProgramName = currentChannel, - Status = status - }); + tuners.Add(new LiveTvTunerInfo + { + Name = name, + SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber, + ProgramName = currentChannel, + Status = status + }); + } } } diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index 96f4353998..16776b6bd6 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -71,25 +71,28 @@ namespace Emby.Server.Implementations.Localization string countryCode = resource.Substring(RatingsPath.Length, 2); var dict = new Dictionary<string, ParentalRating>(StringComparer.OrdinalIgnoreCase); - await using var stream = _assembly.GetManifestResourceStream(resource); - using var reader = new StreamReader(stream!); // shouldn't be null here, we just got the resource path from Assembly.GetManifestResourceNames() - await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false)) + var stream = _assembly.GetManifestResourceStream(resource); + await using (stream!.ConfigureAwait(false)) // shouldn't be null here, we just got the resource path from Assembly.GetManifestResourceNames() { - if (string.IsNullOrWhiteSpace(line)) + using var reader = new StreamReader(stream!); + await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false)) { - continue; - } + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } - string[] parts = line.Split(','); - if (parts.Length == 2 - && int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) - { - var name = parts[0]; - dict.Add(name, new ParentalRating(name, value)); - } - else - { - _logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode); + string[] parts = line.Split(','); + if (parts.Length == 2 + && int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) + { + var name = parts[0]; + dict.Add(name, new ParentalRating(name, value)); + } + else + { + _logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode); + } } } diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs index d7189ef0ca..20793ee394 100644 --- a/Emby.Server.Implementations/Plugins/PluginManager.cs +++ b/Emby.Server.Implementations/Plugins/PluginManager.cs @@ -386,11 +386,11 @@ namespace Emby.Server.Implementations.Plugins var url = new Uri(packageInfo.ImageUrl); imagePath = Path.Join(path, url.Segments[^1]); - await using var fileStream = AsyncFile.OpenWrite(imagePath); - + var fileStream = AsyncFile.OpenWrite(imagePath); + Stream? downloadStream = null; try { - await using var downloadStream = await HttpClientFactory + downloadStream = await HttpClientFactory .CreateClient(NamedClient.Default) .GetStreamAsync(url) .ConfigureAwait(false); @@ -402,6 +402,14 @@ namespace Emby.Server.Implementations.Plugins _logger.LogError(ex, "Failed to download image to path {Path} on disk.", imagePath); imagePath = string.Empty; } + finally + { + await fileStream.DisposeAsync().ConfigureAwait(false); + if (downloadStream is not null) + { + await downloadStream.DisposeAsync().ConfigureAwait(false); + } + } } var manifest = new PluginManifest @@ -421,7 +429,7 @@ namespace Emby.Server.Implementations.Plugins ImagePath = imagePath }; - if (!await ReconcileManifest(manifest, path)) + if (!await ReconcileManifest(manifest, path).ConfigureAwait(false)) { // An error occurred during reconciliation and saving could be undesirable. return false; @@ -458,7 +466,7 @@ namespace Emby.Server.Implementations.Plugins } using var metaStream = File.OpenRead(metafile); - var localManifest = await JsonSerializer.DeserializeAsync<PluginManifest>(metaStream, _jsonOptions); + var localManifest = await JsonSerializer.DeserializeAsync<PluginManifest>(metaStream, _jsonOptions).ConfigureAwait(false); localManifest ??= new PluginManifest(); if (!Equals(localManifest.Id, manifest.Id)) diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index 77d385ba1c..c717744b12 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -520,10 +520,9 @@ namespace Emby.Server.Implementations.Updates // CA5351: Do Not Use Broken Cryptographic Algorithms #pragma warning disable CA5351 - using var md5 = MD5.Create(); cancellationToken.ThrowIfCancellationRequested(); - var hash = Convert.ToHexString(md5.ComputeHash(stream)); + var hash = Convert.ToHexString(await MD5.HashDataAsync(stream, cancellationToken).ConfigureAwait(false)); if (!string.Equals(package.Checksum, hash, StringComparison.OrdinalIgnoreCase)) { _logger.LogError( @@ -556,7 +555,7 @@ namespace Emby.Server.Implementations.Updates reader.ExtractToDirectory(targetDir, true); // Ensure we create one or populate existing ones with missing data. - await _pluginManager.PopulateManifest(package.PackageInfo, package.Version, targetDir, status); + await _pluginManager.PopulateManifest(package.PackageInfo, package.Version, targetDir, status).ConfigureAwait(false); _pluginManager.ImportPluginFrom(targetDir); } diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index 6a0a4706be..7ac231885e 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -8,8 +8,6 @@ <PropertyGroup> <TargetFramework>net7.0</TargetFramework> <GenerateDocumentationFile>true</GenerateDocumentationFile> - <!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 --> - <NoWarn>AD0001</NoWarn> </PropertyGroup> <ItemGroup> diff --git a/MediaBrowser.Controller/ClientEvent/ClientEventLogger.cs b/MediaBrowser.Controller/ClientEvent/ClientEventLogger.cs index dea1c2f32a..2a7e6be0fd 100644 --- a/MediaBrowser.Controller/ClientEvent/ClientEventLogger.cs +++ b/MediaBrowser.Controller/ClientEvent/ClientEventLogger.cs @@ -23,9 +23,12 @@ namespace MediaBrowser.Controller.ClientEvent { var fileName = $"upload_{clientName}_{clientVersion}_{DateTime.UtcNow:yyyyMMddHHmmss}_{Guid.NewGuid():N}.log"; var logFilePath = Path.Combine(_applicationPaths.LogDirectoryPath, fileName); - await using var fileStream = new FileStream(logFilePath, FileMode.CreateNew, FileAccess.Write, FileShare.None); - await fileContents.CopyToAsync(fileStream).ConfigureAwait(false); - return fileName; + var fileStream = new FileStream(logFilePath, FileMode.CreateNew, FileAccess.Write, FileShare.None); + await using (fileStream.ConfigureAwait(false)) + { + await fileContents.CopyToAsync(fileStream).ConfigureAwait(false); + return fileName; + } } } } diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs index 55e2474a5a..daad9706cd 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs @@ -176,17 +176,12 @@ namespace MediaBrowser.Providers.Plugins.AudioDb Directory.CreateDirectory(Path.GetDirectoryName(path)); using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken).ConfigureAwait(false); - var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - await using (stream.ConfigureAwait(false)) + var fileStreamOptions = AsyncFile.WriteOptions; + fileStreamOptions.Mode = FileMode.Create; + var fs = new FileStream(path, fileStreamOptions); + await using (fs.ConfigureAwait(false)) { - var fileStreamOptions = AsyncFile.WriteOptions; - fileStreamOptions.Mode = FileMode.Create; - fileStreamOptions.PreallocationSize = stream.Length; - var xmlFileStream = new FileStream(path, fileStreamOptions); - await using (xmlFileStream.ConfigureAwait(false)) - { - await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false); - } + await response.Content.CopyToAsync(fs, cancellationToken).ConfigureAwait(false); } } diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs index f3385b3a91..92742b1aa5 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs @@ -154,20 +154,15 @@ namespace MediaBrowser.Providers.Plugins.AudioDb using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken).ConfigureAwait(false); response.EnsureSuccessStatusCode(); - var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - await using (stream.ConfigureAwait(false)) - { - var path = GetArtistInfoPath(_config.ApplicationPaths, musicBrainzId); - Directory.CreateDirectory(Path.GetDirectoryName(path)); + var path = GetArtistInfoPath(_config.ApplicationPaths, musicBrainzId); + Directory.CreateDirectory(Path.GetDirectoryName(path)); - var fileStreamOptions = AsyncFile.WriteOptions; - fileStreamOptions.Mode = FileMode.Create; - fileStreamOptions.PreallocationSize = stream.Length; - var xmlFileStream = new FileStream(path, fileStreamOptions); - await using (xmlFileStream.ConfigureAwait(false)) - { - await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false); - } + var fileStreamOptions = AsyncFile.WriteOptions; + fileStreamOptions.Mode = FileMode.Create; + var xmlFileStream = new FileStream(path, fileStreamOptions); + await using (xmlFileStream.ConfigureAwait(false)) + { + await response.Content.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false); } } diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs index e4bb4eaead..e84f1359b7 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs @@ -8,6 +8,7 @@ using System.Globalization; using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Http.Json; using System.Text; using System.Text.Json; using System.Threading; @@ -137,31 +138,27 @@ namespace MediaBrowser.Providers.Plugins.Omdb var url = OmdbProvider.GetOmdbUrl(urlQuery.ToString()); using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken).ConfigureAwait(false); - var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - await using (stream.ConfigureAwait(false)) + if (isSearch) { - if (isSearch) + var searchResultList = await response.Content.ReadFromJsonAsync<SearchResultList>(_jsonOptions, cancellationToken).ConfigureAwait(false); + if (searchResultList?.Search is not null) { - var searchResultList = await JsonSerializer.DeserializeAsync<SearchResultList>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false); - if (searchResultList?.Search is not null) + var resultCount = searchResultList.Search.Count; + var result = new RemoteSearchResult[resultCount]; + for (var i = 0; i < resultCount; i++) { - var resultCount = searchResultList.Search.Count; - var result = new RemoteSearchResult[resultCount]; - for (var i = 0; i < resultCount; i++) - { - result[i] = ResultToMetadataResult(searchResultList.Search[i], searchInfo, indexNumberEnd); - } + result[i] = ResultToMetadataResult(searchResultList.Search[i], searchInfo, indexNumberEnd); + } - return result; - } + return result; } - else + } + else + { + var result = await response.Content.ReadFromJsonAsync<SearchResult>(_jsonOptions, cancellationToken).ConfigureAwait(false); + if (string.Equals(result?.Response, "true", StringComparison.OrdinalIgnoreCase)) { - var result = await JsonSerializer.DeserializeAsync<SearchResult>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false); - if (string.Equals(result?.Response, "true", StringComparison.OrdinalIgnoreCase)) - { - return new[] { ResultToMetadataResult(result, searchInfo, indexNumberEnd) }; - } + return new[] { ResultToMetadataResult(result, searchInfo, indexNumberEnd) }; } } diff --git a/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs b/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs index 5ddbd30d1e..4e8aec9f19 100644 --- a/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs +++ b/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs @@ -40,9 +40,7 @@ namespace Jellyfin.Server.Integration.Tests using var authResponse = await client.SendAsync(httpRequest); authResponse.EnsureSuccessStatusCode(); - var auth = await JsonSerializer.DeserializeAsync<AuthenticationResultDto>( - await authResponse.Content.ReadAsStreamAsync(), - jsonOptions); + var auth = await authResponse.Content.ReadFromJsonAsync<AuthenticationResultDto>(jsonOptions); return auth!.AccessToken; } @@ -51,8 +49,7 @@ namespace Jellyfin.Server.Integration.Tests { using var response = await client.GetAsync("Users/Me"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var userDto = await JsonSerializer.DeserializeAsync<UserDto>( - await response.Content.ReadAsStreamAsync(), JsonDefaults.Options); + var userDto = await response.Content.ReadFromJsonAsync<UserDto>(JsonDefaults.Options); Assert.NotNull(userDto); return userDto; } @@ -67,9 +64,7 @@ namespace Jellyfin.Server.Integration.Tests var response = await client.GetAsync($"Users/{userId}/Items/Root"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var rootDto = await JsonSerializer.DeserializeAsync<BaseItemDto>( - await response.Content.ReadAsStreamAsync(), - JsonDefaults.Options); + var rootDto = await response.Content.ReadFromJsonAsync<BaseItemDto>(JsonDefaults.Options); Assert.NotNull(rootDto); return rootDto; } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/BrandingControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/BrandingControllerTests.cs index 87136dfc8a..8761cf69bc 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/BrandingControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/BrandingControllerTests.cs @@ -1,4 +1,5 @@ using System.Net; +using System.Net.Http.Json; using System.Net.Mime; using System.Text; using System.Text.Json; @@ -30,8 +31,7 @@ namespace Jellyfin.Server.Integration.Tests Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType); Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet); - var responseBody = await response.Content.ReadAsStreamAsync(); - _ = await JsonSerializer.DeserializeAsync<BrandingOptions>(responseBody); + await response.Content.ReadFromJsonAsync<BrandingOptions>(); } [Theory] diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs index bd6e1b690a..39d449e27e 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs @@ -1,5 +1,6 @@ using System.IO; using System.Net; +using System.Net.Http.Json; using System.Net.Mime; using System.Text; using System.Text.Json; @@ -64,8 +65,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var res = await response.Content.ReadAsStreamAsync(); - _ = await JsonSerializer.DeserializeAsync<ConfigurationPageInfo[]>(res, _jsonOpions); + _ = await response.Content.ReadFromJsonAsync<ConfigurationPageInfo[]>(_jsonOpions); // TODO: check content } @@ -81,8 +81,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType); Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet); - var res = await response.Content.ReadAsStreamAsync(); - var data = await JsonSerializer.DeserializeAsync<ConfigurationPageInfo[]>(res, _jsonOpions); + var data = await response.Content.ReadFromJsonAsync<ConfigurationPageInfo[]>(_jsonOpions); Assert.NotNull(data); Assert.Empty(data); } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/DlnaControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/DlnaControllerTests.cs index 65e70caa02..e5d5e785cb 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/DlnaControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/DlnaControllerTests.cs @@ -93,9 +93,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType); Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet); - var profiles = await JsonSerializer.DeserializeAsync<DeviceProfileInfo[]>( - await response.Content.ReadAsStreamAsync(), - _jsonOptions); + var profiles = await response.Content.ReadFromJsonAsync<DeviceProfileInfo[]>(_jsonOptions); var newProfile = profiles?.FirstOrDefault(x => string.Equals(x.Name, "ThisProfileIsNew", StringComparison.Ordinal)); Assert.NotNull(newProfile); @@ -124,9 +122,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType); Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet); - var profiles = await JsonSerializer.DeserializeAsync<DeviceProfileInfo[]>( - await response.Content.ReadAsStreamAsync(), - _jsonOptions); + var profiles = await response.Content.ReadFromJsonAsync<DeviceProfileInfo[]>(_jsonOptions); Assert.Null(profiles?.FirstOrDefault(x => string.Equals(x.Name, "ThisProfileIsNew", StringComparison.Ordinal))); var newProfile = profiles?.FirstOrDefault(x => string.Equals(x.Name, "ThisProfileIsUpdated", StringComparison.Ordinal)); @@ -150,9 +146,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType); Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet); - var profiles = await JsonSerializer.DeserializeAsync<DeviceProfileInfo[]>( - await response.Content.ReadAsStreamAsync(), - _jsonOptions); + var profiles = await response.Content.ReadFromJsonAsync<DeviceProfileInfo[]>(_jsonOptions); Assert.Null(profiles?.FirstOrDefault(x => string.Equals(x.Name, "ThisProfileIsUpdated", StringComparison.Ordinal))); } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs index a12e7ca0d8..23de2489e5 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs @@ -1,6 +1,7 @@ using System; using System.Globalization; using System.Net; +using System.Net.Http.Json; using System.Text.Json; using System.Threading.Tasks; using Jellyfin.Extensions.Json; @@ -56,9 +57,7 @@ public sealed class ItemsControllerTests : IClassFixture<JellyfinApplicationFact var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id)); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var items = await JsonSerializer.DeserializeAsync<QueryResult<BaseItemDto>>( - await response.Content.ReadAsStreamAsync(), - _jsonOptions); + var items = await response.Content.ReadFromJsonAsync<QueryResult<BaseItemDto>>(_jsonOptions); Assert.NotNull(items); } } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs index 2d3879bdb6..36861294b5 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs @@ -43,8 +43,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode); Assert.Equal(MediaTypeNames.Application.Json, getResponse.Content.Headers.ContentType?.MediaType); - using var responseStream = await getResponse.Content.ReadAsStreamAsync(); - var newConfig = await JsonSerializer.DeserializeAsync<StartupConfigurationDto>(responseStream, _jsonOptions); + var newConfig = await getResponse.Content.ReadFromJsonAsync<StartupConfigurationDto>(_jsonOptions); Assert.Equal(config.UICulture, newConfig!.UICulture); Assert.Equal(config.MetadataCountryCode, newConfig.MetadataCountryCode); Assert.Equal(config.PreferredMetadataLanguage, newConfig.PreferredMetadataLanguage); @@ -60,8 +59,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType); - using var contentStream = await response.Content.ReadAsStreamAsync(); - var user = await JsonSerializer.DeserializeAsync<StartupUserDto>(contentStream, _jsonOptions); + var user = await response.Content.ReadFromJsonAsync<StartupUserDto>(_jsonOptions); Assert.NotNull(user); Assert.NotNull(user.Name); Assert.NotEmpty(user.Name); @@ -87,8 +85,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode); Assert.Equal(MediaTypeNames.Application.Json, getResponse.Content.Headers.ContentType?.MediaType); - var contentStream = await getResponse.Content.ReadAsStreamAsync(); - var newUser = await JsonSerializer.DeserializeAsync<StartupUserDto>(contentStream, _jsonOptions); + var newUser = await getResponse.Content.ReadFromJsonAsync<StartupUserDto>(_jsonOptions); Assert.NotNull(newUser); Assert.Equal(user.Name, newUser.Name); Assert.NotNull(newUser.Password); diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs index 79d03d539a..4fcacd2cad 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs @@ -43,8 +43,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers using var response = await client.GetAsync("Users/Public"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var users = await JsonSerializer.DeserializeAsync<UserDto[]>( - await response.Content.ReadAsStreamAsync(), _jsonOpions); + var users = await response.Content.ReadFromJsonAsync<UserDto[]>(_jsonOpions); // User are hidden by default Assert.NotNull(users); Assert.Empty(users); @@ -59,8 +58,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers using var response = await client.GetAsync("Users"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var users = await JsonSerializer.DeserializeAsync<UserDto[]>( - await response.Content.ReadAsStreamAsync(), _jsonOpions); + var users = await response.Content.ReadFromJsonAsync<UserDto[]>(_jsonOpions); Assert.NotNull(users); Assert.Single(users); Assert.False(users![0].HasConfiguredPassword); @@ -92,8 +90,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers using var response = await CreateUserByName(client, createRequest); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var user = await JsonSerializer.DeserializeAsync<UserDto>( - await response.Content.ReadAsStreamAsync(), _jsonOpions); + var user = await response.Content.ReadFromJsonAsync<UserDto>(_jsonOpions); Assert.Equal(TestUsername, user!.Name); Assert.False(user.HasPassword); Assert.False(user.HasConfiguredPassword); diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs index 826a0a69dd..130281c6d2 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs @@ -1,6 +1,7 @@ using System; using System.Globalization; using System.Net; +using System.Net.Http.Json; using System.Text.Json; using System.Threading.Tasks; using Jellyfin.Extensions.Json; @@ -85,9 +86,7 @@ public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicati var response = await client.GetAsync($"Users/{userDto.Id}/Items/{rootFolderDto.Id}"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var rootDto = await JsonSerializer.DeserializeAsync<BaseItemDto>( - await response.Content.ReadAsStreamAsync(), - _jsonOptions); + var rootDto = await response.Content.ReadFromJsonAsync<BaseItemDto>(_jsonOptions); Assert.NotNull(rootDto); } @@ -102,9 +101,7 @@ public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicati var response = await client.GetAsync($"Users/{userDto.Id}/Items/{rootFolderDto.Id}/Intros"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var rootDto = await JsonSerializer.DeserializeAsync<QueryResult<BaseItemDto>>( - await response.Content.ReadAsStreamAsync(), - _jsonOptions); + var rootDto = await response.Content.ReadFromJsonAsync<QueryResult<BaseItemDto>>(_jsonOptions); Assert.NotNull(rootDto); } @@ -121,9 +118,7 @@ public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicati var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id, rootFolderDto.Id)); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var rootDto = await JsonSerializer.DeserializeAsync<BaseItemDto[]>( - await response.Content.ReadAsStreamAsync(), - _jsonOptions); + var rootDto = await response.Content.ReadFromJsonAsync<BaseItemDto[]>(_jsonOptions); Assert.NotNull(rootDto); } } diff --git a/tests/Jellyfin.Server.Integration.Tests/OpenApiSpecTests.cs b/tests/Jellyfin.Server.Integration.Tests/OpenApiSpecTests.cs index 0ade345a1a..98195a2943 100644 --- a/tests/Jellyfin.Server.Integration.Tests/OpenApiSpecTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/OpenApiSpecTests.cs @@ -31,10 +31,10 @@ namespace Jellyfin.Server.Integration.Tests Assert.Equal("application/json; charset=utf-8", response.Content.Headers.ContentType?.ToString()); // Write out for publishing - var responseBody = await response.Content.ReadAsStringAsync(); string outputPath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? ".", "openapi.json")); _outputHelper.WriteLine("Writing OpenAPI Spec JSON to '{0}'.", outputPath); - File.WriteAllText(outputPath, responseBody); + await using var fs = File.Create(outputPath); + await response.Content.CopyToAsync(fs); } } } From f0618ce33531dfdcb8d70dc46de309d66903b54e Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Wed, 11 Oct 2023 13:26:42 -0400 Subject: [PATCH 711/858] Minor cleanup in DlnaEntryPoint --- Emby.Dlna/Main/DlnaEntryPoint.cs | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index 82faa45b39..f17e0ca3d7 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -50,7 +50,7 @@ namespace Emby.Dlna.Main private readonly IDeviceDiscovery _deviceDiscovery; private readonly ISocketFactory _socketFactory; private readonly INetworkManager _networkManager; - private readonly object _syncLock = new object(); + private readonly object _syncLock = new(); private readonly bool _disabled; private PlayToManager _manager; @@ -260,7 +260,7 @@ namespace Emby.Dlna.Main // This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc. }; - SetProperies(device, fullService); + SetProperties(device, fullService); _publisher.AddDevice(device); var embeddedDevices = new[] @@ -281,13 +281,13 @@ namespace Emby.Dlna.Main // This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc. }; - SetProperies(embeddedDevice, subDevice); + SetProperties(embeddedDevice, subDevice); device.AddDevice(embeddedDevice); } } } - private string CreateUuid(string text) + private static string CreateUuid(string text) { if (!Guid.TryParse(text, out var guid)) { @@ -297,15 +297,14 @@ namespace Emby.Dlna.Main return guid.ToString("D", CultureInfo.InvariantCulture); } - private void SetProperies(SsdpDevice device, string fullDeviceType) + private static void SetProperties(SsdpDevice device, string fullDeviceType) { - var service = fullDeviceType.Replace("urn:", string.Empty, StringComparison.OrdinalIgnoreCase).Replace(":1", string.Empty, StringComparison.OrdinalIgnoreCase); + var serviceParts = fullDeviceType + .Replace("urn:", string.Empty, StringComparison.OrdinalIgnoreCase) + .Replace(":1", string.Empty, StringComparison.OrdinalIgnoreCase) + .Split(':'); - var serviceParts = service.Split(':'); - - var deviceTypeNamespace = serviceParts[0].Replace('.', '-'); - - device.DeviceTypeNamespace = deviceTypeNamespace; + device.DeviceTypeNamespace = serviceParts[0].Replace('.', '-'); device.DeviceClass = serviceParts[1]; device.DeviceType = serviceParts[2]; } From 44380933a0091cba642715395d1b1edd64889120 Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Wed, 11 Oct 2023 13:35:51 -0400 Subject: [PATCH 712/858] Use DI for SsdpCommunicationsServer --- .../DlnaServiceCollectionExtensions.cs | 11 +++++ Emby.Dlna/Main/DlnaEntryPoint.cs | 48 +++---------------- 2 files changed, 17 insertions(+), 42 deletions(-) diff --git a/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs b/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs index 8361cc7e78..32f58cade1 100644 --- a/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs +++ b/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs @@ -11,7 +11,10 @@ using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.Dlna; using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Net; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Rssdp.Infrastructure; namespace Emby.Dlna.Extensions; @@ -54,5 +57,13 @@ public static class DlnaServiceCollectionExtensions services.AddSingleton<IContentDirectory, ContentDirectoryService>(); services.AddSingleton<IConnectionManager, ConnectionManagerService>(); services.AddSingleton<IMediaReceiverRegistrar, MediaReceiverRegistrarService>(); + + services.AddSingleton<ISsdpCommunicationsServer>(provider => new SsdpCommunicationsServer( + provider.GetRequiredService<ISocketFactory>(), + provider.GetRequiredService<INetworkManager>(), + provider.GetRequiredService<ILogger<SsdpCommunicationsServer>>()) + { + IsShared = true + }); } } diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index f17e0ca3d7..aa70124870 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -25,7 +25,6 @@ using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.Net; using Microsoft.Extensions.Logging; using Rssdp; using Rssdp.Infrastructure; @@ -48,14 +47,13 @@ namespace Emby.Dlna.Main private readonly IMediaSourceManager _mediaSourceManager; private readonly IMediaEncoder _mediaEncoder; private readonly IDeviceDiscovery _deviceDiscovery; - private readonly ISocketFactory _socketFactory; + private readonly ISsdpCommunicationsServer _communicationsServer; private readonly INetworkManager _networkManager; private readonly object _syncLock = new(); private readonly bool _disabled; private PlayToManager _manager; private SsdpDevicePublisher _publisher; - private ISsdpCommunicationsServer _communicationsServer; private bool _disposed; @@ -74,7 +72,7 @@ namespace Emby.Dlna.Main IMediaSourceManager mediaSourceManager, IDeviceDiscovery deviceDiscovery, IMediaEncoder mediaEncoder, - ISocketFactory socketFactory, + ISsdpCommunicationsServer communicationsServer, INetworkManager networkManager) { _config = config; @@ -90,7 +88,7 @@ namespace Emby.Dlna.Main _mediaSourceManager = mediaSourceManager; _deviceDiscovery = deviceDiscovery; _mediaEncoder = mediaEncoder; - _socketFactory = socketFactory; + _communicationsServer = communicationsServer; _networkManager = networkManager; _logger = loggerFactory.CreateLogger<DlnaEntryPoint>(); @@ -129,7 +127,7 @@ namespace Emby.Dlna.Main private void ReloadComponents() { var options = _config.GetDlnaConfiguration(); - StartSsdpHandler(); + StartDeviceDiscovery(); if (options.EnableServer) { @@ -150,37 +148,11 @@ namespace Emby.Dlna.Main } } - private void StartSsdpHandler() + private void StartDeviceDiscovery() { try { - if (_communicationsServer is null) - { - _communicationsServer = new SsdpCommunicationsServer( - _socketFactory, - _networkManager, - _logger) - { - IsShared = true - }; - - StartDeviceDiscovery(_communicationsServer); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error starting SSDP handlers"); - } - } - - private void StartDeviceDiscovery(ISsdpCommunicationsServer communicationsServer) - { - try - { - if (communicationsServer is not null) - { - ((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer); - } + ((DeviceDiscovery)_deviceDiscovery).Start(_communicationsServer); } catch (Exception ex) { @@ -385,14 +357,6 @@ namespace Emby.Dlna.Main DisposeDevicePublisher(); DisposePlayToManager(); - - if (_communicationsServer is not null) - { - _logger.LogInformation("Disposing SsdpCommunicationsServer"); - _communicationsServer.Dispose(); - _communicationsServer = null; - } - _disposed = true; } } From b8f4c1de697ed512fcc3263183d8e698a9773cac Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Wed, 11 Oct 2023 14:57:29 -0400 Subject: [PATCH 713/858] Fix documentation for AddDlnaServices --- Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs b/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs index 32f58cade1..87ec14d958 100644 --- a/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs +++ b/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs @@ -27,7 +27,7 @@ public static class DlnaServiceCollectionExtensions /// Adds DLNA services to the provided <see cref="IServiceCollection"/>. /// </summary> /// <param name="services">The <see cref="IServiceCollection"/>.</param> - /// <param name="applicationHost">the.</param> + /// <param name="applicationHost">The <see cref="IServerApplicationHost"/>.</param> public static void AddDlnaServices( this IServiceCollection services, IServerApplicationHost applicationHost) From d170be88b0ce9c60f7033123ce253c36dd54d589 Mon Sep 17 00:00:00 2001 From: nextlooper42 <nextlooper42@protonmail.com> Date: Wed, 11 Oct 2023 07:59:56 +0000 Subject: [PATCH 714/858] Translated using Weblate (Slovak) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/sk/ --- Emby.Server.Implementations/Localization/Core/sk.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/sk.json b/Emby.Server.Implementations/Localization/Core/sk.json index 858cc40dd8..c231d76fe0 100644 --- a/Emby.Server.Implementations/Localization/Core/sk.json +++ b/Emby.Server.Implementations/Localization/Core/sk.json @@ -124,5 +124,5 @@ "TaskKeyframeExtractorDescription": "Extrahuje kľúčové snímky z video súborov na vytvorenie presnejších HLS playlistov. Táto úloha môže trvať dlhšiu dobu.", "TaskKeyframeExtractor": "Extraktor kľúčových snímkov", "External": "Externé", - "HearingImpaired": "Sluchovo Postihnutý" + "HearingImpaired": "Sluchovo postihnutí" } From 9bf624b9addab6e1eff5ce255788007e5066a8ac Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 12 Oct 2023 10:52:53 +0000 Subject: [PATCH 715/858] Update github/codeql-action action to v2.22.2 --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 4c9d68d112..a6062940c3 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@fdcae64e1484d349b3366718cdfef3d404390e85 # v2.22.1 + uses: github/codeql-action/init@d90b8d79de6dc1f58e83a1499aa58d6c93dc28de # v2.22.2 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@fdcae64e1484d349b3366718cdfef3d404390e85 # v2.22.1 + uses: github/codeql-action/autobuild@d90b8d79de6dc1f58e83a1499aa58d6c93dc28de # v2.22.2 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@fdcae64e1484d349b3366718cdfef3d404390e85 # v2.22.1 + uses: github/codeql-action/analyze@d90b8d79de6dc1f58e83a1499aa58d6c93dc28de # v2.22.2 From a773b43a814e6e849bbb5eb0fa1bccf09a4c1945 Mon Sep 17 00:00:00 2001 From: Stepan Goremykin <goremukin@gmail.com> Date: Thu, 12 Oct 2023 21:28:04 +0200 Subject: [PATCH 716/858] Merge reading from file and parsing --- .../ScheduledTasks/Tasks/ChapterImagesTask.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs index c4658c32ba..5d15c3a21f 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs @@ -115,8 +115,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks { try { - var failHistoryText = await File.ReadAllTextAsync(failHistoryPath, cancellationToken).ConfigureAwait(false); - previouslyFailedImages = failHistoryText + previouslyFailedImages = (await File.ReadAllTextAsync(failHistoryPath, cancellationToken).ConfigureAwait(false)) .Split('|', StringSplitOptions.RemoveEmptyEntries) .ToList(); } From bbd7d8678c5f4ed54fefd900cc1b3ec2e9678d45 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 13 Oct 2023 03:19:46 +0000 Subject: [PATCH 717/858] Update xunit-dotnet monorepo --- Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 33736482c8..e26005f8e2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -86,8 +86,8 @@ <PackageVersion Include="TMDbLib" Version="2.0.0" /> <PackageVersion Include="UTF.Unknown" Version="2.5.1" /> <PackageVersion Include="Xunit.Priority" Version="1.1.6" /> - <PackageVersion Include="xunit.runner.visualstudio" Version="2.5.1" /> + <PackageVersion Include="xunit.runner.visualstudio" Version="2.5.3" /> <PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" /> - <PackageVersion Include="xunit" Version="2.5.1" /> + <PackageVersion Include="xunit" Version="2.5.2" /> </ItemGroup> </Project> From 2928d42fe0d83f6732be30c881d2a65d64e2fcb1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 13 Oct 2023 13:54:36 +0000 Subject: [PATCH 718/858] Update github/codeql-action action to v2.22.3 --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index a6062940c3..8385e84bf8 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@d90b8d79de6dc1f58e83a1499aa58d6c93dc28de # v2.22.2 + uses: github/codeql-action/init@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@d90b8d79de6dc1f58e83a1499aa58d6c93dc28de # v2.22.2 + uses: github/codeql-action/autobuild@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@d90b8d79de6dc1f58e83a1499aa58d6c93dc28de # v2.22.2 + uses: github/codeql-action/analyze@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3 From c7feea27fde8af60984c8fe41444dc245dbde395 Mon Sep 17 00:00:00 2001 From: Nick <20588554+nicknsy@users.noreply.github.com> Date: Fri, 13 Oct 2023 16:13:42 -0700 Subject: [PATCH 719/858] Avoid unnecessary string -> byte[] conversion (Bond-009) --- Jellyfin.Api/Controllers/TrickplayController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/TrickplayController.cs b/Jellyfin.Api/Controllers/TrickplayController.cs index e4f8f076e4..2dc960229a 100644 --- a/Jellyfin.Api/Controllers/TrickplayController.cs +++ b/Jellyfin.Api/Controllers/TrickplayController.cs @@ -61,7 +61,7 @@ public class TrickplayController : BaseJellyfinApiController return NotFound(); } - return new FileContentResult(Encoding.UTF8.GetBytes(playlist), MimeTypes.GetMimeType("playlist.m3u8")); + return Content(playlist, MimeTypes.GetMimeType("playlist.m3u8"), Encoding.UTF8); } /// <summary> From 13f46e3fff0cd387a7ed1fe34168e9fd83ea65ef Mon Sep 17 00:00:00 2001 From: Cody Robibero <cody@robibe.ro> Date: Sat, 14 Oct 2023 10:55:12 -0600 Subject: [PATCH 720/858] Shorten lines from review --- Jellyfin.Server.Implementations/Users/UserManager.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 31eda08a01..7e56e4c9e4 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -293,6 +293,7 @@ namespace Jellyfin.Server.Implementations.Users public UserDto GetUserDto(User user, string? remoteEndPoint = null) { var hasPassword = GetAuthenticationProvider(user).HasPassword(user); + var castReceiverApplications = _serverConfigurationManager.Configuration.CastReceiverApplications; return new UserDto { Name = user.Username, @@ -322,9 +323,9 @@ namespace Jellyfin.Server.Implementations.Users MyMediaExcludes = user.GetPreferenceValues<Guid>(PreferenceKind.MyMediaExcludes), LatestItemsExcludes = user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes), CastReceiverId = string.IsNullOrEmpty(user.CastReceiverId) - ? _serverConfigurationManager.Configuration.CastReceiverApplications.FirstOrDefault()?.Id - : _serverConfigurationManager.Configuration.CastReceiverApplications.FirstOrDefault(c => string.Equals(c.Id, user.CastReceiverId, StringComparison.Ordinal))?.Id - ?? _serverConfigurationManager.Configuration.CastReceiverApplications.FirstOrDefault()?.Id + ? castReceiverApplications.FirstOrDefault()?.Id + : castReceiverApplications.FirstOrDefault(c => string.Equals(c.Id, user.CastReceiverId, StringComparison.Ordinal))?.Id + ?? castReceiverApplications.FirstOrDefault()?.Id }, Policy = new UserPolicy { From 35bd0c00c965176d6ddb167605ed3a3eba9a4524 Mon Sep 17 00:00:00 2001 From: Cody Robibero <cody@robibe.ro> Date: Sat, 14 Oct 2023 11:01:03 -0600 Subject: [PATCH 721/858] Fix inverted condition --- Emby.Server.Implementations/Playlists/PlaylistManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index c783f2b01f..d2e2fd7d56 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -85,7 +85,7 @@ namespace Emby.Server.Implementations.Playlists throw new ArgumentException("No item exists with the supplied Id"); } - if (item.MediaType == MediaType.Unknown) + if (item.MediaType != MediaType.Unknown) { options.MediaType = item.MediaType; } @@ -107,7 +107,7 @@ namespace Emby.Server.Implementations.Playlists } } - if (options.MediaType is null || options.MediaType == MediaType.Unknown) + if (options.MediaType is not null && options.MediaType != MediaType.Unknown) { break; } From ed42fcf522084d4e98d1e0c56ac448c312805c25 Mon Sep 17 00:00:00 2001 From: forkh <dev@forkh.dev> Date: Sun, 15 Oct 2023 16:57:56 -0400 Subject: [PATCH 722/858] Added translation using Weblate (Faroese) --- Emby.Server.Implementations/Localization/Core/fo.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 Emby.Server.Implementations/Localization/Core/fo.json diff --git a/Emby.Server.Implementations/Localization/Core/fo.json b/Emby.Server.Implementations/Localization/Core/fo.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/fo.json @@ -0,0 +1 @@ +{} From 298a7488a3a45e51e0defdf0196070ad1bff85cc Mon Sep 17 00:00:00 2001 From: LJQ <leejunquan7@gmail.com> Date: Mon, 16 Oct 2023 19:48:01 +0800 Subject: [PATCH 723/858] Applied 2nd Round of Suggested Changes --- .../Plugins/Tmdb/TV/TmdbEpisodeProvider.cs | 48 ++++++++++--------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs index 69ffd7859c..90abaa6956 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs @@ -104,47 +104,49 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV return metadataResult; } - var episodeResult = new TvEpisode(); - if (!info.IndexNumberEnd.HasValue) - { - episodeResult = await _tmdbClientManager - .GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken) - .ConfigureAwait(false); - } - else + TvEpisode? episodeResult = null; + if (info.IndexNumberEnd.HasValue) { var startindex = episodeNumber; var endindex = info.IndexNumberEnd; - List<TvEpisode> result = new List<TvEpisode>(); + List<TvEpisode>? result = null; for (int? episode = startindex; episode <= endindex; episode++) { var episodeInfo = await _tmdbClientManager.GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episode.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken).ConfigureAwait(false); if (episodeInfo is not null) { - result.Add(episodeInfo); + (result ??= new List<TvEpisode>()).Add(episodeInfo); } } - if (result.Count > 0) + if (result is not null) { episodeResult = result[0]; + if (result.Count > 1) + { + var name = new StringBuilder(episodeResult.Name); + var overview = new StringBuilder(episodeResult.Overview); + + for (int i = 1; i < result.Count; i++) + { + name.Append(" / ").Append(result[i].Name); + overview.Append(" / ").Append(result[i].Overview); + } + + episodeResult.Name = name.ToString(); + episodeResult.Overview = overview.ToString(); + } } else { return metadataResult; } - - var name = new StringBuilder(episodeResult.Name); - var overview = new StringBuilder(episodeResult.Overview); - - for (int i = 1; i < result.Count; i++) - { - name.Append(" / ").Append(result[i].Name); - overview.Append(" / ").Append(result[i].Overview); - } - - episodeResult.Name = name.ToString(); - episodeResult.Overview = overview.ToString(); + } + else + { + episodeResult = await _tmdbClientManager + .GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken) + .ConfigureAwait(false); } if (episodeResult is null) From 1d19fe50b4889406f8cf5517392668f09b3e1f18 Mon Sep 17 00:00:00 2001 From: LJQ <leejunquan7@gmail.com> Date: Mon, 16 Oct 2023 21:18:25 +0800 Subject: [PATCH 724/858] Deep copy instead of Shallow copy --- .../Plugins/Tmdb/TV/TmdbEpisodeProvider.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs index 90abaa6956..489f5e2a17 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs @@ -121,7 +121,18 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV if (result is not null) { - episodeResult = result[0]; + // Forces a deep copy of the first TvEpisode, so we don't modify the original because it's cached + episodeResult = new TvEpisode() + { + Name = result[0].Name, + Overview = result[0].Overview, + AirDate = result[0].AirDate, + VoteAverage = result[0].VoteAverage, + ExternalIds = result[0].ExternalIds, + Videos = result[0].Videos, + Credits = result[0].Credits + }; + if (result.Count > 1) { var name = new StringBuilder(episodeResult.Name); From 8928e0d2af452424d5ec6b1587d7227cd80a4ba9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96ssur=20Ingi=20J=C3=B3nsson?= <pm@ossur.xyz> Date: Mon, 16 Oct 2023 13:33:50 +0000 Subject: [PATCH 725/858] Translated using Weblate (Icelandic) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/is/ --- .../Localization/Core/is.json | 63 ++++++++++--------- 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/is.json b/Emby.Server.Implementations/Localization/Core/is.json index a40f495061..0f1f0b3d24 100644 --- a/Emby.Server.Implementations/Localization/Core/is.json +++ b/Emby.Server.Implementations/Localization/Core/is.json @@ -13,8 +13,8 @@ "HeaderFavoriteArtists": "Uppáhalds Listamenn", "HeaderFavoriteAlbums": "Uppáhalds Plötur", "HeaderContinueWatching": "Halda áfram að horfa", - "HeaderAlbumArtists": "Höfundur plötu", - "Genres": "Tegundir", + "HeaderAlbumArtists": "Listamaður á umslagi", + "Genres": "Stefnur", "Folders": "Möppur", "Favorites": "Uppáhalds", "FailedLoginAttemptWithUserName": "{0} reyndi að auðkenna sig", @@ -22,32 +22,32 @@ "DeviceOfflineWithName": "{0} hefur aftengst", "Collections": "Söfn", "ChapterNameValue": "Kafli {0}", - "Channels": "Stöðvar", - "CameraImageUploadedFrom": "Ný ljósmynd frá myndavél hefur verið hlaðið upp frá {0}", + "Channels": "Rásir", + "CameraImageUploadedFrom": "{0} hefur hlaðið upp nýrri ljósmynd úr myndavél sinni", "Books": "Bækur", - "AuthenticationSucceededWithUserName": "{0} auðkenning tókst", - "Artists": "Listamaður", + "AuthenticationSucceededWithUserName": "Auðkenning fyrir {0} tókst", + "Artists": "Listamenn", "Application": "Forrit", "AppDeviceValues": "Snjallforrit: {0}, Tæki: {1}", "Albums": "Plötur", - "Plugin": "Viðbót", - "Photos": "Myndir", - "NotificationOptionVideoPlaybackStopped": "Myndbandafspilun stöðvuð", - "NotificationOptionVideoPlayback": "Myndbandafspilun hafin", + "Plugin": "Viðbótarvirkni", + "Photos": "Ljósmyndir", + "NotificationOptionVideoPlaybackStopped": "Myndbandsafspilun stöðvuð", + "NotificationOptionVideoPlayback": "Myndbandsafspilun hafin", "NotificationOptionUserLockedOut": "Notandi læstur úti", - "NotificationOptionServerRestartRequired": "Endurræsing þjóns er nauðsynileg", - "NotificationOptionPluginUpdateInstalled": "Viðbótar uppfærsla uppsett", - "NotificationOptionPluginUninstalled": "Viðbót fjarlægð", - "NotificationOptionPluginInstalled": "Viðbót sett upp", + "NotificationOptionServerRestartRequired": "Endurræsing þjóns er nauðsynleg", + "NotificationOptionPluginUpdateInstalled": "Uppfærslu á viðbótarvirkni lokið", + "NotificationOptionPluginUninstalled": "Viðbótarvirkni fjarlægð", + "NotificationOptionPluginInstalled": "Viðbótarvirkni sett upp", "NotificationOptionPluginError": "Bilun í viðbót", "NotificationOptionInstallationFailed": "Uppsetning tókst ekki", - "NotificationOptionCameraImageUploaded": "Myndavélarmynd hlaðið upp", + "NotificationOptionCameraImageUploaded": "Ljósmynd hlaðið upp", "NotificationOptionAudioPlaybackStopped": "Hljóðafspilun stöðvuð", "NotificationOptionAudioPlayback": "Hljóðafspilun hafin", "NotificationOptionApplicationUpdateInstalled": "Uppfærsla uppsett", "NotificationOptionApplicationUpdateAvailable": "Uppfærsla í boði", - "NameSeasonUnknown": "Sería óþekkt", - "NameSeasonNumber": "Sería {0}", + "NameSeasonUnknown": "Þáttaröð óþekkt", + "NameSeasonNumber": "Þáttaröð {0}", "MixedContent": "Blandað efni", "MessageServerConfigurationUpdated": "Stillingar þjóns hafa verið uppfærðar", "MessageApplicationUpdatedTo": "Jellyfin þjónn hefur verið uppfærður í {0}", @@ -57,24 +57,24 @@ "User": "Notandi", "System": "Kerfi", "NotificationOptionNewLibraryContent": "Nýju efni bætt við", - "NewVersionIsAvailable": "Ný útgáfa af Jellyfin þjón er fáanleg til niðurhals.", + "NewVersionIsAvailable": "Ný útgáfa af Jellyfin þjón er tilbúin til niðurhals.", "NameInstallFailed": "{0} uppsetning mistókst", "MusicVideos": "Tónlistarmyndbönd", "Music": "Tónlist", "Movies": "Kvikmyndir", "UserDeletedWithName": "Notanda {0} hefur verið eytt", "UserCreatedWithName": "Notandi {0} hefur verið stofnaður", - "TvShows": "Þættir", + "TvShows": "Sjónvarpsþættir", "Sync": "Samstilla", "Songs": "Lög", - "ServerNameNeedsToBeRestarted": "{0} þarf að endurræsa", + "ServerNameNeedsToBeRestarted": "{0} þarf að vera endurræstur", "ScheduledTaskStartedWithName": "{0} hafin", "ScheduledTaskFailedWithName": "{0} mistókst", "PluginUpdatedWithName": "{0} var uppfært", "PluginUninstalledWithName": "{0} var fjarlægt", "PluginInstalledWithName": "{0} var sett upp", "NotificationOptionTaskFailed": "Tímasett verkefni mistókst", - "StartupEmbyServerIsLoading": "Jellyfin netþjónnin er að hlaðast. Vinsamlega prufaðu aftur fljótlega.", + "StartupEmbyServerIsLoading": "Jellyfin netþjónnin er að ræsa sig upp. Vinsamlegast reyndu aftur fljótlega.", "VersionNumber": "Útgáfa {0}", "ValueHasBeenAddedToLibrary": "{0} hefur verið bætt við í gagnasafnið þitt", "UserStoppedPlayingItemWithValues": "{0} hefur lokið spilunar af {1} á {2}", @@ -83,14 +83,14 @@ "UserPasswordChangedWithName": "Lykilorði fyrir notandann {0} hefur verið breytt", "UserOnlineFromDevice": "{0} hefur verið virkur síðan {1}", "UserOfflineFromDevice": "{0} hefur aftengst frá {1}", - "UserLockedOutWithName": "Notanda {0} hefur verið heflaður aðgangur", - "UserDownloadingItemWithValues": "{0} Hleður niður {1}", + "UserLockedOutWithName": "Notandi {0} hefur verið læstur úti", + "UserDownloadingItemWithValues": "{0} hleður niður {1}", "SubtitleDownloadFailureFromForItem": "Tókst ekki að hala niður skjátextum frá {0} til {1}", - "ProviderValue": "Veitandi: {0}", + "ProviderValue": "Efnisveita: {0}", "MessageNamedServerConfigurationUpdatedWithValue": "Stilling {0} hefur verið uppfærð á netþjón", - "ValueSpecialEpisodeName": "Sérstakt - {0}", - "Shows": "Sýningar", - "Playlists": "Spilunarlisti", + "ValueSpecialEpisodeName": "Sérstaktur - {0}", + "Shows": "Þættir", + "Playlists": "Efnisskrár", "TaskRefreshChannelsDescription": "Endurhlaða upplýsingum netrása.", "TaskRefreshChannels": "Endurhlaða Rásir", "TaskCleanTranscodeDescription": "Eyða umkóðuðum skrám sem eru meira en einum degi eldri.", @@ -116,5 +116,12 @@ "TaskCleanLogsDescription": "Eyðir færslu skrám sem eru meira en {0} gömul.", "TaskCleanLogs": "Hreinsa færslu skrá", "TaskDownloadMissingSubtitlesDescription": "Leitar á netinu að texta sem vantar miðað við uppsetningu lýsigagna.", - "HearingImpaired": "Heyrnarskertur" + "HearingImpaired": "Heyrnarskertur", + "TaskOptimizeDatabaseDescription": "Þjappar gagnagrunni og bætir við lausu diskaplássi. Að keyra þessa aðgerð eftir skönnun safnsins, eða eftir einhverjar breytingar sem fela í sér gagnagrunnsbreytingar, gætu aukið hraðvirkni.", + "TaskKeyframeExtractor": "Lykilrammaplokkari", + "TaskKeyframeExtractorDescription": "Plokkar lykilramma úr myndbandsskrám til að búa til nákvæmari HLS uppskiptingarlista. Þetta verk getur tekið langan tíma.", + "TaskRefreshChapterImages": "Plokka kafla-myndir", + "TaskCleanActivityLogDescription": "Eyðir virkniskráningarfærslum sem hafa náð settum hámarksaldri.", + "Forced": "Þvingað", + "External": "Útvær" } From 3704382545f455a5b38a498c22c9927b3403336d Mon Sep 17 00:00:00 2001 From: Andrejs <tlpbu@droplar.com> Date: Mon, 16 Oct 2023 20:14:32 +0000 Subject: [PATCH 726/858] Translated using Weblate (Latvian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/lv/ --- Emby.Server.Implementations/Localization/Core/lv.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/lv.json b/Emby.Server.Implementations/Localization/Core/lv.json index f7b24412af..d558cdfe00 100644 --- a/Emby.Server.Implementations/Localization/Core/lv.json +++ b/Emby.Server.Implementations/Localization/Core/lv.json @@ -75,7 +75,7 @@ "HeaderAlbumArtists": "Albumu Izpildītāji", "Genres": "Žanri", "Folders": "Mapes", - "Favorites": "Favorīti", + "Favorites": "Izlase", "FailedLoginAttemptWithUserName": "Neizdevies pieslēgšanās mēģinājums no {0}", "DeviceOnlineWithName": "{0} ir pievienojies", "DeviceOfflineWithName": "{0} ir atvienojies", @@ -113,7 +113,7 @@ "TaskCleanCache": "Iztīrīt Kešošanas Mapi", "TasksChannelsCategory": "Interneta Kanāli", "TasksMaintenanceCategory": "Apkope", - "Forced": "Piespiests", + "Forced": "Piespiedu", "TaskCleanActivityLogDescription": "Nodzēš darbību žurnāla ierakstus, kuri ir vecāki par doto vecumu.", "TaskCleanActivityLog": "Notīrīt Darbību Žurnālu", "Undefined": "Nenoteikts", From c727d2cebcedc00143f45e2836fa92e281d80c92 Mon Sep 17 00:00:00 2001 From: forkh <dev@forkh.dev> Date: Sun, 15 Oct 2023 21:00:29 +0000 Subject: [PATCH 727/858] Translated using Weblate (Faroese) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fo/ --- .../Localization/Core/fo.json | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/fo.json b/Emby.Server.Implementations/Localization/Core/fo.json index 0967ef424b..40aa5f71a4 100644 --- a/Emby.Server.Implementations/Localization/Core/fo.json +++ b/Emby.Server.Implementations/Localization/Core/fo.json @@ -1 +1,18 @@ -{} +{ + "Artists": "Listafólk", + "Collections": "Søvn", + "Default": "Sjálvgildi", + "DeviceOfflineWithName": "{0} hevur slitið sambandið", + "External": "Ytri", + "Genres": "Greinar", + "Albums": "Album", + "AppDeviceValues": "App: {0}, Eind: {1}", + "Application": "Nýtsluskipan", + "Books": "Bøkur", + "Channels": "Rásir", + "ChapterNameValue": "Kapittul {0}", + "DeviceOnlineWithName": "{0} er sambundið", + "Favorites": "Yndis", + "Folders": "Mappur", + "Forced": "Kravt" +} From 6512d7186d010ad1c532c98a3238581e89d4e416 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 17 Oct 2023 00:48:55 +0000 Subject: [PATCH 728/858] Update dependency xunit to v2.5.3 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index e26005f8e2..29bf423287 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -88,6 +88,6 @@ <PackageVersion Include="Xunit.Priority" Version="1.1.6" /> <PackageVersion Include="xunit.runner.visualstudio" Version="2.5.3" /> <PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" /> - <PackageVersion Include="xunit" Version="2.5.2" /> + <PackageVersion Include="xunit" Version="2.5.3" /> </ItemGroup> </Project> From 12a4f5d8b1ee8018605f83690c089fc347154ed7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 17 Oct 2023 10:59:22 +0000 Subject: [PATCH 729/858] Update dependency EFCoreSecondLevelCacheInterceptor to v3.9.3 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 29bf423287..32e2d65ce9 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -17,7 +17,7 @@ <PackageVersion Include="Diacritics" Version="3.3.18" /> <PackageVersion Include="DiscUtils.Udf" Version="0.16.13" /> <PackageVersion Include="DotNet.Glob" Version="3.1.3" /> - <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.9.2" /> + <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.9.3" /> <PackageVersion Include="FsCheck.Xunit" Version="2.16.6" /> <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0" /> <PackageVersion Include="IDisposableAnalyzers" Version="4.0.7" /> From f60aefcbe8b5c5c4e9079d429bd3297ba4b4164c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 17 Oct 2023 12:29:50 +0000 Subject: [PATCH 730/858] Update dependency dotnet-ef to v7.0.12 --- .config/dotnet-tools.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 96be437d5d..2f789b031a 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "7.0.11", + "version": "7.0.12", "commands": [ "dotnet-ef" ] From add89a48217f97cc95ac7546b7401e7c5fd57196 Mon Sep 17 00:00:00 2001 From: Andrejs <tlpbu@droplar.com> Date: Mon, 16 Oct 2023 20:30:30 +0000 Subject: [PATCH 731/858] Translated using Weblate (Latvian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/lv/ --- .../Localization/Core/lv.json | 58 +++++++++---------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/lv.json b/Emby.Server.Implementations/Localization/Core/lv.json index d558cdfe00..a5980b6f6e 100644 --- a/Emby.Server.Implementations/Localization/Core/lv.json +++ b/Emby.Server.Implementations/Localization/Core/lv.json @@ -1,7 +1,7 @@ { "ServerNameNeedsToBeRestarted": "{0} ir vajadzīgs restarts", "NotificationOptionTaskFailed": "Plānota uzdevuma kļūme", - "HeaderRecordingGroups": "Ierakstu Grupas", + "HeaderRecordingGroups": "Ierakstu grupas", "UserPolicyUpdatedWithName": "Lietotāju politika atjaunota priekš {0}", "SubtitleDownloadFailureFromForItem": "Subtitru lejupielāde no {0} priekš {1} neizdevās", "NotificationOptionVideoPlaybackStopped": "Video atskaņošana apturēta", @@ -14,7 +14,7 @@ "Photos": "Attēli", "NotificationOptionUserLockedOut": "Lietotājs bloķēts", "LabelRunningTimeValue": "Garums: {0}", - "Inherit": "Mantot", + "Inherit": "Pārmantot", "AppDeviceValues": "Lietotne: {0}, Ierīce: {1}", "VersionNumber": "Versija {0}", "ValueHasBeenAddedToLibrary": "{0} ir ticis pievienots jūsu multvides bibliotēkai", @@ -28,7 +28,7 @@ "UserDeletedWithName": "Lietotājs {0} ir izdzēsts", "UserCreatedWithName": "Lietotājs {0} ir ticis izveidots", "User": "Lietotājs", - "TvShows": "TV Raidījumi", + "TvShows": "TV raidījumi", "Sync": "Sinhronizācija", "System": "Sistēma", "StartupEmbyServerIsLoading": "Jellyfin Serveris lādējas. Lūdzu mēģiniet vēlreiz pēc brīža.", @@ -38,9 +38,9 @@ "PluginUninstalledWithName": "{0} tika noņemts", "PluginInstalledWithName": "{0} tika uzstādīts", "Plugin": "Paplašinājums", - "Playlists": "Atskaņošanas Saraksti", + "Playlists": "Atskaņošanas saraksti", "MixedContent": "Jaukts saturs", - "HomeVideos": "Mājas Video", + "HomeVideos": "Mājas video", "HeaderNextUp": "Nākamais", "ChapterNameValue": "Nodaļa {0}", "Application": "Lietotne", @@ -56,7 +56,7 @@ "NotificationOptionApplicationUpdateInstalled": "Lietotnes atjauninājums uzstādīts", "NotificationOptionApplicationUpdateAvailable": "Lietotnes atjauninājums pieejams", "NewVersionIsAvailable": "Lejupielādei ir pieejama jauna Jellyfin Server versija.", - "NameSeasonUnknown": "Nezināma Sezona", + "NameSeasonUnknown": "Nezināma sezona", "NameSeasonNumber": "Sezona {0}", "NameInstallFailed": "{0} instalācija neizdevās", "MusicVideos": "Mūzikas video", @@ -71,8 +71,8 @@ "ItemRemovedWithName": "{0} tika noņemts no bibliotēkas", "ItemAddedWithName": "{0} tika pievienots bibliotēkai", "HeaderLiveTV": "Tiešraides TV", - "HeaderContinueWatching": "Turpināt Skatīšanos", - "HeaderAlbumArtists": "Albumu Izpildītāji", + "HeaderContinueWatching": "Turpināt skatīšanos", + "HeaderAlbumArtists": "Albumu izpildītāji", "Genres": "Žanri", "Folders": "Mapes", "Favorites": "Izlase", @@ -86,42 +86,42 @@ "Artists": "Izpildītāji", "Albums": "Albumi", "ProviderValue": "Provider: {0}", - "HeaderFavoriteSongs": "Dziesmu Favorīti", - "HeaderFavoriteShows": "Raidījumu Favorīti", - "HeaderFavoriteEpisodes": "Episožu Favorīti", - "HeaderFavoriteArtists": "Izpildītāju Favorīti", - "HeaderFavoriteAlbums": "Albumu Favorīti", - "TaskCleanCacheDescription": "Nodzēš keša datnes, kas vairs nav sistēmai vajadzīgas.", - "TaskRefreshChapterImages": "Izvilkt Nodaļu Attēlus", + "HeaderFavoriteSongs": "Dziesmu izlase", + "HeaderFavoriteShows": "Raidījumu izlase", + "HeaderFavoriteEpisodes": "Sēriju izlase", + "HeaderFavoriteArtists": "Izpildītāju izlase", + "HeaderFavoriteAlbums": "Albumu izlase", + "TaskCleanCacheDescription": "Nodzēš kešatmiņas datnes, kas vairs nav sistēmai vajadzīgas.", + "TaskRefreshChapterImages": "Izvilkt nodaļu attēlus", "TasksApplicationCategory": "Lietotne", "TasksLibraryCategory": "Bibliotēka", "TaskDownloadMissingSubtitlesDescription": "Internetā meklē trūkstošus subtitrus balstoties uz metadatu uzstādījumiem.", - "TaskDownloadMissingSubtitles": "Lejupielādēt trūkstošus subtitrus", + "TaskDownloadMissingSubtitles": "Lejupielādēt trūkstošos subtitrus", "TaskRefreshChannelsDescription": "Atjauno interneta kanālu informāciju.", - "TaskRefreshChannels": "Atjaunot Kanālus", - "TaskCleanTranscodeDescription": "Izdzēš trans-kodēšanas datnes, kas ir vecākas par vienu dienu.", - "TaskCleanTranscode": "Iztīrīt Trans-kodēšanas Mapi", + "TaskRefreshChannels": "Atjaunot kanālus", + "TaskCleanTranscodeDescription": "Izdzēš transkodēšanas datnes, kas ir senākas par vienu dienu.", + "TaskCleanTranscode": "Iztīrīt transkodēšanas mapi", "TaskUpdatePluginsDescription": "Lejupielādē un uzstāda atjauninājumus paplašinājumiem, kam ir uzstādīta automātiskā atjaunināšana.", - "TaskUpdatePlugins": "Atjaunot Paplašinājumus", + "TaskUpdatePlugins": "Atjaunot paplašinājumus", "TaskRefreshPeopleDescription": "Atjauno metadatus aktieriem un direktoriem jūsu multivides bibliotēkā.", - "TaskRefreshPeople": "Atjaunot Cilvēkus", - "TaskCleanLogsDescription": "Nodzēš log datnes, kas ir vairāk par {0} dienām vecas.", - "TaskCleanLogs": "Iztīrīt Logdatņu Mapi", + "TaskRefreshPeople": "Atjaunot cilvēkus", + "TaskCleanLogsDescription": "Nodzēš logdatnes, kas ir senākas par {0} dienām.", + "TaskCleanLogs": "Iztīrīt logdatņu mapi", "TaskRefreshLibraryDescription": "Skenē jūsu multivides bibliotēku, lai atrastu jaunas datnes, un atsvaidzina metadatus.", - "TaskRefreshLibrary": "Skenēt Multivides Bibliotēku", + "TaskRefreshLibrary": "Skenēt multivides bibliotēku", "TaskRefreshChapterImagesDescription": "Izveido sīktēlus priekš video ar sadaļām.", - "TaskCleanCache": "Iztīrīt Kešošanas Mapi", - "TasksChannelsCategory": "Interneta Kanāli", + "TaskCleanCache": "Iztīrīt kešatmiņas mapi", + "TasksChannelsCategory": "Interneta kanāli", "TasksMaintenanceCategory": "Apkope", "Forced": "Piespiedu", "TaskCleanActivityLogDescription": "Nodzēš darbību žurnāla ierakstus, kuri ir vecāki par doto vecumu.", - "TaskCleanActivityLog": "Notīrīt Darbību Žurnālu", + "TaskCleanActivityLog": "Notīrīt darbību žurnālu", "Undefined": "Nenoteikts", "Default": "Noklusējuma", - "TaskOptimizeDatabaseDescription": "Saspiež datubāzi un atbrīvo atmiņu. Uzdevum palaišana pēc bibliotēku skenēšanas vai citām, ar datubāzi saistītām, izmaiņām iespējams uzlabos ātrdarbību.", + "TaskOptimizeDatabaseDescription": "Saspiež datubāzi un atbrīvo atmiņu. Šī uzdevuma palaišana pēc bibliotēku skenēšanas vai citām, ar datubāzi saistītām, izmaiņām iespējams uzlabos ātrdarbību.", "TaskOptimizeDatabase": "Optimizēt datubāzi", "External": "Ārējais", "HearingImpaired": "Ar dzirdes traucējumiem", - "TaskKeyframeExtractor": "Atslēgkadru Ekstraktors", + "TaskKeyframeExtractor": "Atslēgkadru ekstraktors", "TaskKeyframeExtractorDescription": "Ekstraktē atslēgkadrus no video failiem lai izveidotu precīzākus HLS atskaņošanas sarakstus. Šis process var būt ilgs." } From 089876170a36346955fb82f849fa8713844e4be3 Mon Sep 17 00:00:00 2001 From: kasundigital <kasunindika@gmail.com> Date: Tue, 17 Oct 2023 17:19:12 -0400 Subject: [PATCH 732/858] Added translation using Weblate (Sinhala) --- Emby.Server.Implementations/Localization/Core/si.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 Emby.Server.Implementations/Localization/Core/si.json diff --git a/Emby.Server.Implementations/Localization/Core/si.json b/Emby.Server.Implementations/Localization/Core/si.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/si.json @@ -0,0 +1 @@ +{} From cff30c0e6b7d647070423f42ab0d80bfd073256f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 17 Oct 2023 22:56:53 +0000 Subject: [PATCH 733/858] Update dependency EFCoreSecondLevelCacheInterceptor to v3.9.4 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 32e2d65ce9..d95cecdbf8 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -17,7 +17,7 @@ <PackageVersion Include="Diacritics" Version="3.3.18" /> <PackageVersion Include="DiscUtils.Udf" Version="0.16.13" /> <PackageVersion Include="DotNet.Glob" Version="3.1.3" /> - <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.9.3" /> + <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.9.4" /> <PackageVersion Include="FsCheck.Xunit" Version="2.16.6" /> <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0" /> <PackageVersion Include="IDisposableAnalyzers" Version="4.0.7" /> From 1c4b0be85dbe4aac026060ea585cbbf9a6a33a69 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 17 Oct 2023 18:15:17 -0600 Subject: [PATCH 734/858] Update actions/checkout action to v4.1.1 (#10427) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/commands.yml | 4 ++-- .github/workflows/openapi.yml | 4 ++-- .github/workflows/repo-bump-version.yaml | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 8385e84bf8..a5f36eab42 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Setup .NET uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0 with: diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index ba7883a734..8055438b5a 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -24,7 +24,7 @@ jobs: reactions: '+1' - name: Checkout the latest code - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: token: ${{ secrets.JF_BOT_TOKEN }} fetch-depth: 0 @@ -51,7 +51,7 @@ jobs: reactions: eyes - name: Checkout the latest code - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: token: ${{ secrets.JF_BOT_TOKEN }} fetch-depth: 0 diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index 693f98d160..c267fdcc27 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -14,7 +14,7 @@ jobs: permissions: read-all steps: - name: Checkout repository - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} @@ -39,7 +39,7 @@ jobs: permissions: read-all steps: - name: Checkout repository - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} diff --git a/.github/workflows/repo-bump-version.yaml b/.github/workflows/repo-bump-version.yaml index 0ba68dda36..e0383afd23 100644 --- a/.github/workflows/repo-bump-version.yaml +++ b/.github/workflows/repo-bump-version.yaml @@ -33,7 +33,7 @@ jobs: yq-version: v4.9.8 - name: Checkout Repository - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: ref: ${{ env.TAG_BRANCH }} @@ -66,7 +66,7 @@ jobs: NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }} steps: - name: Checkout Repository - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: ref: ${{ env.TAG_BRANCH }} From 9f259aa404a4bf4185f22ece9a13be2a7ae346df Mon Sep 17 00:00:00 2001 From: chinkara <poubelledechinkara@outlook.com> Date: Sun, 15 Oct 2023 15:53:53 +0200 Subject: [PATCH 735/858] add EnableSubtitleManagement permission --- Jellyfin.Data/Entities/User.cs | 1 + Jellyfin.Data/Enums/PermissionKind.cs | 7 ++++++- Jellyfin.Server.Implementations/Users/UserManager.cs | 2 ++ MediaBrowser.Model/Users/UserPolicy.cs | 8 ++++++++ 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs index 58ddaaf83a..8e66a32875 100644 --- a/Jellyfin.Data/Entities/User.cs +++ b/Jellyfin.Data/Entities/User.cs @@ -499,6 +499,7 @@ namespace Jellyfin.Data.Entities Permissions.Add(new Permission(PermissionKind.ForceRemoteSourceTranscoding, false)); Permissions.Add(new Permission(PermissionKind.EnableRemoteControlOfOtherUsers, false)); Permissions.Add(new Permission(PermissionKind.EnableCollectionManagement, false)); + Permissions.Add(new Permission(PermissionKind.EnableSubtitleManagement, false)); } /// <summary> diff --git a/Jellyfin.Data/Enums/PermissionKind.cs b/Jellyfin.Data/Enums/PermissionKind.cs index 40280b95ef..6644f01515 100644 --- a/Jellyfin.Data/Enums/PermissionKind.cs +++ b/Jellyfin.Data/Enums/PermissionKind.cs @@ -113,6 +113,11 @@ namespace Jellyfin.Data.Enums /// <summary> /// Whether the user can create, modify and delete collections. /// </summary> - EnableCollectionManagement = 21 + EnableCollectionManagement = 21, + + /// <summary> + /// Whether the user can edit subtitles. + /// </summary> + EnableSubtitleManagement = 22 } } diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 94ac4798ca..59d655c829 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -349,6 +349,7 @@ namespace Jellyfin.Server.Implementations.Users ForceRemoteSourceTranscoding = user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding), EnablePublicSharing = user.HasPermission(PermissionKind.EnablePublicSharing), EnableCollectionManagement = user.HasPermission(PermissionKind.EnableCollectionManagement), + EnableSubtitleManagement = user.HasPermission(PermissionKind.EnableSubtitleManagement), AccessSchedules = user.AccessSchedules.ToArray(), BlockedTags = user.GetPreference(PreferenceKind.BlockedTags), AllowedTags = user.GetPreference(PreferenceKind.AllowedTags), @@ -666,6 +667,7 @@ namespace Jellyfin.Server.Implementations.Users user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOtherUsers); user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing); user.SetPermission(PermissionKind.EnableCollectionManagement, policy.EnableCollectionManagement); + user.SetPermission(PermissionKind.EnableSubtitleManagement, policy.EnableSubtitleManagement); user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding); user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing); diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs index 8354c60efb..f5aff07db4 100644 --- a/MediaBrowser.Model/Users/UserPolicy.cs +++ b/MediaBrowser.Model/Users/UserPolicy.cs @@ -15,6 +15,7 @@ namespace MediaBrowser.Model.Users { IsHidden = true; EnableCollectionManagement = false; + EnableSubtitleManagement = false; EnableContentDeletion = false; EnableContentDeletionFromFolders = Array.Empty<string>(); @@ -83,6 +84,13 @@ namespace MediaBrowser.Model.Users [DefaultValue(false)] public bool EnableCollectionManagement { get; set; } + /// <summary> + /// Gets or sets a value indicating whether this instance can manage subtitles. + /// </summary> + /// <value><c>true</c> if this instance is allowed; otherwise, <c>false</c>.</value> + [DefaultValue(false)] + public bool EnableSubtitleManagement { get; set; } + /// <summary> /// Gets or sets a value indicating whether this instance is disabled. /// </summary> From 8ada8dbbac6fc4f55ad5834f301f5c42139b4171 Mon Sep 17 00:00:00 2001 From: chinkara <poubelledechinkara@outlook.com> Date: Mon, 16 Oct 2023 23:57:08 +0200 Subject: [PATCH 736/858] add policy to the subtitle controller --- Jellyfin.Api/Constants/Policies.cs | 5 +++++ Jellyfin.Api/Controllers/SubtitleController.cs | 6 +++--- .../Extensions/ApiServiceCollectionExtensions.cs | 1 + 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Api/Constants/Policies.cs b/Jellyfin.Api/Constants/Policies.cs index 53841b0c44..02fdef1507 100644 --- a/Jellyfin.Api/Constants/Policies.cs +++ b/Jellyfin.Api/Constants/Policies.cs @@ -84,4 +84,9 @@ public static class Policies /// Policy name for managing LiveTV. /// </summary> public const string LiveTvManagement = "LiveTvManagement"; + + /// <summary> + /// Policy name for accessing subtitles management. + /// </summary> + public const string SubtitleManagement = "SubtitleManagement"; } diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index fb89e96108..c9e256af38 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -115,7 +115,7 @@ public class SubtitleController : BaseJellyfinApiController /// <response code="200">Subtitles retrieved.</response> /// <returns>An array of <see cref="RemoteSubtitleInfo"/>.</returns> [HttpGet("Items/{itemId}/RemoteSearch/Subtitles/{language}")] - [Authorize] + [Authorize(Policy = Policies.SubtitleManagement)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task<ActionResult<IEnumerable<RemoteSubtitleInfo>>> SearchRemoteSubtitles( [FromRoute, Required] Guid itemId, @@ -135,7 +135,7 @@ public class SubtitleController : BaseJellyfinApiController /// <response code="204">Subtitle downloaded.</response> /// <returns>A <see cref="NoContentResult"/>.</returns> [HttpPost("Items/{itemId}/RemoteSearch/Subtitles/{subtitleId}")] - [Authorize] + [Authorize(Policy = Policies.SubtitleManagement)] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task<ActionResult> DownloadRemoteSubtitles( [FromRoute, Required] Guid itemId, @@ -399,7 +399,7 @@ public class SubtitleController : BaseJellyfinApiController /// <response code="204">Subtitle uploaded.</response> /// <returns>A <see cref="NoContentResult"/>.</returns> [HttpPost("Videos/{itemId}/Subtitles")] - [Authorize(Policy = Policies.RequiresElevation)] + [Authorize(Policy = Policies.SubtitleManagement)] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task<ActionResult> UploadSubtitle( [FromRoute, Required] Guid itemId, diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index cb1680558f..b7e71a81db 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -82,6 +82,7 @@ namespace Jellyfin.Server.Extensions options.AddPolicy(Policies.SyncPlayCreateGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.CreateGroup)); options.AddPolicy(Policies.SyncPlayJoinGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.JoinGroup)); options.AddPolicy(Policies.SyncPlayIsInGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.IsInGroup)); + options.AddPolicy(Policies.SubtitleManagement, new UserPermissionRequirement(PermissionKind.EnableSubtitleManagement)); options.AddPolicy( Policies.RequiresElevation, policy => policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication) From de08d38a6f2a6e773fa1000574e08322605b56d3 Mon Sep 17 00:00:00 2001 From: Andrejs <tlpbu@droplar.com> Date: Tue, 17 Oct 2023 21:57:52 +0000 Subject: [PATCH 737/858] Translated using Weblate (Latvian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/lv/ --- .../Localization/Core/lv.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/lv.json b/Emby.Server.Implementations/Localization/Core/lv.json index a5980b6f6e..83a000014d 100644 --- a/Emby.Server.Implementations/Localization/Core/lv.json +++ b/Emby.Server.Implementations/Localization/Core/lv.json @@ -42,7 +42,7 @@ "MixedContent": "Jaukts saturs", "HomeVideos": "Mājas video", "HeaderNextUp": "Nākamais", - "ChapterNameValue": "Nodaļa {0}", + "ChapterNameValue": "{0}. nodaļa", "Application": "Lietotne", "NotificationOptionServerRestartRequired": "Vajadzīgs servera restarts", "NotificationOptionPluginUpdateInstalled": "Paplašinājuma atjauninājums uzstādīts", @@ -57,13 +57,13 @@ "NotificationOptionApplicationUpdateAvailable": "Lietotnes atjauninājums pieejams", "NewVersionIsAvailable": "Lejupielādei ir pieejama jauna Jellyfin Server versija.", "NameSeasonUnknown": "Nezināma sezona", - "NameSeasonNumber": "Sezona {0}", + "NameSeasonNumber": "{0}. sezona", "NameInstallFailed": "{0} instalācija neizdevās", "MusicVideos": "Mūzikas video", "Music": "Mūzika", "Movies": "Filmas", "MessageServerConfigurationUpdated": "Servera konfigurācija ir tikusi atjaunota", - "MessageNamedServerConfigurationUpdatedWithValue": "Servera konfigurācijas sadaļa {0} ir tikusi atjaunota", + "MessageNamedServerConfigurationUpdatedWithValue": "Servera konfigurācijas sadaļa {0} tika atjaunota", "MessageApplicationUpdatedTo": "Jellyfin Server ir ticis atjaunots uz {0}", "MessageApplicationUpdated": "Jellyfin Server ir ticis atjaunots", "Latest": "Jaunākais", @@ -76,12 +76,12 @@ "Genres": "Žanri", "Folders": "Mapes", "Favorites": "Izlase", - "FailedLoginAttemptWithUserName": "Neizdevies pieslēgšanās mēģinājums no {0}", - "DeviceOnlineWithName": "{0} ir pievienojies", - "DeviceOfflineWithName": "{0} ir atvienojies", + "FailedLoginAttemptWithUserName": "Neizdevies ieiešanas mēģinājums no {0}", + "DeviceOnlineWithName": "Savienojums ar {0} ir izveidots", + "DeviceOfflineWithName": "Savienojums ar {0} ir pārtraukts", "Collections": "Kolekcijas", "Channels": "Kanāli", - "CameraImageUploadedFrom": "Jauns kameras attēls ir ticis augšupielādēts no {0}", + "CameraImageUploadedFrom": "Jauns kameras attēls tika augšupielādēts no {0}", "Books": "Grāmatas", "Artists": "Izpildītāji", "Albums": "Albumi", From 6b94d55e1ef7ca95e0ddc2a88b0ff0fd63d630a9 Mon Sep 17 00:00:00 2001 From: Nick <20588554+nicknsy@users.noreply.github.com> Date: Wed, 18 Oct 2023 20:01:40 -0700 Subject: [PATCH 738/858] Fix for new WaitForExitAsync method --- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 0b603715d9..4668b8bbbc 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -920,14 +920,22 @@ namespace MediaBrowser.MediaEncoding.Encoder bool isResponsive = true; int lastCount = 0; + var timeoutMs = _configurationManager.Configuration.ImageExtractionTimeoutMs; + timeoutMs = timeoutMs <= 0 ? DefaultHdrImageExtractionTimeout : timeoutMs; while (isResponsive) { - if (await process.WaitForExitAsync(TimeSpan.FromSeconds(30)).ConfigureAwait(false)) + try { + await process.WaitForExitAsync(TimeSpan.FromMilliseconds(timeoutMs)).ConfigureAwait(false); + ranToCompletion = true; break; } + catch (OperationCanceledException) + { + // We don't actually expect the process to be finished in one timeout span, just that one image has been generated. + } cancellationToken.ThrowIfCancellationRequested(); @@ -939,7 +947,7 @@ namespace MediaBrowser.MediaEncoding.Encoder if (!ranToCompletion) { - _logger.LogInformation("Killing ffmpeg extraction process due to inactivity."); + _logger.LogInformation("Stopping trickplay extraction due to process inactivity."); StopProcess(processWrapper, 1000); } } From 556228b21928ae6c51640f0f8a7521225bb4aec0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 19 Oct 2023 20:54:31 +0000 Subject: [PATCH 739/858] Update peter-evans/create-or-update-comment action to v3.1.0 --- .github/workflows/commands.yml | 10 +++++----- .github/workflows/openapi.yml | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index 8055438b5a..75b6a73e56 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Notify as seen - uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2 + uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0 with: token: ${{ secrets.JF_BOT_TOKEN }} comment-id: ${{ github.event.comment.id }} @@ -43,7 +43,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Notify as seen - uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2 + uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0 if: ${{ github.event.comment != null }} with: token: ${{ secrets.JF_BOT_TOKEN }} @@ -58,7 +58,7 @@ jobs: - name: Notify as running id: comment_running - uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2 + uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0 if: ${{ github.event.comment != null }} with: token: ${{ secrets.JF_BOT_TOKEN }} @@ -93,7 +93,7 @@ jobs: exit ${retcode} - name: Notify with result success - uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2 + uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0 if: ${{ github.event.comment != null && success() }} with: token: ${{ secrets.JF_BOT_TOKEN }} @@ -108,7 +108,7 @@ jobs: reactions: hooray - name: Notify with result failure - uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2 + uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0 if: ${{ github.event.comment != null && failure() }} with: token: ${{ secrets.JF_BOT_TOKEN }} diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index c267fdcc27..8c463a8fcf 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -112,7 +112,7 @@ jobs: direction: last body-includes: openapi-diff-workflow-comment - name: Reply or edit difference comment (changed) - uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2 + uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0 if: ${{ steps.read-diff.outputs.body != '' }} with: issue-number: ${{ github.event.pull_request.number }} @@ -127,7 +127,7 @@ jobs: </details> - name: Edit difference comment (unchanged) - uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2 + uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0 if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }} with: issue-number: ${{ github.event.pull_request.number }} From 6c5a4a93ccdb7c6ca8b26c36505d0954a7c79300 Mon Sep 17 00:00:00 2001 From: herby2212 <12448284+herby2212@users.noreply.github.com> Date: Sat, 21 Oct 2023 11:49:01 +0200 Subject: [PATCH 740/858] fix indentation after merge --- .../Configuration/ServerConfiguration.cs | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 2fb5fbd0b0..fe92251e9b 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -158,20 +158,20 @@ public class ServerConfiguration : BaseApplicationConfiguration /// <value>The remaining time in minutes.</value> public int MaxAudiobookResume { get; set; } = 5; - /// <summary> - /// Gets or sets the threshold in minutes after a inactive session gets closed automatically. - /// If set to 0 the check for inactive sessions gets disabled. - /// </summary> - /// <value>The close inactive session threshold in minutes. 0 to disable.</value> - public int InactiveSessionThreshold { get; set; } = 10; + /// <summary> + /// Gets or sets the threshold in minutes after a inactive session gets closed automatically. + /// If set to 0 the check for inactive sessions gets disabled. + /// </summary> + /// <value>The close inactive session threshold in minutes. 0 to disable.</value> + public int InactiveSessionThreshold { get; set; } = 10; - /// <summary> - /// Gets or sets the delay in seconds that we will wait after a file system change to try and discover what has been added/removed - /// Some delay is necessary with some items because their creation is not atomic. It involves the creation of several - /// different directories and files. - /// </summary> - /// <value>The file watcher delay.</value> - public int LibraryMonitorDelay { get; set; } = 60; + /// <summary> + /// Gets or sets the delay in seconds that we will wait after a file system change to try and discover what has been added/removed + /// Some delay is necessary with some items because their creation is not atomic. It involves the creation of several + /// different directories and files. + /// </summary> + /// <value>The file watcher delay.</value> + public int LibraryMonitorDelay { get; set; } = 60; /// <summary> /// Gets or sets the duration in seconds that we will wait after a library updated event before executing the library changed notification. From 181601a1e58e5ac302583f4b10ac0e056351401c Mon Sep 17 00:00:00 2001 From: Jacob Slusser <jacobslusser@hotmail.com> Date: Sat, 21 Oct 2023 12:03:21 -0700 Subject: [PATCH 741/858] #10333 Increases operations per run to work around no cache state between runs --- .github/workflows/repo-stale.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/repo-stale.yaml b/.github/workflows/repo-stale.yaml index 2b11641166..e0789ea588 100644 --- a/.github/workflows/repo-stale.yaml +++ b/.github/workflows/repo-stale.yaml @@ -2,7 +2,7 @@ name: Stale Check on: schedule: - - cron: '30 */12 * * *' + - cron: '30 1 * * *' workflow_dispatch: permissions: @@ -19,11 +19,12 @@ jobs: - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0 with: repo-token: ${{ secrets.JF_BOT_TOKEN }} + ascending: true days-before-stale: 120 days-before-pr-stale: -1 days-before-close: 21 days-before-pr-close: -1 - operations-per-run: 75 + operations-per-run: 500 exempt-issue-labels: regression,security,roadmap,future,feature,enhancement,confirmed stale-issue-label: stale stale-issue-message: |- From 8ee9a0adf9f4b1ea72a4bbe322ef7941975a4b70 Mon Sep 17 00:00:00 2001 From: Vincent Lark <v.lark@astonitf.com> Date: Sat, 21 Oct 2023 23:57:05 +0200 Subject: [PATCH 742/858] Forward user_agent config to ffprobe --- .../Encoder/MediaEncoder.cs | 6 +++ .../Probing/ProbeExternalSourcesTests.cs | 47 +++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeExternalSourcesTests.cs diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 4668b8bbbc..e8041f1984 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -418,6 +418,7 @@ namespace MediaBrowser.MediaEncoding.Encoder public Task<MediaInfo> GetMediaInfo(MediaInfoRequest request, CancellationToken cancellationToken) { var extractChapters = request.MediaType == DlnaProfileType.Video && request.ExtractChapters; + var requiredHeaders = request.MediaSource.RequiredHttpHeaders; var analyzeDuration = string.Empty; var ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty; var ffmpegProbeSize = _config.GetFFmpegProbeSize() ?? string.Empty; @@ -442,6 +443,11 @@ namespace MediaBrowser.MediaEncoding.Encoder extraArgs += " -probesize " + ffmpegProbeSize; } + if (requiredHeaders.ContainsKey("user_agent")) + { + extraArgs += " -user_agent " + requiredHeaders["user_agent"]; + } + return GetMediaInfoInternal( GetInputArgument(request.MediaSource.Path, request.MediaSource), request.MediaSource.Path, diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeExternalSourcesTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeExternalSourcesTests.cs new file mode 100644 index 0000000000..17f8ec163b --- /dev/null +++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeExternalSourcesTests.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.Threading; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.MediaEncoding.Encoder; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.MediaInfo; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; + +namespace Jellyfin.MediaEncoding.Tests.Probing +{ + public class ProbeExternalSourcesTests + { + [Fact] + public void GetMediaInfo_Uses_UserAgent() + { + var encoder = new MediaEncoder( + Mock.Of<ILogger<MediaEncoder>>(), + Mock.Of<IServerConfigurationManager>(), + Mock.Of<IFileSystem>(), + Mock.Of<IBlurayExaminer>(), + Mock.Of<ILocalizationManager>(), + new ConfigurationBuilder().Build(), + Mock.Of<IServerConfigurationManager>()); + + var req = new MediaBrowser.Controller.MediaEncoding.MediaInfoRequest() + { + MediaSource = new MediaBrowser.Model.Dto.MediaSourceInfo + { + Path = "/path/to/stream", + Protocol = MediaProtocol.Http, + RequiredHttpHeaders = new Dictionary<string, string>() + { + { "user_agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/530.35 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/530.35" }, + } + }, + ExtractChapters = false, + MediaType = MediaBrowser.Model.Dlna.DlnaProfileType.Video, + }; + + encoder.GetMediaInfo(req, CancellationToken.None); + } + } +} From da5184d16977e427ac1ed7f5208b6b9b5fba5dd9 Mon Sep 17 00:00:00 2001 From: INOUE Daisuke <inoue.daisuke@gmail.com> Date: Sat, 21 Oct 2023 08:06:25 +0000 Subject: [PATCH 743/858] Translated using Weblate (Japanese) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ja/ --- .../Localization/Core/ja.json | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/ja.json b/Emby.Server.Implementations/Localization/Core/ja.json index 7b059c68ea..db6116080b 100644 --- a/Emby.Server.Implementations/Localization/Core/ja.json +++ b/Emby.Server.Implementations/Localization/Core/ja.json @@ -4,19 +4,19 @@ "Application": "アプリケーション", "Artists": "アーティスト", "AuthenticationSucceededWithUserName": "{0} 認証に成功しました", - "Books": "ブックス", + "Books": "ブック", "CameraImageUploadedFrom": "新しいカメライメージが {0}からアップロードされました", "Channels": "チャンネル", "ChapterNameValue": "チャプター {0}", "Collections": "コレクション", - "DeviceOfflineWithName": "{0} が切断されました", - "DeviceOnlineWithName": "{0} が接続されました", - "FailedLoginAttemptWithUserName": "ログインを試行しましたが {0} によって失敗しました", + "DeviceOfflineWithName": "{0} が切断しました", + "DeviceOnlineWithName": "{0} が接続しました", + "FailedLoginAttemptWithUserName": "{0} からのログインに失敗しました", "Favorites": "お気に入り", "Folders": "フォルダー", "Genres": "ジャンル", "HeaderAlbumArtists": "アルバムアーティスト", - "HeaderContinueWatching": "続けて見る", + "HeaderContinueWatching": "再生を続ける", "HeaderFavoriteAlbums": "お気に入りのアルバム", "HeaderFavoriteArtists": "お気に入りのアーティスト", "HeaderFavoriteEpisodes": "お気に入りのエピソード", @@ -30,19 +30,19 @@ "ItemAddedWithName": "{0} をライブラリに追加しました", "ItemRemovedWithName": "{0} をライブラリから削除しました", "LabelIpAddressValue": "IPアドレス: {0}", - "LabelRunningTimeValue": "稼働時間: {0}", + "LabelRunningTimeValue": "時間: {0}", "Latest": "最新", - "MessageApplicationUpdated": "Jellyfin Server が更新されました", - "MessageApplicationUpdatedTo": "Jellyfin Server が {0}に更新されました", - "MessageNamedServerConfigurationUpdatedWithValue": "サーバー設定項目の {0} が更新されました", - "MessageServerConfigurationUpdated": "サーバー設定が更新されました", + "MessageApplicationUpdated": "Jellyfin Server を更新しました", + "MessageApplicationUpdatedTo": "Jellyfin Server を {0}に更新しました", + "MessageNamedServerConfigurationUpdatedWithValue": "サーバー設定項目の {0} を更新しました", + "MessageServerConfigurationUpdated": "サーバー設定を更新しました", "MixedContent": "ミックスコンテンツ", "Movies": "映画", "Music": "音楽", "MusicVideos": "ミュージックビデオ", "NameInstallFailed": "{0}のインストールに失敗しました", "NameSeasonNumber": "シーズン {0}", - "NameSeasonUnknown": "不明なシーズン", + "NameSeasonUnknown": "シーズン不明", "NewVersionIsAvailable": "新しいバージョンの Jellyfin Server がダウンロード可能です。", "NotificationOptionApplicationUpdateAvailable": "アプリケーションの更新があります", "NotificationOptionApplicationUpdateInstalled": "アプリケーションは最新です", From 028b2122ce39ec54bcab78ffd252a41518231458 Mon Sep 17 00:00:00 2001 From: Andrejs <tlpbu@droplar.com> Date: Fri, 20 Oct 2023 15:48:45 +0000 Subject: [PATCH 744/858] Translated using Weblate (Latvian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/lv/ --- Emby.Server.Implementations/Localization/Core/lv.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/lv.json b/Emby.Server.Implementations/Localization/Core/lv.json index 83a000014d..82a071309e 100644 --- a/Emby.Server.Implementations/Localization/Core/lv.json +++ b/Emby.Server.Implementations/Localization/Core/lv.json @@ -20,7 +20,7 @@ "ValueHasBeenAddedToLibrary": "{0} ir ticis pievienots jūsu multvides bibliotēkai", "UserStoppedPlayingItemWithValues": "{0} ir beidzis atskaņot {1} uz {2}", "UserStartedPlayingItemWithValues": "{0} atskaņo {1} uz {2}", - "UserPasswordChangedWithName": "Parole nomainīta lietotājam {0}", + "UserPasswordChangedWithName": "Lietotāja {0} parole tika nomainīta", "UserOnlineFromDevice": "{0} ir tiešsaistē no {1}", "UserOfflineFromDevice": "{0} ir atvienojies no {1}", "UserLockedOutWithName": "Lietotājs {0} ir ticis bloķēts", @@ -33,7 +33,7 @@ "System": "Sistēma", "StartupEmbyServerIsLoading": "Jellyfin Serveris lādējas. Lūdzu mēģiniet vēlreiz pēc brīža.", "Songs": "Dziesmas", - "Shows": "Raidījumi", + "Shows": "Šovi", "PluginUpdatedWithName": "{0} tika atjaunots", "PluginUninstalledWithName": "{0} tika noņemts", "PluginInstalledWithName": "{0} tika uzstādīts", @@ -44,7 +44,7 @@ "HeaderNextUp": "Nākamais", "ChapterNameValue": "{0}. nodaļa", "Application": "Lietotne", - "NotificationOptionServerRestartRequired": "Vajadzīgs servera restarts", + "NotificationOptionServerRestartRequired": "Nepieciešams servera restarts", "NotificationOptionPluginUpdateInstalled": "Paplašinājuma atjauninājums uzstādīts", "NotificationOptionPluginUninstalled": "Paplašinājums noņemts", "NotificationOptionPluginInstalled": "Paplašinājums uzstādīts", @@ -71,7 +71,7 @@ "ItemRemovedWithName": "{0} tika noņemts no bibliotēkas", "ItemAddedWithName": "{0} tika pievienots bibliotēkai", "HeaderLiveTV": "Tiešraides TV", - "HeaderContinueWatching": "Turpināt skatīšanos", + "HeaderContinueWatching": "Turpini skatīties", "HeaderAlbumArtists": "Albumu izpildītāji", "Genres": "Žanri", "Folders": "Mapes", From 9c270b149c4b3401e5bb455e851d6b5e2e94031b Mon Sep 17 00:00:00 2001 From: nyanmisaka <nst799610810@gmail.com> Date: Sun, 22 Oct 2023 19:06:35 +0800 Subject: [PATCH 745/858] Fix mismatch between intel VAAPI UMD/KMD in rare cases Signed-off-by: nyanmisaka <nst799610810@gmail.com> --- .../MediaEncoding/EncodingHelper.cs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index c3a20cdb4e..6621ae2842 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -775,12 +775,17 @@ namespace MediaBrowser.Controller.MediaEncoding private string GetVaapiDeviceArgs(string renderNodePath, string driver, string kernelDriver, string srcDeviceAlias, string alias) { alias ??= VaapiAlias; - renderNodePath = renderNodePath ?? "/dev/dri/renderD128"; - var driverOpts = string.IsNullOrEmpty(driver) - ? ":" + renderNodePath - : ":,driver=" + driver + (string.IsNullOrEmpty(kernelDriver) ? string.Empty : ",kernel_driver=" + kernelDriver); + + // 'renderNodePath' has higher priority than 'kernelDriver' + var driverOpts = string.IsNullOrEmpty(renderNodePath) + ? (string.IsNullOrEmpty(kernelDriver) ? string.Empty : ",kernel_driver=" + kernelDriver) + : renderNodePath; + + // 'driver' behaves similarly to env LIBVA_DRIVER_NAME + driverOpts += string.IsNullOrEmpty(driver) ? string.Empty : ",driver=" + driver; + var options = string.IsNullOrEmpty(srcDeviceAlias) - ? driverOpts + ? (string.IsNullOrEmpty(driverOpts) ? string.Empty : ":" + driverOpts) : "@" + srcDeviceAlias; return string.Format( @@ -902,14 +907,14 @@ namespace MediaBrowser.Controller.MediaEncoding if (_mediaEncoder.IsVaapiDeviceInteliHD) { - args.Append(GetVaapiDeviceArgs(null, "iHD", null, null, VaapiAlias)); + args.Append(GetVaapiDeviceArgs(options.VaapiDevice, "iHD", null, null, VaapiAlias)); } else if (_mediaEncoder.IsVaapiDeviceInteli965) { // Only override i965 since it has lower priority than iHD in libva lookup. Environment.SetEnvironmentVariable("LIBVA_DRIVER_NAME", "i965"); Environment.SetEnvironmentVariable("LIBVA_DRIVER_NAME_JELLYFIN", "i965"); - args.Append(GetVaapiDeviceArgs(null, "i965", null, null, VaapiAlias)); + args.Append(GetVaapiDeviceArgs(options.VaapiDevice, "i965", null, null, VaapiAlias)); } var filterDevArgs = string.Empty; From b16033df03db7a6c3e3b3636c9eac4dad8e49f9d Mon Sep 17 00:00:00 2001 From: Bond-009 <bond.009@outlook.com> Date: Sun, 22 Oct 2023 17:01:51 +0200 Subject: [PATCH 746/858] Fix fuzz projects (#10416) --- .../Program.cs | 9 +++++++++ fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh | 2 +- .../Jellyfin.Api.Fuzz.csproj} | 4 ++-- .../Program.cs | 4 ++-- .../Testcases/UrlDecodeQueryFeature/test1.txt | 0 fuzz/Jellyfin.Api.Fuzz/fuzz.sh | 11 ++++++++++ fuzz/Jellyfin.Server.Fuzz/fuzz.sh | 11 ---------- fuzz/README.md | 20 +++++++++++++++++++ .../Middleware}/UrlDecodeQueryFeatureTests.cs | 3 +-- 9 files changed, 46 insertions(+), 18 deletions(-) rename fuzz/{Jellyfin.Server.Fuzz/Jellyfin.Server.Fuzz.csproj => Jellyfin.Api.Fuzz/Jellyfin.Api.Fuzz.csproj} (83%) rename fuzz/{Jellyfin.Server.Fuzz => Jellyfin.Api.Fuzz}/Program.cs (92%) rename fuzz/{Jellyfin.Server.Fuzz => Jellyfin.Api.Fuzz}/Testcases/UrlDecodeQueryFeature/test1.txt (100%) create mode 100755 fuzz/Jellyfin.Api.Fuzz/fuzz.sh delete mode 100755 fuzz/Jellyfin.Server.Fuzz/fuzz.sh create mode 100644 fuzz/README.md rename tests/{Jellyfin.Server.Tests => Jellyfin.Api.Tests/Middleware}/UrlDecodeQueryFeatureTests.cs (92%) diff --git a/fuzz/Emby.Server.Implementations.Fuzz/Program.cs b/fuzz/Emby.Server.Implementations.Fuzz/Program.cs index 03b2964948..1571b5ab07 100644 --- a/fuzz/Emby.Server.Implementations.Fuzz/Program.cs +++ b/fuzz/Emby.Server.Implementations.Fuzz/Program.cs @@ -6,6 +6,7 @@ using Emby.Server.Implementations.Library; using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Entities; +using Microsoft.Extensions.Configuration; using Moq; using SharpFuzz; @@ -54,8 +55,16 @@ namespace Emby.Server.Implementations.Fuzz appHost.Setup(x => x.ReverseVirtualPath(It.IsAny<string>())) .Returns((string x) => x.Replace(MetaDataPath, VirtualMetaDataPath, StringComparison.Ordinal)); + var configSection = new Mock<IConfigurationSection>(); + configSection.SetupGet(x => x[It.Is<string>(s => s == MediaBrowser.Controller.Extensions.ConfigurationExtensions.SqliteCacheSizeKey)]) + .Returns("0"); + var config = new Mock<IConfiguration>(); + config.Setup(x => x.GetSection(It.Is<string>(s => s == MediaBrowser.Controller.Extensions.ConfigurationExtensions.SqliteCacheSizeKey))) + .Returns(configSection.Object); + IFixture fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true }); fixture.Inject(appHost); + fixture.Inject(config); return fixture.Create<SqliteItemRepository>(); } } diff --git a/fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh b/fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh index 37e6bdb764..aa2a34cdde 100755 --- a/fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh +++ b/fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh @@ -8,4 +8,4 @@ cp bin/Emby.Server.Implementations.dll . dotnet build mkdir -p Findings -AFL_SKIP_BIN_CHECK=1 afl-fuzz -i "Testcases/$1" -o "Findings/$1" -t 5000 -m 10240 dotnet bin/Debug/net6.0/Emby.Server.Implementations.Fuzz.dll "$1" +AFL_SKIP_BIN_CHECK=1 afl-fuzz -i "Testcases/$1" -o "Findings/$1" -t 5000 ./bin/Debug/net7.0/Emby.Server.Implementations.Fuzz "$1" diff --git a/fuzz/Jellyfin.Server.Fuzz/Jellyfin.Server.Fuzz.csproj b/fuzz/Jellyfin.Api.Fuzz/Jellyfin.Api.Fuzz.csproj similarity index 83% rename from fuzz/Jellyfin.Server.Fuzz/Jellyfin.Server.Fuzz.csproj rename to fuzz/Jellyfin.Api.Fuzz/Jellyfin.Api.Fuzz.csproj index 20bc4c7244..da46e63a5e 100644 --- a/fuzz/Jellyfin.Server.Fuzz/Jellyfin.Server.Fuzz.csproj +++ b/fuzz/Jellyfin.Api.Fuzz/Jellyfin.Api.Fuzz.csproj @@ -6,8 +6,8 @@ </PropertyGroup> <ItemGroup> - <Reference Include="Jellyfin.Server"> - <HintPath>jellyfin.dll</HintPath> + <Reference Include="Jellyfin.Api"> + <HintPath>Jellyfin.Api.dll</HintPath> </Reference> </ItemGroup> diff --git a/fuzz/Jellyfin.Server.Fuzz/Program.cs b/fuzz/Jellyfin.Api.Fuzz/Program.cs similarity index 92% rename from fuzz/Jellyfin.Server.Fuzz/Program.cs rename to fuzz/Jellyfin.Api.Fuzz/Program.cs index e47286c131..6713322ac8 100644 --- a/fuzz/Jellyfin.Server.Fuzz/Program.cs +++ b/fuzz/Jellyfin.Api.Fuzz/Program.cs @@ -1,12 +1,12 @@ using System; using System.Collections.Generic; -using Jellyfin.Server.Middleware; +using Jellyfin.Api.Middleware; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Primitives; using SharpFuzz; -namespace Emby.Server.Implementations.Fuzz +namespace Jellyfin.Api.Fuzz { public static class Program { diff --git a/fuzz/Jellyfin.Server.Fuzz/Testcases/UrlDecodeQueryFeature/test1.txt b/fuzz/Jellyfin.Api.Fuzz/Testcases/UrlDecodeQueryFeature/test1.txt similarity index 100% rename from fuzz/Jellyfin.Server.Fuzz/Testcases/UrlDecodeQueryFeature/test1.txt rename to fuzz/Jellyfin.Api.Fuzz/Testcases/UrlDecodeQueryFeature/test1.txt diff --git a/fuzz/Jellyfin.Api.Fuzz/fuzz.sh b/fuzz/Jellyfin.Api.Fuzz/fuzz.sh new file mode 100755 index 0000000000..edf9655626 --- /dev/null +++ b/fuzz/Jellyfin.Api.Fuzz/fuzz.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +set -e + +dotnet build -c Release ../../Jellyfin.Api/Jellyfin.Api.csproj --output bin +sharpfuzz bin/Jellyfin.Api.dll +cp bin/Jellyfin.Api.dll . + +dotnet build +mkdir -p Findings +AFL_SKIP_BIN_CHECK=1 afl-fuzz -i "Testcases/$1" -o "Findings/$1" -t 5000 ./bin/Debug/net7.0/Jellyfin.Api.Fuzz "$1" diff --git a/fuzz/Jellyfin.Server.Fuzz/fuzz.sh b/fuzz/Jellyfin.Server.Fuzz/fuzz.sh deleted file mode 100755 index 303eb21356..0000000000 --- a/fuzz/Jellyfin.Server.Fuzz/fuzz.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh - -set -e - -dotnet build -c Release ../../Jellyfin.Server/Jellyfin.Server.csproj --output bin -sharpfuzz bin/jellyfin.dll -cp bin/jellyfin.dll . - -dotnet build -mkdir -p Findings -AFL_SKIP_BIN_CHECK=1 afl-fuzz -i "Testcases/$1" -o "Findings/$1" -t 5000 -m 10240 dotnet bin/Debug/net6.0/Jellyfin.Server.Fuzz.dll "$1" diff --git a/fuzz/README.md b/fuzz/README.md new file mode 100644 index 0000000000..25ba7d05cc --- /dev/null +++ b/fuzz/README.md @@ -0,0 +1,20 @@ +# Jellyfin fuzzing + +## Setup + +Install AFL++ +```sh +git clone https://github.com/AFLplusplus/AFLplusplus +cd AFLplusplus +make all +sudo make install +``` + +Install SharpFuzz.CommandLine global .NET tool +```sh +dotnet tool install --global SharpFuzz.CommandLine +``` + +## Running +Run the `fuzz.sh` in the directory corresponding to the project you want to fuzz. +The script takes a parameter of which fuzz case you want to run. diff --git a/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs b/tests/Jellyfin.Api.Tests/Middleware/UrlDecodeQueryFeatureTests.cs similarity index 92% rename from tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs rename to tests/Jellyfin.Api.Tests/Middleware/UrlDecodeQueryFeatureTests.cs index 93e0656856..1ff7e7b7a4 100644 --- a/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs +++ b/tests/Jellyfin.Api.Tests/Middleware/UrlDecodeQueryFeatureTests.cs @@ -1,12 +1,11 @@ using System.Collections.Generic; using System.Linq; -using Jellyfin.Api.Middleware; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Primitives; using Xunit; -namespace Jellyfin.Server.Tests +namespace Jellyfin.Api.Middleware.Tests { public static class UrlDecodeQueryFeatureTests { From 4aa1f127ecc1760880c4f68fa7f0928032743ca6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 22 Oct 2023 09:02:29 -0600 Subject: [PATCH 747/858] Update github/codeql-action action to v2.22.4 (#10440) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index a5f36eab42..64730e5541 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3 + uses: github/codeql-action/init@49abf0ba24d0b7953cb586944e918a0b92074c80 # v2.22.4 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3 + uses: github/codeql-action/autobuild@49abf0ba24d0b7953cb586944e918a0b92074c80 # v2.22.4 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3 + uses: github/codeql-action/analyze@49abf0ba24d0b7953cb586944e918a0b92074c80 # v2.22.4 From 1009836a792f5b76829b8ec9b2a6d859d2cde298 Mon Sep 17 00:00:00 2001 From: cvium <clausvium@gmail.com> Date: Sun, 22 Oct 2023 22:28:45 +0200 Subject: [PATCH 748/858] add IAsyncDisposable to DisplayPreferencesManager Properly dispose dbcontext Add IDisposableAnalyzer to Jellyfin.Server.Implementations --- .../Jellyfin.Server.Implementations.csproj | 4 ++++ .../Users/DisplayPreferencesManager.cs | 9 ++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index 390ed58b3b..fa6adb0220 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -8,6 +8,10 @@ <!-- Code analysers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="IDisposableAnalyzers"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> + </PackageReference> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> diff --git a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs index bfae81e4ca..edc6aa1738 100644 --- a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs +++ b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Jellyfin.Data.Entities; using MediaBrowser.Controller; using Microsoft.EntityFrameworkCore; @@ -13,7 +14,7 @@ namespace Jellyfin.Server.Implementations.Users /// <summary> /// Manages the storage and retrieval of display preferences through Entity Framework. /// </summary> - public class DisplayPreferencesManager : IDisplayPreferencesManager + public sealed class DisplayPreferencesManager : IDisplayPreferencesManager, IAsyncDisposable { private readonly JellyfinDbContext _dbContext; @@ -97,5 +98,11 @@ namespace Jellyfin.Server.Implementations.Users { _dbContext.SaveChanges(); } + + /// <inheritdoc /> + public async ValueTask DisposeAsync() + { + await _dbContext.DisposeAsync().ConfigureAwait(false); + } } } From 977ce9f8d5ee3a389c92d762fda4537bb7363be0 Mon Sep 17 00:00:00 2001 From: cvium <clausvium@gmail.com> Date: Mon, 23 Oct 2023 07:05:34 +0200 Subject: [PATCH 749/858] chore(deps): downgrade IDisposableAnalyzers to 4.0.4 Many distros seem to be behind on SDK updates, which causes issues. Temporarily downgrade the package. --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index d95cecdbf8..5fd81d6c61 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -20,7 +20,7 @@ <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.9.4" /> <PackageVersion Include="FsCheck.Xunit" Version="2.16.6" /> <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0" /> - <PackageVersion Include="IDisposableAnalyzers" Version="4.0.7" /> + <PackageVersion Include="IDisposableAnalyzers" Version="4.0.4" /> <PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" /> <PackageVersion Include="libse" Version="3.6.13" /> <PackageVersion Include="LrcParser" Version="2023.524.0" /> From 348ab7a58e8c525c9d139a0cf8f72253bd43ad5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Kucharczyk?= <lukas@kucharczyk.xyz> Date: Sun, 22 Oct 2023 14:22:40 +0000 Subject: [PATCH 750/858] Translated using Weblate (Czech) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/cs/ --- Emby.Server.Implementations/Localization/Core/cs.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json index f33ea2fc95..5da33febe3 100644 --- a/Emby.Server.Implementations/Localization/Core/cs.json +++ b/Emby.Server.Implementations/Localization/Core/cs.json @@ -124,5 +124,7 @@ "TaskKeyframeExtractorDescription": "Vytahuje klíčové snímky ze souborů videa za účelem vytváření přesnějších seznamů přehrávání HLS. Tento úkol může trvat velmi dlouho.", "TaskKeyframeExtractor": "Vytahovač klíčových snímků", "External": "Externí", - "HearingImpaired": "Sluchově postižení" + "HearingImpaired": "Sluchově postižení", + "TaskRefreshTrickplayImages": "Generovat obrázky pro Trickplay", + "TaskRefreshTrickplayImagesDescription": "Obrázky Trickplay se používají k zobrazení náhledů u videí v knihovnách, kde je to povoleno." } From 8dfb77451e97f01e091a2016ad57cd311837a0f4 Mon Sep 17 00:00:00 2001 From: Johannes Hartel <info@jojojux.de> Date: Sun, 22 Oct 2023 20:21:28 +0000 Subject: [PATCH 751/858] Translated using Weblate (German) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/de/ --- Emby.Server.Implementations/Localization/Core/de.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json index e1c3e9de12..229f055aa5 100644 --- a/Emby.Server.Implementations/Localization/Core/de.json +++ b/Emby.Server.Implementations/Localization/Core/de.json @@ -124,5 +124,7 @@ "TaskKeyframeExtractorDescription": "Extrahiere Keyframes aus Videodateien, um präzisere HLS-Playlisten zu erzeugen. Dieser Vorgang kann sehr lange dauern.", "TaskKeyframeExtractor": "Keyframe Extraktor", "External": "Extern", - "HearingImpaired": "Hörgeschädigt" + "HearingImpaired": "Hörgeschädigt", + "TaskRefreshTrickplayImages": "Trickplay-Bilder generieren", + "TaskRefreshTrickplayImagesDescription": "Erstellt eine Trickplay-Vorschau für Videos in aktivierten Bibliotheken" } From 716dd2f5eab4ba21e9dc2cf02e521ee18d608525 Mon Sep 17 00:00:00 2001 From: Bas <weblate@hanka.mp> Date: Sun, 22 Oct 2023 19:44:10 +0000 Subject: [PATCH 752/858] Translated using Weblate (Dutch) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/nl/ --- Emby.Server.Implementations/Localization/Core/nl.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index ac7b92de60..be397f1b89 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -124,5 +124,7 @@ "TaskKeyframeExtractorDescription": "Haalt keyframes uit videobestanden om preciezere HLS-afspeellijsten te maken. Deze taak kan lang duren.", "TaskKeyframeExtractor": "Keyframe-uitpakker", "External": "Extern", - "HearingImpaired": "Slechthorend" + "HearingImpaired": "Slechthorend", + "TaskRefreshTrickplayImages": "Trickplay-afbeeldingen genereren", + "TaskRefreshTrickplayImagesDescription": "Genereert trickplay-afbeeldingen voor video's in bibliotheken waarvoor dit is ingeschakeld." } From dc3d3051e177315d0f32e896c0ab79d6e3fc717f Mon Sep 17 00:00:00 2001 From: Kityn <kitynska@gmail.com> Date: Sun, 22 Oct 2023 11:40:12 +0000 Subject: [PATCH 753/858] Translated using Weblate (Polish) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pl/ --- Emby.Server.Implementations/Localization/Core/pl.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/pl.json b/Emby.Server.Implementations/Localization/Core/pl.json index d4c15ac876..bd572b744b 100644 --- a/Emby.Server.Implementations/Localization/Core/pl.json +++ b/Emby.Server.Implementations/Localization/Core/pl.json @@ -124,5 +124,7 @@ "External": "Zewnętrzny", "TaskKeyframeExtractorDescription": "Wyodrębnia klatki kluczowe z plików wideo w celu utworzenia bardziej precyzyjnych list odtwarzania HLS. To zadanie może trwać przez długi czas.", "TaskKeyframeExtractor": "Ekstraktor klatek kluczowych", - "HearingImpaired": "Niedosłyszący" + "HearingImpaired": "Niedosłyszący", + "TaskRefreshTrickplayImages": "Generuj obrazy trickplay", + "TaskRefreshTrickplayImagesDescription": "Tworzy podglądy trickplay dla filmów we włączonych bibliotekach." } From 0ff1c6991068cddb050abb46236882bcaa2520af Mon Sep 17 00:00:00 2001 From: nextlooper42 <nextlooper42@protonmail.com> Date: Sun, 22 Oct 2023 13:29:32 +0000 Subject: [PATCH 754/858] Translated using Weblate (Slovak) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/sk/ --- Emby.Server.Implementations/Localization/Core/sk.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/sk.json b/Emby.Server.Implementations/Localization/Core/sk.json index c231d76fe0..43594a42eb 100644 --- a/Emby.Server.Implementations/Localization/Core/sk.json +++ b/Emby.Server.Implementations/Localization/Core/sk.json @@ -124,5 +124,7 @@ "TaskKeyframeExtractorDescription": "Extrahuje kľúčové snímky z video súborov na vytvorenie presnejších HLS playlistov. Táto úloha môže trvať dlhšiu dobu.", "TaskKeyframeExtractor": "Extraktor kľúčových snímkov", "External": "Externé", - "HearingImpaired": "Sluchovo postihnutí" + "HearingImpaired": "Sluchovo postihnutí", + "TaskRefreshTrickplayImages": "Generovanie obrázkov Trickplay", + "TaskRefreshTrickplayImagesDescription": "Vytvára trickplay náhľady pre videá v povolených knižniciach." } From 2ebe54060238e0484adf53b2f37d422f6203d831 Mon Sep 17 00:00:00 2001 From: Xzonn <Xzonn@outlook.com> Date: Mon, 23 Oct 2023 12:03:53 +0000 Subject: [PATCH 755/858] Translated using Weblate (Chinese (Simplified)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hans/ --- Emby.Server.Implementations/Localization/Core/zh-CN.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json index 03265d3fb0..b88d4eeaf5 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-CN.json +++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json @@ -121,8 +121,10 @@ "Default": "默认", "TaskOptimizeDatabaseDescription": "压缩数据库并优化可用空间,在扫描库或执行其他数据库修改后运行此任务可能会提高性能。", "TaskOptimizeDatabase": "优化数据库", - "TaskKeyframeExtractorDescription": "从视频文件中提取关键帧以创建更准确的HLS播放列表。这项任务可能需要很长时间。", + "TaskKeyframeExtractorDescription": "从视频文件中提取关键帧以创建更准确的 HLS 播放列表。这项任务可能需要很长时间。", "TaskKeyframeExtractor": "关键帧提取器", "External": "外部", - "HearingImpaired": "听力障碍" + "HearingImpaired": "听力障碍", + "TaskRefreshTrickplayImages": "生成时间轴缩略图", + "TaskRefreshTrickplayImagesDescription": "为启用的媒体库中的视频生成时间轴缩略图。" } From 208b585a3f7d9764c023024b2a14e619464bdc04 Mon Sep 17 00:00:00 2001 From: Vijay <vcmvijay@gmail.com> Date: Mon, 23 Oct 2023 12:46:45 +0000 Subject: [PATCH 756/858] Translated using Weblate (Tamil) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ta/ --- Emby.Server.Implementations/Localization/Core/ta.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/ta.json b/Emby.Server.Implementations/Localization/Core/ta.json index 770624a8df..646d7d7a5f 100644 --- a/Emby.Server.Implementations/Localization/Core/ta.json +++ b/Emby.Server.Implementations/Localization/Core/ta.json @@ -102,7 +102,7 @@ "SubtitleDownloadFailureFromForItem": "வசன வரிகள் {0} இல் இருந்து {1} க்கு பதிவிறக்கத் தவறிவிட்டன", "TaskDownloadMissingSubtitlesDescription": "மீத்தரவு உள்ளமைவின் அடிப்படையில் வசன வரிகள் காணாமல் போனதற்கு இணையத்தைத் தேடுகிறது.", "TaskCleanTranscodeDescription": "ஒரு நாளைக்கு மேற்பட்ட பழைய டிரான்ஸ்கோட் கோப்புகளை நீக்குகிறது.", - "TaskUpdatePluginsDescription": "தானாகவே புதுப்பிக்க கட்டமைக்கப்பட்ட உட்செருகிகளுக்கான புதுப்பிப்புகளை பதிவிறக்குகிறது மற்றும் நிறுவுகிறது.", + "TaskUpdatePluginsDescription": "தானாகவே புதுப்பிக்கும்படி கட்டமைக்கப்பட்ட உட்செருகிகளுக்கான புதுப்பிப்புகளை பதிவிறக்கி நிறுவுகிறது.", "TaskRefreshPeopleDescription": "உங்கள் ஊடக நூலகத்தில் உள்ள நடிகர்கள் மற்றும் இயக்குனர்களுக்கான மீத்தரவை புதுப்பிக்கும்.", "TaskCleanLogsDescription": "{0} நாட்களுக்கு மேல் இருக்கும் பதிவு கோப்புகளை நீக்கும்.", "TaskCleanLogs": "பதிவு அடைவை சுத்தம் செய்யுங்கள்", @@ -123,5 +123,7 @@ "TaskKeyframeExtractorDescription": "மிகவும் துல்லியமான HLS பிளேலிஸ்ட்களை உருவாக்க வீடியோ கோப்புகளிலிருந்து கீஃப்ரேம்களைப் பிரித்தெடுக்கிறது. இந்த பணி நீண்ட காலமாக இருக்கலாம்.", "TaskKeyframeExtractor": "கீஃப்ரேம் எக்ஸ்ட்ராக்டர்", "External": "வெளி", - "HearingImpaired": "செவித்திறன் குறைபாடுடையவர்" + "HearingImpaired": "செவித்திறன் குறைபாடுடையவர்", + "TaskRefreshTrickplayImages": "முன்னோட்ட படங்களை உருவாக்கு", + "TaskRefreshTrickplayImagesDescription": "செயல்பாட்டில் உள்ள தொகுப்புகளுக்கு முன்னோட்ட படங்களை உருவாக்கும்." } From ca40fd1386275b746248283c765fd5b581f66b1f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 24 Oct 2023 21:24:50 +0000 Subject: [PATCH 757/858] chore(deps): update dependency efcoresecondlevelcacheinterceptor to v3.9.5 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 5fd81d6c61..cce8cc7a5a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -17,7 +17,7 @@ <PackageVersion Include="Diacritics" Version="3.3.18" /> <PackageVersion Include="DiscUtils.Udf" Version="0.16.13" /> <PackageVersion Include="DotNet.Glob" Version="3.1.3" /> - <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.9.4" /> + <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.9.5" /> <PackageVersion Include="FsCheck.Xunit" Version="2.16.6" /> <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0" /> <PackageVersion Include="IDisposableAnalyzers" Version="4.0.4" /> From a43991059704806b43dea279c64f971ddb1bb9e1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 24 Oct 2023 17:24:26 -0600 Subject: [PATCH 758/858] chore(deps): update dotnet monorepo to v7.0.13 (#10466) * Update dotnet monorepo to v7.0.12 * update sdk * chore(deps): update dotnet monorepo to v7.0.13 * update sdk * update sdk --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Cody Robibero <cody@robibe.ro> --- .config/dotnet-tools.json | 2 +- Directory.Packages.props | 18 +++++++++--------- deployment/Dockerfile.centos.amd64 | 2 +- deployment/Dockerfile.fedora.amd64 | 2 +- deployment/Dockerfile.ubuntu.amd64 | 2 +- deployment/Dockerfile.ubuntu.arm64 | 2 +- deployment/Dockerfile.ubuntu.armhf | 2 +- 7 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 2f789b031a..dbe78984a2 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "7.0.12", + "version": "7.0.13", "commands": [ "dotnet-ef" ] diff --git a/Directory.Packages.props b/Directory.Packages.props index 5fd81d6c61..01799ce260 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -25,15 +25,15 @@ <PackageVersion Include="libse" Version="3.6.13" /> <PackageVersion Include="LrcParser" Version="2023.524.0" /> <PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" /> - <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.12" /> + <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.13" /> <PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" /> - <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.12" /> + <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.13" /> <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" /> - <PackageVersion Include="Microsoft.Data.Sqlite" Version="7.0.12" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.12" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.12" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.12" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.12" /> + <PackageVersion Include="Microsoft.Data.Sqlite" Version="7.0.13" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.13" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.13" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.13" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.13" /> <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" /> @@ -42,8 +42,8 @@ <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" /> - <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.12" /> - <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.12" /> + <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.13" /> + <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.13" /> <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Http" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" /> diff --git a/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64 index 6c2086bee3..e5cf638c1d 100644 --- a/deployment/Dockerfile.centos.amd64 +++ b/deployment/Dockerfile.centos.amd64 @@ -13,7 +13,7 @@ RUN yum update -yq \ && yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget # Install DotNET SDK -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c4b5aad8-a416-436b-927c-3ebd5a9793ad/38efd1b64c8edc7c5f13699dd0be54e1/dotnet-sdk-7.0.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ff8c660f-ffa9-4814-ac2d-4089e6ec4eb5/dc806d344844f1d58d8015d105e85c65/dotnet-sdk-7.0.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.fedora.amd64 b/deployment/Dockerfile.fedora.amd64 index 16002f7905..777d92c116 100644 --- a/deployment/Dockerfile.fedora.amd64 +++ b/deployment/Dockerfile.fedora.amd64 @@ -12,7 +12,7 @@ RUN dnf update -yq \ && dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd wget make # Install DotNET SDK -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c4b5aad8-a416-436b-927c-3ebd5a9793ad/38efd1b64c8edc7c5f13699dd0be54e1/dotnet-sdk-7.0.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ff8c660f-ffa9-4814-ac2d-4089e6ec4eb5/dc806d344844f1d58d8015d105e85c65/dotnet-sdk-7.0.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64 index 2befb4b55b..f34c0358d1 100644 --- a/deployment/Dockerfile.ubuntu.amd64 +++ b/deployment/Dockerfile.ubuntu.amd64 @@ -17,7 +17,7 @@ RUN apt-get update -yqq \ libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c4b5aad8-a416-436b-927c-3ebd5a9793ad/38efd1b64c8edc7c5f13699dd0be54e1/dotnet-sdk-7.0.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ff8c660f-ffa9-4814-ac2d-4089e6ec4eb5/dc806d344844f1d58d8015d105e85c65/dotnet-sdk-7.0.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.arm64 b/deployment/Dockerfile.ubuntu.arm64 index 497a7a3ddd..87466d20ea 100644 --- a/deployment/Dockerfile.ubuntu.arm64 +++ b/deployment/Dockerfile.ubuntu.arm64 @@ -16,7 +16,7 @@ RUN apt-get update -yqq \ mmv build-essential lsb-release # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c4b5aad8-a416-436b-927c-3ebd5a9793ad/38efd1b64c8edc7c5f13699dd0be54e1/dotnet-sdk-7.0.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ff8c660f-ffa9-4814-ac2d-4089e6ec4eb5/dc806d344844f1d58d8015d105e85c65/dotnet-sdk-7.0.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.armhf b/deployment/Dockerfile.ubuntu.armhf index fae7b209fa..261deb3e45 100644 --- a/deployment/Dockerfile.ubuntu.armhf +++ b/deployment/Dockerfile.ubuntu.armhf @@ -16,7 +16,7 @@ RUN apt-get update -yqq \ mmv build-essential lsb-release # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c4b5aad8-a416-436b-927c-3ebd5a9793ad/38efd1b64c8edc7c5f13699dd0be54e1/dotnet-sdk-7.0.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ff8c660f-ffa9-4814-ac2d-4089e6ec4eb5/dc806d344844f1d58d8015d105e85c65/dotnet-sdk-7.0.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet From 4be0f4267d41a4948815ff9a7ca055c1b16dffe1 Mon Sep 17 00:00:00 2001 From: cvium <clausvium@gmail.com> Date: Wed, 25 Oct 2023 23:05:47 +0200 Subject: [PATCH 759/858] chore(deps): use Svg.Skia instead of the deprecated SkiaSharp.Svg --- Directory.Packages.props | 6 ++---- .../Jellyfin.Drawing.Skia.csproj | 4 ++-- src/Jellyfin.Drawing.Skia/SkiaEncoder.cs | 19 +++++++++---------- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 0619c4ef9c..d4d2e84553 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -2,9 +2,7 @@ <PropertyGroup> <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> </PropertyGroup> - <!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.--> - <ItemGroup Label="Package Dependencies"> <PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.0" /> <PackageVersion Include="AutoFixture.Xunit2" Version="4.18.0" /> @@ -72,9 +70,9 @@ <PackageVersion Include="SkiaSharp" Version="2.88.5" /> <PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.5" /> <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.5" /> - <PackageVersion Include="SkiaSharp.Svg" Version="1.60.0" /> <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.507" /> + <PackageVersion Include="Svg.Skia" Version="1.0.0.2" /> <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" /> <PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" /> <PackageVersion Include="System.Globalization" Version="4.3.0" /> @@ -90,4 +88,4 @@ <PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" /> <PackageVersion Include="xunit" Version="2.5.3" /> </ItemGroup> -</Project> +</Project> \ No newline at end of file diff --git a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj index c465c4ad09..09e103c6b5 100644 --- a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj +++ b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj @@ -18,11 +18,11 @@ <ItemGroup> <PackageReference Include="BlurHashSharp" /> <PackageReference Include="BlurHashSharp.SkiaSharp" /> + <PackageReference Include="HarfBuzzSharp.NativeAssets.Linux" /> <PackageReference Include="SkiaSharp" /> <PackageReference Include="SkiaSharp.NativeAssets.Linux" /> - <PackageReference Include="SkiaSharp.Svg" /> <PackageReference Include="SkiaSharp.HarfBuzz" /> - <PackageReference Include="HarfBuzzSharp.NativeAssets.Linux" /> + <PackageReference Include="Svg.Skia" /> </ItemGroup> <ItemGroup> diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 554fbe9417..21abe6e45c 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -2,19 +2,15 @@ using System; using System.Collections.Generic; using System.Globalization; using System.IO; -using System.Linq; -using System.Security.Cryptography.Xml; using BlurHashSharp.SkiaSharp; using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Drawing; -using MediaBrowser.Controller.LiveTv; using MediaBrowser.Model.Drawing; using Microsoft.Extensions.Logging; using SkiaSharp; -using static System.Net.Mime.MediaTypeNames; -using SKSvg = SkiaSharp.Extended.Svg.SKSvg; +using Svg.Skia; namespace Jellyfin.Drawing.Skia; @@ -123,10 +119,16 @@ public class SkiaEncoder : IImageEncoder var extension = Path.GetExtension(path.AsSpan()); if (extension.Equals(".svg", StringComparison.OrdinalIgnoreCase)) { - var svg = new SKSvg(); + using var svg = new SKSvg(); try { using var picture = svg.Load(path); + if (picture is null) + { + _logger.LogError("Unable to determine image dimensions for {FilePath}", path); + return default; + } + return new ImageDimensions(Convert.ToInt32(picture.CullRect.Width), Convert.ToInt32(picture.CullRect.Height)); } catch (FormatException skiaColorException) @@ -289,10 +291,7 @@ public class SkiaEncoder : IImageEncoder private SKBitmap OrientImage(SKBitmap bitmap, SKEncodedOrigin origin) { - var needsFlip = origin == SKEncodedOrigin.LeftBottom - || origin == SKEncodedOrigin.LeftTop - || origin == SKEncodedOrigin.RightBottom - || origin == SKEncodedOrigin.RightTop; + var needsFlip = origin is SKEncodedOrigin.LeftBottom or SKEncodedOrigin.LeftTop or SKEncodedOrigin.RightBottom or SKEncodedOrigin.RightTop; var rotated = needsFlip ? new SKBitmap(bitmap.Height, bitmap.Width) : new SKBitmap(bitmap.Width, bitmap.Height); From 0a284dc0ab3d8503df88b96c5c74640710942a76 Mon Sep 17 00:00:00 2001 From: cvium <clausvium@gmail.com> Date: Wed, 25 Oct 2023 23:29:05 +0200 Subject: [PATCH 760/858] refactor: cache the resize image filter --- src/Jellyfin.Drawing.Skia/SkiaEncoder.cs | 44 ++++++++++++++---------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 554fbe9417..b8290c5fcd 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -27,6 +27,30 @@ public class SkiaEncoder : IImageEncoder private readonly ILogger<SkiaEncoder> _logger; private readonly IApplicationPaths _appPaths; + private static readonly SKImageFilter _imageFilter; + +#pragma warning disable CA1810 + static SkiaEncoder() +#pragma warning restore CA1810 + { + var kernel = new[] + { + 0, -.1f, 0, + -.1f, 1.4f, -.1f, + 0, -.1f, 0, + }; + + var kernelSize = new SKSizeI(3, 3); + var kernelOffset = new SKPointI(1, 1); + _imageFilter = SKImageFilter.CreateMatrixConvolution( + kernelSize, + kernel, + 1f, + 0f, + kernelOffset, + SKShaderTileMode.Clamp, + true); + } /// <summary> /// Initializes a new instance of the <see cref="SkiaEncoder"/> class. @@ -357,25 +381,7 @@ public class SkiaEncoder : IImageEncoder IsDither = isDither }; - var kernel = new float[9] - { - 0, -.1f, 0, - -.1f, 1.4f, -.1f, - 0, -.1f, 0, - }; - - var kernelSize = new SKSizeI(3, 3); - var kernelOffset = new SKPointI(1, 1); - - paint.ImageFilter = SKImageFilter.CreateMatrixConvolution( - kernelSize, - kernel, - 1f, - 0f, - kernelOffset, - SKShaderTileMode.Clamp, - true); - + paint.ImageFilter = _imageFilter; canvas.DrawBitmap( source, SKRect.Create(0, 0, source.Width, source.Height), From 9bc29ceabdf88aca5e2a2f620651e86e11b92fab Mon Sep 17 00:00:00 2001 From: Jacob Slusser <jacobslusser@hotmail.com> Date: Wed, 25 Oct 2023 15:10:56 -0700 Subject: [PATCH 761/858] #10333 Applies the same fix for stale PRs: increase operations, process ascending --- .github/workflows/repo-stale.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/repo-stale.yaml b/.github/workflows/repo-stale.yaml index e0789ea588..d275e1eb83 100644 --- a/.github/workflows/repo-stale.yaml +++ b/.github/workflows/repo-stale.yaml @@ -28,7 +28,7 @@ jobs: exempt-issue-labels: regression,security,roadmap,future,feature,enhancement,confirmed stale-issue-label: stale stale-issue-message: |- - This issue has gone 120 days without an update and will be closed within 21 days if there is no new activity. To prevent this issue from being closed, please confirm the issue has not already been fixed by providing updated examples or logs. + This issue has gone 120 days without an update and will be closed within 21 days if there is no new activity. To prevent this issue from being closed, please confirm the issue has not already been fixed by providing updated examples or logs. If you have any questions you can use one of several ways to [contact us](https://jellyfin.org/contact). close-issue-message: |- @@ -42,7 +42,8 @@ jobs: - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0 with: repo-token: ${{ secrets.JF_BOT_TOKEN }} - operations-per-run: 75 + ascending: true + operations-per-run: 150 # The merge conflict action will remove the label when updated remove-stale-when-updated: false days-before-stale: -1 From 8d0a03e9e12cb5275d783e39e0ef41703751e673 Mon Sep 17 00:00:00 2001 From: Jacob Slusser <jacobslusser@hotmail.com> Date: Wed, 25 Oct 2023 15:13:25 -0700 Subject: [PATCH 762/858] Fixes required spacing for linebreak in markdown --- .github/workflows/repo-stale.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/repo-stale.yaml b/.github/workflows/repo-stale.yaml index d275e1eb83..f9075ba03a 100644 --- a/.github/workflows/repo-stale.yaml +++ b/.github/workflows/repo-stale.yaml @@ -28,7 +28,7 @@ jobs: exempt-issue-labels: regression,security,roadmap,future,feature,enhancement,confirmed stale-issue-label: stale stale-issue-message: |- - This issue has gone 120 days without an update and will be closed within 21 days if there is no new activity. To prevent this issue from being closed, please confirm the issue has not already been fixed by providing updated examples or logs. + This issue has gone 120 days without an update and will be closed within 21 days if there is no new activity. To prevent this issue from being closed, please confirm the issue has not already been fixed by providing updated examples or logs. If you have any questions you can use one of several ways to [contact us](https://jellyfin.org/contact). close-issue-message: |- From 123c6e7d1bc3a9459eb54697c293763b753b7295 Mon Sep 17 00:00:00 2001 From: Vincent Lark <v.lark@astonitf.com> Date: Thu, 26 Oct 2023 20:06:45 +0200 Subject: [PATCH 763/858] Extract the MediaEncoder probing command arguments builder --- .../Encoder/MediaEncoder.cs | 32 +++++++++++-------- .../Probing/ProbeExternalSourcesTests.cs | 11 ++++--- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index e8041f1984..4dbefca4bb 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -418,10 +418,24 @@ namespace MediaBrowser.MediaEncoding.Encoder public Task<MediaInfo> GetMediaInfo(MediaInfoRequest request, CancellationToken cancellationToken) { var extractChapters = request.MediaType == DlnaProfileType.Video && request.ExtractChapters; - var requiredHeaders = request.MediaSource.RequiredHttpHeaders; - var analyzeDuration = string.Empty; + var extraArgs = GetExtraArguments(request); + + return GetMediaInfoInternal( + GetInputArgument(request.MediaSource.Path, request.MediaSource), + request.MediaSource.Path, + request.MediaSource.Protocol, + extractChapters, + extraArgs, + request.MediaType == DlnaProfileType.Audio, + request.MediaSource.VideoType, + cancellationToken); + } + + internal string GetExtraArguments(MediaInfoRequest request) + { var ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty; var ffmpegProbeSize = _config.GetFFmpegProbeSize() ?? string.Empty; + var analyzeDuration = string.Empty; var extraArgs = string.Empty; if (request.MediaSource.AnalyzeDurationMs > 0) @@ -443,20 +457,12 @@ namespace MediaBrowser.MediaEncoding.Encoder extraArgs += " -probesize " + ffmpegProbeSize; } - if (requiredHeaders.ContainsKey("user_agent")) + if (request.MediaSource.RequiredHttpHeaders.TryGetValue("user_agent", out var userAgent)) { - extraArgs += " -user_agent " + requiredHeaders["user_agent"]; + extraArgs += " -user_agent " + userAgent; } - return GetMediaInfoInternal( - GetInputArgument(request.MediaSource.Path, request.MediaSource), - request.MediaSource.Path, - request.MediaSource.Protocol, - extractChapters, - extraArgs, - request.MediaType == DlnaProfileType.Audio, - request.MediaSource.VideoType, - cancellationToken); + return extraArgs; } /// <inheritdoc /> diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeExternalSourcesTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeExternalSourcesTests.cs index 17f8ec163b..263f74c900 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeExternalSourcesTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeExternalSourcesTests.cs @@ -1,5 +1,5 @@ +using System; using System.Collections.Generic; -using System.Threading; using MediaBrowser.Controller.Configuration; using MediaBrowser.MediaEncoding.Encoder; using MediaBrowser.Model.Globalization; @@ -15,7 +15,7 @@ namespace Jellyfin.MediaEncoding.Tests.Probing public class ProbeExternalSourcesTests { [Fact] - public void GetMediaInfo_Uses_UserAgent() + public void GetExtraArguments_Forwards_UserAgent() { var encoder = new MediaEncoder( Mock.Of<ILogger<MediaEncoder>>(), @@ -26,6 +26,7 @@ namespace Jellyfin.MediaEncoding.Tests.Probing new ConfigurationBuilder().Build(), Mock.Of<IServerConfigurationManager>()); + var userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"; var req = new MediaBrowser.Controller.MediaEncoding.MediaInfoRequest() { MediaSource = new MediaBrowser.Model.Dto.MediaSourceInfo @@ -34,14 +35,16 @@ namespace Jellyfin.MediaEncoding.Tests.Probing Protocol = MediaProtocol.Http, RequiredHttpHeaders = new Dictionary<string, string>() { - { "user_agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/530.35 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/530.35" }, + { "user_agent", userAgent }, } }, ExtractChapters = false, MediaType = MediaBrowser.Model.Dlna.DlnaProfileType.Video, }; - encoder.GetMediaInfo(req, CancellationToken.None); + var extraArg = encoder.GetExtraArguments(req); + + Assert.Contains(userAgent, extraArg, StringComparison.InvariantCulture); } } } From 622e0414bd7e085571259f41d7901dc3c7879c0f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Oct 2023 10:36:15 +0000 Subject: [PATCH 764/858] chore(deps): update github/codeql-action action to v2.22.5 --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 64730e5541..f43d743f04 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@49abf0ba24d0b7953cb586944e918a0b92074c80 # v2.22.4 + uses: github/codeql-action/init@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@49abf0ba24d0b7953cb586944e918a0b92074c80 # v2.22.4 + uses: github/codeql-action/autobuild@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@49abf0ba24d0b7953cb586944e918a0b92074c80 # v2.22.4 + uses: github/codeql-action/analyze@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5 From bc99c567ce3c185dc5992dc710adb69a753ef8b4 Mon Sep 17 00:00:00 2001 From: xsiviso <abholungen_platte_0r@icloud.com> Date: Thu, 26 Oct 2023 23:07:11 +0000 Subject: [PATCH 765/858] Translated using Weblate (German) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/de/ --- Emby.Server.Implementations/Localization/Core/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json index 229f055aa5..f1dbf3c89d 100644 --- a/Emby.Server.Implementations/Localization/Core/de.json +++ b/Emby.Server.Implementations/Localization/Core/de.json @@ -126,5 +126,5 @@ "External": "Extern", "HearingImpaired": "Hörgeschädigt", "TaskRefreshTrickplayImages": "Trickplay-Bilder generieren", - "TaskRefreshTrickplayImagesDescription": "Erstellt eine Trickplay-Vorschau für Videos in aktivierten Bibliotheken" + "TaskRefreshTrickplayImagesDescription": "Erstellt eine Trickplay-Vorschau für Videos in aktivierten Bibliotheken." } From b55810fce4ff2469d836f7b2d43a0e797e8235ef Mon Sep 17 00:00:00 2001 From: Daniel Finol Sola <danielfinol12490@gmail.com> Date: Fri, 27 Oct 2023 06:43:43 +0000 Subject: [PATCH 766/858] Translated using Weblate (Spanish) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/es/ --- Emby.Server.Implementations/Localization/Core/es.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json index 4c56f789d3..fe10be3085 100644 --- a/Emby.Server.Implementations/Localization/Core/es.json +++ b/Emby.Server.Implementations/Localization/Core/es.json @@ -124,5 +124,7 @@ "TaskKeyframeExtractorDescription": "Extrae los fotogramas clave de los archivos de vídeo para crear listas HLS más precisas. Esta tarea puede tardar mucho tiempo.", "TaskKeyframeExtractor": "Extractor de Fotogramas Clave", "External": "Externo", - "HearingImpaired": "Discapacidad Auditiva" + "HearingImpaired": "Discapacidad Auditiva", + "TaskRefreshTrickplayImages": "Generar miniaturas de línea de tiempo", + "TaskRefreshTrickplayImagesDescription": "Crear miniaturas de tiempo para videos en las librerías habilitadas." } From 809b27f7c8976547b02fa3c1dfcf9459b6c287d3 Mon Sep 17 00:00:00 2001 From: Alexander Weimer <alexskypie@gmail.com> Date: Fri, 27 Oct 2023 00:36:14 +0000 Subject: [PATCH 767/858] Translated using Weblate (Swedish) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/sv/ --- Emby.Server.Implementations/Localization/Core/sv.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json index 785e6b2262..97062deece 100644 --- a/Emby.Server.Implementations/Localization/Core/sv.json +++ b/Emby.Server.Implementations/Localization/Core/sv.json @@ -124,5 +124,7 @@ "TaskKeyframeExtractorDescription": "Exporterar nyckelbildrutor från videofiler för att skapa mer exakta HLS-spellistor. Denna rutin kan ta lång tid.", "TaskKeyframeExtractor": "Extraktor för nyckelbildrutor", "External": "Extern", - "HearingImpaired": "Hörselskadad" + "HearingImpaired": "Hörselskadad", + "TaskRefreshTrickplayImages": "Generera Trickplay-bilder", + "TaskRefreshTrickplayImagesDescription": "Skapar trickplay-förhandsvisningar för videor i aktiverade bibliotek." } From e8a82d29148b6b50669ece5f8fd5d2512bc44f55 Mon Sep 17 00:00:00 2001 From: stanol <stanol777@gmail.com> Date: Thu, 26 Oct 2023 18:18:32 +0000 Subject: [PATCH 768/858] Translated using Weblate (Ukrainian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/uk/ --- Emby.Server.Implementations/Localization/Core/uk.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json index ff77fb8c56..bd5398f083 100644 --- a/Emby.Server.Implementations/Localization/Core/uk.json +++ b/Emby.Server.Implementations/Localization/Core/uk.json @@ -123,5 +123,7 @@ "TaskKeyframeExtractorDescription": "Витягує ключові кадри з відеофайлів для створення більш точних списків відтворення HLS. Це завдання може виконуватися протягом тривалого часу.", "TaskKeyframeExtractor": "Екстрактор ключових кадрів", "External": "Зовнішній", - "HearingImpaired": "З порушеннями слуху" + "HearingImpaired": "З порушеннями слуху", + "TaskRefreshTrickplayImagesDescription": "Створює trickplay-зображення для відео у ввімкнених медіатеках.", + "TaskRefreshTrickplayImages": "Створення Trickplay-зображень" } From 0264f9f9472f30b1c1b2543423688b0def8ca9b8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 29 Oct 2023 12:36:43 +0000 Subject: [PATCH 769/858] chore(deps): update prometheus-net monorepo to v8.1.0 --- Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index d4d2e84553..05211ca652 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -55,9 +55,9 @@ <PackageVersion Include="NEbml" Version="0.11.0" /> <PackageVersion Include="Newtonsoft.Json" Version="13.0.3" /> <PackageVersion Include="PlaylistsNET" Version="1.4.0" /> - <PackageVersion Include="prometheus-net.AspNetCore" Version="8.0.1" /> + <PackageVersion Include="prometheus-net.AspNetCore" Version="8.1.0" /> <PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" /> - <PackageVersion Include="prometheus-net" Version="8.0.1" /> + <PackageVersion Include="prometheus-net" Version="8.1.0" /> <PackageVersion Include="Serilog.AspNetCore" Version="7.0.0" /> <PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" /> <PackageVersion Include="Serilog.Settings.Configuration" Version="7.0.1" /> From 536b94ccdbf5a6a79a52d15c1ffb7010bccdf9de Mon Sep 17 00:00:00 2001 From: Nurzhan Kozhanov <nurzhan.k@lightsoul.dev> Date: Sun, 29 Oct 2023 11:02:15 +0000 Subject: [PATCH 770/858] Translated using Weblate (Kazakh) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/kk/ --- Emby.Server.Implementations/Localization/Core/kk.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/kk.json b/Emby.Server.Implementations/Localization/Core/kk.json index c5a93cb963..e050196bc9 100644 --- a/Emby.Server.Implementations/Localization/Core/kk.json +++ b/Emby.Server.Implementations/Localization/Core/kk.json @@ -123,5 +123,8 @@ "TaskOptimizeDatabase": "Derekqordy oñtailandyru", "TaskKeyframeExtractorDescription": "Naqtyraq HLS oynatu tızımderın jasau üşın beinefaildardan negızgı kadrlardy şyğarady. Būl tapsyrma ūzaq uaqytqa sozyluy mümkın.", "TaskKeyframeExtractor": "Negızgı kadrlardy şyğaru", - "External": "Syrtqy" + "External": "Syrtqy", + "TaskRefreshTrickplayImagesDescription": "Іске қосылған кітапханалардағы бейнелер үшін Trickplay алдын ала түрінде көрсетілімді жасайды.", + "TaskRefreshTrickplayImages": "Trickplay үшін суреттерді жасау", + "HearingImpaired": "Есту қабілеті нашарға" } From b26eb7dd6bb928bd3e6f292833092fd518c6db0b Mon Sep 17 00:00:00 2001 From: Nurzhan Kozhanov <nurzhan.k@lightsoul.dev> Date: Sun, 29 Oct 2023 10:29:33 +0000 Subject: [PATCH 771/858] Translated using Weblate (Russian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ru/ --- Emby.Server.Implementations/Localization/Core/ru.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json index fa6c753b60..26d678a0c3 100644 --- a/Emby.Server.Implementations/Localization/Core/ru.json +++ b/Emby.Server.Implementations/Localization/Core/ru.json @@ -124,5 +124,7 @@ "TaskKeyframeExtractorDescription": "Извлекаются ключевые кадры из видеофайлов для создания более точных списков плей-листов HLS. Эта задача может выполняться в течение длительного времени.", "TaskKeyframeExtractor": "Извлечение ключевых кадров", "External": "Внешние", - "HearingImpaired": "Для слабослышащих" + "HearingImpaired": "Для слабослышащих", + "TaskRefreshTrickplayImages": "Сгенерировать изображения для Trickplay", + "TaskRefreshTrickplayImagesDescription": "Создает предпросмотры для Trickplay для видео в библиотеках, где эта функция включена." } From 8c5fc8028240adec57a4b39147dbeac81a1835a0 Mon Sep 17 00:00:00 2001 From: Cody Robibero <cody@robibe.ro> Date: Mon, 30 Oct 2023 15:31:13 -0600 Subject: [PATCH 772/858] Don't remove all tokens if invalid header (#10490) --- .../Session/SessionManager.cs | 12 +- .../SessionManager/SessionManagerTests.cs | 111 ++++++++++++++++++ 2 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 tests/Jellyfin.Server.Implementations.Tests/SessionManager/SessionManagerTests.cs diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index dc59a45239..e8e63d286d 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1453,10 +1453,15 @@ namespace Emby.Server.Implementations.Session return AuthenticateNewSessionInternal(request, false); } - private async Task<AuthenticationResult> AuthenticateNewSessionInternal(AuthenticationRequest request, bool enforcePassword) + internal async Task<AuthenticationResult> AuthenticateNewSessionInternal(AuthenticationRequest request, bool enforcePassword) { CheckDisposed(); + ArgumentException.ThrowIfNullOrEmpty(request.App); + ArgumentException.ThrowIfNullOrEmpty(request.DeviceId); + ArgumentException.ThrowIfNullOrEmpty(request.DeviceName); + ArgumentException.ThrowIfNullOrEmpty(request.AppVersion); + User user = null; if (!request.UserId.Equals(default)) { @@ -1517,8 +1522,11 @@ namespace Emby.Server.Implementations.Session return returnResult; } - private async Task<string> GetAuthorizationToken(User user, string deviceId, string app, string appVersion, string deviceName) + internal async Task<string> GetAuthorizationToken(User user, string deviceId, string app, string appVersion, string deviceName) { + // This should be validated above, but if it isn't don't delete all tokens. + ArgumentException.ThrowIfNullOrEmpty(deviceId); + var existing = (await _deviceManager.GetDevices( new DeviceQuery { diff --git a/tests/Jellyfin.Server.Implementations.Tests/SessionManager/SessionManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/SessionManager/SessionManagerTests.cs new file mode 100644 index 0000000000..ebd3a3891c --- /dev/null +++ b/tests/Jellyfin.Server.Implementations.Tests/SessionManager/SessionManagerTests.cs @@ -0,0 +1,111 @@ +using System; +using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Devices; +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Events; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Session; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using Xunit; + +namespace Jellyfin.Server.Implementations.Tests.SessionManager; + +public class SessionManagerTests +{ + [Theory] + [InlineData("", typeof(ArgumentException))] + [InlineData(null, typeof(ArgumentNullException))] + public async Task GetAuthorizationToken_Should_ThrowException(string deviceId, Type exceptionType) + { + await using var sessionManager = new Emby.Server.Implementations.Session.SessionManager( + NullLogger<Emby.Server.Implementations.Session.SessionManager>.Instance, + Mock.Of<IEventManager>(), + Mock.Of<IUserDataManager>(), + Mock.Of<IServerConfigurationManager>(), + Mock.Of<ILibraryManager>(), + Mock.Of<IUserManager>(), + Mock.Of<IMusicManager>(), + Mock.Of<IDtoService>(), + Mock.Of<IImageProcessor>(), + Mock.Of<IServerApplicationHost>(), + Mock.Of<IDeviceManager>(), + Mock.Of<IMediaSourceManager>(), + Mock.Of<IHostApplicationLifetime>()); + + await Assert.ThrowsAsync(exceptionType, () => sessionManager.GetAuthorizationToken( + new User("test", "default", "default"), + deviceId, + "app_name", + "0.0.0", + "device_name")); + } + + [Theory] + [MemberData(nameof(AuthenticateNewSessionInternal_Exception_TestData))] + public async Task AuthenticateNewSessionInternal_Should_ThrowException(AuthenticationRequest authenticationRequest, Type exceptionType) + { + await using var sessionManager = new Emby.Server.Implementations.Session.SessionManager( + NullLogger<Emby.Server.Implementations.Session.SessionManager>.Instance, + Mock.Of<IEventManager>(), + Mock.Of<IUserDataManager>(), + Mock.Of<IServerConfigurationManager>(), + Mock.Of<ILibraryManager>(), + Mock.Of<IUserManager>(), + Mock.Of<IMusicManager>(), + Mock.Of<IDtoService>(), + Mock.Of<IImageProcessor>(), + Mock.Of<IServerApplicationHost>(), + Mock.Of<IDeviceManager>(), + Mock.Of<IMediaSourceManager>(), + Mock.Of<IHostApplicationLifetime>()); + + await Assert.ThrowsAsync(exceptionType, () => sessionManager.AuthenticateNewSessionInternal(authenticationRequest, false)); + } + + public static TheoryData<AuthenticationRequest, Type> AuthenticateNewSessionInternal_Exception_TestData() + { + var data = new TheoryData<AuthenticationRequest, Type> + { + { + new AuthenticationRequest { App = string.Empty, DeviceId = "device_id", DeviceName = "device_name", AppVersion = "app_version" }, + typeof(ArgumentException) + }, + { + new AuthenticationRequest { App = null, DeviceId = "device_id", DeviceName = "device_name", AppVersion = "app_version" }, + typeof(ArgumentNullException) + }, + { + new AuthenticationRequest { App = "app_name", DeviceId = string.Empty, DeviceName = "device_name", AppVersion = "app_version" }, + typeof(ArgumentException) + }, + { + new AuthenticationRequest { App = "app_name", DeviceId = null, DeviceName = "device_name", AppVersion = "app_version" }, + typeof(ArgumentNullException) + }, + { + new AuthenticationRequest { App = "app_name", DeviceId = "device_id", DeviceName = string.Empty, AppVersion = "app_version" }, + typeof(ArgumentException) + }, + { + new AuthenticationRequest { App = "app_name", DeviceId = "device_id", DeviceName = null, AppVersion = "app_version" }, + typeof(ArgumentNullException) + }, + { + new AuthenticationRequest { App = "app_name", DeviceId = "device_id", DeviceName = "device_name", AppVersion = string.Empty }, + typeof(ArgumentException) + }, + { + new AuthenticationRequest { App = "app_name", DeviceId = "device_id", DeviceName = "device_name", AppVersion = null }, + typeof(ArgumentNullException) + } + }; + + return data; + } +} From 75c47da539e46a6e70232679234eb55884484702 Mon Sep 17 00:00:00 2001 From: Metin Bektas <30674934+methbkts@users.noreply.github.com> Date: Tue, 31 Oct 2023 09:37:37 +0100 Subject: [PATCH 773/858] Update jellyfin.xml Fix the link to the Jellyfin Project --- deployment/unraid/docker-templates/jellyfin.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/unraid/docker-templates/jellyfin.xml b/deployment/unraid/docker-templates/jellyfin.xml index 57b4cc5ae1..69742066eb 100644 --- a/deployment/unraid/docker-templates/jellyfin.xml +++ b/deployment/unraid/docker-templates/jellyfin.xml @@ -21,7 +21,7 @@ <Registry>https://hub.docker.com/r/jellyfin/jellyfin/</Registry> <GitHub>https://github.com/jellyfin/jellyfin/></GitHub> <Repository>jellyfin/jellyfin</Repository> - <Project>https://jellyfin.media/</Project> + <Project>https://jellyfin.org/</Project> <BindTime>true</BindTime> <Privileged>false</Privileged> <Networking> From 4dfbb8f74a04b9ec67481dc3490f132538a1d78e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 31 Oct 2023 14:23:29 +0000 Subject: [PATCH 774/858] chore(deps): update dependency efcoresecondlevelcacheinterceptor to v4 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 05211ca652..bcf0ac544f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -15,7 +15,7 @@ <PackageVersion Include="Diacritics" Version="3.3.18" /> <PackageVersion Include="DiscUtils.Udf" Version="0.16.13" /> <PackageVersion Include="DotNet.Glob" Version="3.1.3" /> - <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.9.5" /> + <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="4.0.0" /> <PackageVersion Include="FsCheck.Xunit" Version="2.16.6" /> <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0" /> <PackageVersion Include="IDisposableAnalyzers" Version="4.0.4" /> From e016ef285e05bfcb5fc2d7ef29a12504be160b02 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 31 Oct 2023 22:14:58 +0000 Subject: [PATCH 775/858] chore(deps): update dependency xunit to v2.6.0 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 05211ca652..64a42b9abf 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -86,6 +86,6 @@ <PackageVersion Include="Xunit.Priority" Version="1.1.6" /> <PackageVersion Include="xunit.runner.visualstudio" Version="2.5.3" /> <PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" /> - <PackageVersion Include="xunit" Version="2.5.3" /> + <PackageVersion Include="xunit" Version="2.6.0" /> </ItemGroup> </Project> \ No newline at end of file From 66a18bf464097ade768caf0511752c2a5ad6dbbb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 1 Nov 2023 12:45:46 +0000 Subject: [PATCH 776/858] chore(deps): update dependency metabrainz.musicbrainz to v5.0.1 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index bcf0ac544f..cddbf9f590 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -22,7 +22,7 @@ <PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" /> <PackageVersion Include="libse" Version="3.6.13" /> <PackageVersion Include="LrcParser" Version="2023.524.0" /> - <PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" /> + <PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.1" /> <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.13" /> <PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" /> <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.13" /> From 0be06f843b6d07f836e3bac390238b8eb5c8bbb8 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Wed, 1 Nov 2023 22:22:41 +0100 Subject: [PATCH 777/858] Update SkiaSharp to v2.88.5 --- Directory.Packages.props | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index a363b1f42a..3a45d40a43 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -67,9 +67,9 @@ <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.0" /> <PackageVersion Include="SerilogAnalyzer" Version="0.15.0" /> <PackageVersion Include="SharpFuzz" Version="2.1.1" /> - <PackageVersion Include="SkiaSharp" Version="2.88.5" /> - <PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.5" /> - <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.5" /> + <PackageVersion Include="SkiaSharp" Version="2.88.6" /> + <PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.6" /> + <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.6" /> <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.507" /> <PackageVersion Include="Svg.Skia" Version="1.0.0.2" /> @@ -88,4 +88,4 @@ <PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" /> <PackageVersion Include="xunit" Version="2.6.0" /> </ItemGroup> -</Project> \ No newline at end of file +</Project> From 5e19459f943263253761d2b90d216dc8b29d667d Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Wed, 1 Nov 2023 23:12:38 +0100 Subject: [PATCH 778/858] Update BlurHashSharp --- Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 3a45d40a43..4e9025c650 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,8 +8,8 @@ <PackageVersion Include="AutoFixture.Xunit2" Version="4.18.0" /> <PackageVersion Include="AutoFixture" Version="4.18.0" /> <PackageVersion Include="BDInfo" Version="0.7.6.2" /> - <PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.3.0" /> - <PackageVersion Include="BlurHashSharp" Version="1.3.0" /> + <PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.3.1" /> + <PackageVersion Include="BlurHashSharp" Version="1.3.1" /> <PackageVersion Include="CommandLineParser" Version="2.9.1" /> <PackageVersion Include="coverlet.collector" Version="6.0.0" /> <PackageVersion Include="Diacritics" Version="3.3.18" /> From 9785b58b857129cee85788637f216248a49c6e7c Mon Sep 17 00:00:00 2001 From: Steve Kowalik <steven@wedontsleep.org> Date: Thu, 2 Nov 2023 22:00:13 +1100 Subject: [PATCH 779/858] Correct docstring for /Upcoming The docstring for /Upcoming looks very similar to /NextUp, also including the same return value, when it should be slightly different, correct it. --- Jellyfin.Api/Controllers/TvShowsController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index bdbbd1e0db..af403fa80e 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -135,7 +135,7 @@ public class TvShowsController : BaseJellyfinApiController /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> /// <param name="enableUserData">Optional. Include user data.</param> - /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns> + /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the upcoming episodes.</returns> [HttpGet("Upcoming")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<QueryResult<BaseItemDto>> GetUpcomingEpisodes( From d168b977c0321c80369377f8fbde93a5b243866b Mon Sep 17 00:00:00 2001 From: Jesse <jbourkespriggs@gmail.com> Date: Wed, 1 Nov 2023 08:20:40 +0000 Subject: [PATCH 780/858] Translated using Weblate (English (United Kingdom)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/en_GB/ --- Emby.Server.Implementations/Localization/Core/en-GB.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/en-GB.json b/Emby.Server.Implementations/Localization/Core/en-GB.json index 2436883883..32bf893100 100644 --- a/Emby.Server.Implementations/Localization/Core/en-GB.json +++ b/Emby.Server.Implementations/Localization/Core/en-GB.json @@ -124,5 +124,7 @@ "TaskKeyframeExtractorDescription": "Extracts keyframes from video files to create more precise HLS playlists. This task may run for a long time.", "TaskKeyframeExtractor": "Keyframe Extractor", "External": "External", - "HearingImpaired": "Hearing Impaired" + "HearingImpaired": "Hearing Impaired", + "TaskRefreshTrickplayImages": "Generate Trickplay Images", + "TaskRefreshTrickplayImagesDescription": "Creates trickplay previews for videos in enabled libraries." } From 58e24fa25bcaa41dae12a3123ec0f1a85da280f1 Mon Sep 17 00:00:00 2001 From: Pit Plumer <pplumer@yahoo.de> Date: Tue, 31 Oct 2023 13:20:04 +0000 Subject: [PATCH 781/858] Translated using Weblate (French) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fr/ --- .../Localization/Core/fr.json | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json index a2b429dcdd..03002476ce 100644 --- a/Emby.Server.Implementations/Localization/Core/fr.json +++ b/Emby.Server.Implementations/Localization/Core/fr.json @@ -5,7 +5,7 @@ "Artists": "Artistes", "AuthenticationSucceededWithUserName": "{0} authentifié avec succès", "Books": "Livres", - "CameraImageUploadedFrom": "Une photo a été téléversée depuis {0}", + "CameraImageUploadedFrom": "Une photo a été téléchargée depuis {0}", "Channels": "Chaînes", "ChapterNameValue": "Chapitre {0}", "Collections": "Collections", @@ -16,14 +16,14 @@ "Folders": "Dossiers", "Genres": "Genres", "HeaderAlbumArtists": "Artistes de l'album", - "HeaderContinueWatching": "Reprendre le visionnage", + "HeaderContinueWatching": "Continuer de regarder", "HeaderFavoriteAlbums": "Albums favoris", "HeaderFavoriteArtists": "Artistes préférés", "HeaderFavoriteEpisodes": "Épisodes favoris", "HeaderFavoriteShows": "Séries favorites", "HeaderFavoriteSongs": "Chansons préférées", "HeaderLiveTV": "TV en direct", - "HeaderNextUp": "À suivre", + "HeaderNextUp": "Prochain à venir", "HeaderRecordingGroups": "Groupes d'enregistrements", "HomeVideos": "Vidéos personnelles", "Inherit": "Hériter", @@ -71,7 +71,7 @@ "ScheduledTaskStartedWithName": "{0} a démarré", "ServerNameNeedsToBeRestarted": "{0} doit être redémarré", "Shows": "Séries", - "Songs": "Titres", + "Songs": "Chansons", "StartupEmbyServerIsLoading": "Le serveur Jellyfin est en cours de chargement. Veuillez réessayer dans quelques instants.", "SubtitleDownloadFailureForItem": "Le téléchargement des sous-titres pour {0} a échoué.", "SubtitleDownloadFailureFromForItem": "Échec du téléchargement des sous-titres depuis {0} pour {1}", @@ -122,7 +122,9 @@ "TaskOptimizeDatabaseDescription": "Réduit les espaces vides ou inutiles et compacte la base de données. Utiliser cette fonction après une mise à jour de la médiathèque ou toute autre modification de la base de données peut améliorer les performances du serveur.", "TaskOptimizeDatabase": "Optimiser la base de données", "TaskKeyframeExtractorDescription": "Extrait les images clés des fichiers vidéo pour créer des listes de lecture HLS plus précises. Cette tâche peut durer très longtemps.", - "TaskKeyframeExtractor": "Extracteur d'image clé", + "TaskKeyframeExtractor": "Extracteur d'images clés", "External": "Externe", - "HearingImpaired": "Malentendants" + "HearingImpaired": "Malentendants", + "TaskRefreshTrickplayImages": "Générer des images Trickplay", + "TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées." } From 2549b544a743056ef75587e2d924435ea079af3c Mon Sep 17 00:00:00 2001 From: Yaron Shahrabani <sh.yaron@gmail.com> Date: Mon, 30 Oct 2023 20:14:03 +0000 Subject: [PATCH 782/858] Translated using Weblate (Hebrew) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/he/ --- Emby.Server.Implementations/Localization/Core/he.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json index 68e9fe8339..26eab392e7 100644 --- a/Emby.Server.Implementations/Localization/Core/he.json +++ b/Emby.Server.Implementations/Localization/Core/he.json @@ -124,5 +124,7 @@ "TaskKeyframeExtractorDescription": "חלץ תמונות מפתח מקבצי וידאו בכדי ליצור רשימות השמעה מדויקות יותר של HLS. משימה זו עלולה להימשך זמן רב.", "TaskKeyframeExtractor": "מחלץ תמונות מפתח", "External": "חיצוני", - "HearingImpaired": "לקוי שמיעה" + "HearingImpaired": "לקוי שמיעה", + "TaskRefreshTrickplayImages": "יצירת תמונות המחשה", + "TaskRefreshTrickplayImagesDescription": "יוצר תמונות המחשה לסרטונים שפעילים בספריות." } From d964befc48357b201ce8301a03c0813bdbbd85a5 Mon Sep 17 00:00:00 2001 From: felix920506 <felix920506@gmail.com> Date: Tue, 31 Oct 2023 22:14:21 +0000 Subject: [PATCH 783/858] Translated using Weblate (Chinese (Traditional)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant/ --- Emby.Server.Implementations/Localization/Core/zh-TW.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json index 36f4df93d1..d57a2811d1 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-TW.json +++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json @@ -123,5 +123,7 @@ "TaskKeyframeExtractorDescription": "將關鍵幀從影片檔案提取出來並建立更精準的HLS播放清單。這可能需要很長時間。", "TaskKeyframeExtractor": "關鍵幀提取器", "External": "外部", - "HearingImpaired": "聽力障礙" + "HearingImpaired": "聽力障礙", + "TaskRefreshTrickplayImages": "生成快轉縮圖", + "TaskRefreshTrickplayImagesDescription": "為啟用此設定的媒體庫生成快轉縮圖。" } From e0adabe3185d186650f887498073976ac3c2a1df Mon Sep 17 00:00:00 2001 From: INOUE Daisuke <inoue.daisuke@gmail.com> Date: Wed, 1 Nov 2023 03:05:18 +0000 Subject: [PATCH 784/858] Translated using Weblate (Japanese) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ja/ --- .../Localization/Core/ja.json | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/ja.json b/Emby.Server.Implementations/Localization/Core/ja.json index db6116080b..ab6988006a 100644 --- a/Emby.Server.Implementations/Localization/Core/ja.json +++ b/Emby.Server.Implementations/Localization/Core/ja.json @@ -27,8 +27,8 @@ "HeaderRecordingGroups": "レコーディンググループ", "HomeVideos": "ホームビデオ", "Inherit": "継承", - "ItemAddedWithName": "{0} をライブラリに追加しました", - "ItemRemovedWithName": "{0} をライブラリから削除しました", + "ItemAddedWithName": "{0} をライブラリーに追加しました", + "ItemRemovedWithName": "{0} をライブラリーから削除しました", "LabelIpAddressValue": "IPアドレス: {0}", "LabelRunningTimeValue": "時間: {0}", "Latest": "最新", @@ -88,18 +88,18 @@ "UserPolicyUpdatedWithName": "ユーザーポリシーが{0}に更新されました", "UserStartedPlayingItemWithValues": "{0} は {2}で{1} を再生しています", "UserStoppedPlayingItemWithValues": "{0} は{2}で{1} の再生が終わりました", - "ValueHasBeenAddedToLibrary": "{0}はあなたのメディアライブラリに追加されました", + "ValueHasBeenAddedToLibrary": "{0} をメディアライブラリーに追加しました", "ValueSpecialEpisodeName": "スペシャル - {0}", "VersionNumber": "バージョン {0}", "TaskCleanLogsDescription": "{0} 日以上前のログを消去します。", "TaskCleanLogs": "ログの掃除", - "TaskRefreshLibraryDescription": "メディアライブラリをスキャンして新しいファイルを探し、メタデータを更新します。", - "TaskRefreshLibrary": "メディアライブラリのスキャン", + "TaskRefreshLibraryDescription": "メディアライブラリーをスキャンして、新しいファイルを探し、メタデータを更新します。", + "TaskRefreshLibrary": "メディアライブラリーをスキャン", "TaskCleanCacheDescription": "不要なキャッシュを消去します。", "TaskCleanCache": "キャッシュを消去", "TasksChannelsCategory": "ネットチャンネル", "TasksApplicationCategory": "アプリケーション", - "TasksLibraryCategory": "ライブラリ", + "TasksLibraryCategory": "ライブラリー", "TasksMaintenanceCategory": "メンテナンス", "TaskRefreshChannelsDescription": "ネットチャンネルの情報を更新する。", "TaskRefreshChannels": "チャンネルの更新", @@ -107,7 +107,7 @@ "TaskCleanTranscode": "トランスコードディレクトリの削除", "TaskUpdatePluginsDescription": "自動更新可能なプラグインのアップデートをダウンロードしてインストールします。", "TaskUpdatePlugins": "プラグインの更新", - "TaskRefreshPeopleDescription": "メディアライブラリで俳優や監督のメタデータを更新します。", + "TaskRefreshPeopleDescription": "メディアライブラリー内の俳優や監督のメタデータを更新します。", "TaskRefreshPeople": "俳優や監督のデータの更新", "TaskDownloadMissingSubtitlesDescription": "メタデータ構成に基づいて、欠落している字幕をインターネットで検索する。", "TaskRefreshChapterImagesDescription": "チャプターのあるビデオのサムネイルを作成します。", @@ -118,10 +118,12 @@ "Undefined": "未定義", "Forced": "強制", "Default": "デフォルト", - "TaskOptimizeDatabaseDescription": "データベースをコンパクトにして、空き領域を切り詰めます。メディアライブラリのスキャン後でこのタスクを実行するとパフォーマンスが向上する可能性があります。", + "TaskOptimizeDatabaseDescription": "データベースをコンパクトにして、空き領域を切り詰めます。メディアライブラリーのスキャンやその他のデータベースの更新を伴う変更の後でこのタスクを実行すると、パフォーマンスが向上します。", "TaskOptimizeDatabase": "データベースの最適化", "TaskKeyframeExtractorDescription": "より正確なHLSプレイリストを作成するため、動画ファイルからキーフレームを抽出する。この処理には時間がかかる場合があります。", "TaskKeyframeExtractor": "キーフレーム抽出", "External": "外部", - "HearingImpaired": "聴覚障害の方" + "HearingImpaired": "聴覚障害の方", + "TaskRefreshTrickplayImages": "トリックプレー画像を生成", + "TaskRefreshTrickplayImagesDescription": "有効なライブラリ内のビデオをもとにトリックプレーのプレビューを生成します。" } From b40ed87cdca9f3e6a1aa592574c84c7731db320c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Lopes?= <tomglopes@gmail.com> Date: Tue, 31 Oct 2023 00:28:59 +0000 Subject: [PATCH 785/858] Translated using Weblate (Portuguese) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt/ --- Emby.Server.Implementations/Localization/Core/pt.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json index 2281e80c8a..103393a1e4 100644 --- a/Emby.Server.Implementations/Localization/Core/pt.json +++ b/Emby.Server.Implementations/Localization/Core/pt.json @@ -92,7 +92,7 @@ "Application": "Aplicação", "AppDeviceValues": "Aplicação: {0}, Dispositivo: {1}", "TaskCleanCache": "Limpar Diretório de Cache", - "TasksApplicationCategory": "Aplicativo", + "TasksApplicationCategory": "Aplicação", "TasksLibraryCategory": "Biblioteca", "TasksMaintenanceCategory": "Manutenção", "TaskRefreshChannels": "Atualizar Canais", @@ -123,5 +123,7 @@ "External": "Externo", "HearingImpaired": "Problemas auditivos", "TaskKeyframeExtractor": "Extrator de quadro-chave", - "TaskKeyframeExtractorDescription": "Retira frames chave do video para criar listas HLS precisas. Esta tarefa pode correr durante algum tempo." + "TaskKeyframeExtractorDescription": "Retira frames chave do video para criar listas HLS precisas. Esta tarefa pode correr durante algum tempo.", + "TaskRefreshTrickplayImages": "Gerar miniaturas de vídeo", + "TaskRefreshTrickplayImagesDescription": "Cria miniaturas de vídeo para vídeos nas bibliotecas definidas." } From c0283a40e2293ac77a684445856333bc4290a9e5 Mon Sep 17 00:00:00 2001 From: Patrick Mondarte <pj.mondarte@gmail.com> Date: Wed, 1 Nov 2023 11:36:27 +0000 Subject: [PATCH 786/858] Translated using Weblate (Filipino) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fil/ --- Emby.Server.Implementations/Localization/Core/fil.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/fil.json b/Emby.Server.Implementations/Localization/Core/fil.json index 01b3e95fc3..88a4a358e2 100644 --- a/Emby.Server.Implementations/Localization/Core/fil.json +++ b/Emby.Server.Implementations/Localization/Core/fil.json @@ -123,5 +123,6 @@ "HearingImpaired": "Bingi", "TaskKeyframeExtractor": "Tagabunot ng Keyframe", "TaskKeyframeExtractorDescription": "Nagbubunot ng keyframe mula sa mga bidyo upang makabuo ng mas tumpak na HLS playlist. Maaaring matagal ito gawin.", - "External": "External" + "External": "External", + "TaskRefreshTrickplayImages": "Gumawa ng Trickplay na Imahe" } From e383cd4cd5543c62fa77f3f68604a53a40654881 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 2 Nov 2023 22:55:42 +0000 Subject: [PATCH 787/858] chore(deps): update dependency xunit to v2.6.1 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 11301ca453..1ddbf14be9 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -86,6 +86,6 @@ <PackageVersion Include="Xunit.Priority" Version="1.1.6" /> <PackageVersion Include="xunit.runner.visualstudio" Version="2.5.3" /> <PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" /> - <PackageVersion Include="xunit" Version="2.6.0" /> + <PackageVersion Include="xunit" Version="2.6.1" /> </ItemGroup> </Project> \ No newline at end of file From 6392a8037f9be3526c350f61372c8fe17ab96937 Mon Sep 17 00:00:00 2001 From: Zan <mestermc594@gmail.com> Date: Fri, 3 Nov 2023 15:00:45 +0000 Subject: [PATCH 788/858] Translated using Weblate (Hungarian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hu/ --- Emby.Server.Implementations/Localization/Core/hu.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json index 5a4a02d80d..ba3d5872ad 100644 --- a/Emby.Server.Implementations/Localization/Core/hu.json +++ b/Emby.Server.Implementations/Localization/Core/hu.json @@ -124,5 +124,7 @@ "TaskKeyframeExtractor": "Kulcsképkockák kibontása", "TaskKeyframeExtractorDescription": "Kibontja a kulcsképkockákat a videófájlokból, hogy pontosabb HLS lejátszási listákat hozzon létre. Ez a feladat hosszú ideig tarthat.", "External": "Külső", - "HearingImpaired": "Hallássérült" + "HearingImpaired": "Hallássérült", + "TaskRefreshTrickplayImages": "Trickplay képek generálása", + "TaskRefreshTrickplayImagesDescription": "Trickplay előnézetet készít az engedélyezett könyvtárakban lévő videókhoz." } From a9ef103c95a7460031879726f4afda3013ca6619 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Sun, 5 Nov 2023 02:01:14 +0100 Subject: [PATCH 789/858] Add IDisposableAnalyzers to more projects --- Emby.Dlna/Emby.Dlna.csproj | 6 +++++- Emby.Dlna/PlayTo/Device.cs | 5 ++--- Emby.Dlna/PlayTo/PlayToController.cs | 13 ++++++------ Emby.Naming/Emby.Naming.csproj | 6 +++++- Emby.Photos/Emby.Photos.csproj | 8 +++++-- .../Emby.Server.Implementations.csproj | 7 ++++++- Jellyfin.Api/Jellyfin.Api.csproj | 6 +++++- .../ActivityLogWebSocketListener.cs | 5 ++++- .../ScheduledTasksWebSocketListener.cs | 7 +++++-- .../SessionInfoWebSocketListener.cs | 17 ++++++++------- Jellyfin.Data/Jellyfin.Data.csproj | 6 +++++- .../Jellyfin.Networking.csproj | 6 +++++- .../Jellyfin.Server.Implementations.csproj | 2 +- Jellyfin.Server/Jellyfin.Server.csproj | 6 +++++- .../MediaBrowser.Common.csproj | 6 +++++- .../MediaBrowser.Controller.csproj | 6 +++++- .../MediaBrowser.LocalMetadata.csproj | 6 +++++- .../MediaBrowser.MediaEncoding.csproj | 6 +++++- .../Subtitles/SubtitleEncoder.cs | 8 +++---- MediaBrowser.Model/MediaBrowser.Model.csproj | 7 ++++++- .../Manager/ProviderManager.cs | 10 ++++----- .../MediaBrowser.Providers.csproj | 6 +++++- .../MediaBrowser.XbmcMetadata.csproj | 4 ++++ src/Directory.Build.props | 21 +++++++++++++++++++ .../Jellyfin.Drawing.Skia.csproj | 15 ------------- src/Jellyfin.Drawing/Jellyfin.Drawing.csproj | 15 ------------- .../Jellyfin.Extensions.csproj | 16 -------------- .../Jellyfin.MediaEncoding.Hls.csproj | 15 ------------- .../Jellyfin.MediaEncoding.Keyframes.csproj | 15 ------------- 29 files changed, 134 insertions(+), 122 deletions(-) create mode 100644 src/Directory.Build.props diff --git a/Emby.Dlna/Emby.Dlna.csproj b/Emby.Dlna/Emby.Dlna.csproj index aca2399644..efbef05640 100644 --- a/Emby.Dlna/Emby.Dlna.csproj +++ b/Emby.Dlna/Emby.Dlna.csproj @@ -26,8 +26,12 @@ <CodeAnalysisTreatWarningsAsErrors>false</CodeAnalysisTreatWarningsAsErrors> </PropertyGroup> - <!-- Code Analyzers--> + <!-- Code Analyzers --> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="IDisposableAnalyzers"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> + </PackageReference> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs index bb9b8b0fdc..18fa196508 100644 --- a/Emby.Dlna/PlayTo/Device.cs +++ b/Emby.Dlna/PlayTo/Device.cs @@ -1248,11 +1248,10 @@ namespace Emby.Dlna.PlayTo if (disposing) { _timer?.Dispose(); + _timer = null; + Properties = null!; } - _timer = null; - Properties = null!; - _disposed = true; } diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs index b1ad15cdc9..df02fe631e 100644 --- a/Emby.Dlna/PlayTo/PlayToController.cs +++ b/Emby.Dlna/PlayTo/PlayToController.cs @@ -683,16 +683,15 @@ namespace Emby.Dlna.PlayTo if (disposing) { + _device.PlaybackStart -= OnDevicePlaybackStart; + _device.PlaybackProgress -= OnDevicePlaybackProgress; + _device.PlaybackStopped -= OnDevicePlaybackStopped; + _device.MediaChanged -= OnDeviceMediaChanged; + _deviceDiscovery.DeviceLeft -= OnDeviceDiscoveryDeviceLeft; + _device.OnDeviceUnavailable = null; _device.Dispose(); } - _device.PlaybackStart -= OnDevicePlaybackStart; - _device.PlaybackProgress -= OnDevicePlaybackProgress; - _device.PlaybackStopped -= OnDevicePlaybackStopped; - _device.MediaChanged -= OnDeviceMediaChanged; - _deviceDiscovery.DeviceLeft -= OnDeviceDiscoveryDeviceLeft; - _device.OnDeviceUnavailable = null; - _disposed = true; } diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj index f3973dad95..bc7548189b 100644 --- a/Emby.Naming/Emby.Naming.csproj +++ b/Emby.Naming/Emby.Naming.csproj @@ -45,8 +45,12 @@ <PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" /> </ItemGroup> - <!-- Code Analyzers--> + <!-- Code Analyzers --> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="IDisposableAnalyzers"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> + </PackageReference> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> diff --git a/Emby.Photos/Emby.Photos.csproj b/Emby.Photos/Emby.Photos.csproj index 0f97a06867..5a04bbe49b 100644 --- a/Emby.Photos/Emby.Photos.csproj +++ b/Emby.Photos/Emby.Photos.csproj @@ -24,14 +24,18 @@ <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> - <!-- Code Analyzers--> + <!-- Code Analyzers --> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="IDisposableAnalyzers"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> + </PackageReference> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> - <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" /> </ItemGroup> diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 80263c1394..b48e389ace 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -49,8 +49,13 @@ <CodeAnalysisTreatWarningsAsErrors>false</CodeAnalysisTreatWarningsAsErrors> </PropertyGroup> - <!-- Code Analyzers--> + <!-- Code Analyzers --> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <!-- TODO: Add IDisposableAnalyzers --> + <!-- <PackageReference Include="IDisposableAnalyzers"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> + </PackageReference> --> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index 7ac231885e..03dd97367f 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -24,8 +24,12 @@ <ProjectReference Include="..\src\Jellyfin.MediaEncoding.Hls\Jellyfin.MediaEncoding.Hls.csproj" /> </ItemGroup> - <!-- Code Analyzers--> + <!-- Code Analyzers --> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="IDisposableAnalyzers"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> + </PackageReference> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> diff --git a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs index 5b90d65d84..ba228cb002 100644 --- a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs +++ b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs @@ -53,7 +53,10 @@ public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<Activi /// <inheritdoc /> protected override void Dispose(bool dispose) { - _activityManager.EntryCreated -= OnEntryCreated; + if (dispose) + { + _activityManager.EntryCreated -= OnEntryCreated; + } base.Dispose(dispose); } diff --git a/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs index a9df2d6712..37c108d5a6 100644 --- a/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs +++ b/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs @@ -58,8 +58,11 @@ public class ScheduledTasksWebSocketListener : BasePeriodicWebSocketListener<IEn /// <inheritdoc /> protected override void Dispose(bool dispose) { - _taskManager.TaskExecuting -= OnTaskExecuting; - _taskManager.TaskCompleted -= OnTaskCompleted; + if (dispose) + { + _taskManager.TaskExecuting -= OnTaskExecuting; + _taskManager.TaskCompleted -= OnTaskCompleted; + } base.Dispose(dispose); } diff --git a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs index b403ff46d0..3c2b86142e 100644 --- a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs +++ b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs @@ -57,13 +57,16 @@ public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnume /// <inheritdoc /> protected override void Dispose(bool dispose) { - _sessionManager.SessionStarted -= OnSessionManagerSessionStarted; - _sessionManager.SessionEnded -= OnSessionManagerSessionEnded; - _sessionManager.PlaybackStart -= OnSessionManagerPlaybackStart; - _sessionManager.PlaybackStopped -= OnSessionManagerPlaybackStopped; - _sessionManager.PlaybackProgress -= OnSessionManagerPlaybackProgress; - _sessionManager.CapabilitiesChanged -= OnSessionManagerCapabilitiesChanged; - _sessionManager.SessionActivity -= OnSessionManagerSessionActivity; + if (dispose) + { + _sessionManager.SessionStarted -= OnSessionManagerSessionStarted; + _sessionManager.SessionEnded -= OnSessionManagerSessionEnded; + _sessionManager.PlaybackStart -= OnSessionManagerPlaybackStart; + _sessionManager.PlaybackStopped -= OnSessionManagerPlaybackStopped; + _sessionManager.PlaybackProgress -= OnSessionManagerPlaybackProgress; + _sessionManager.CapabilitiesChanged -= OnSessionManagerCapabilitiesChanged; + _sessionManager.SessionActivity -= OnSessionManagerSessionActivity; + } base.Dispose(dispose); } diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj index 1bc5d8bf91..847853ca00 100644 --- a/Jellyfin.Data/Jellyfin.Data.csproj +++ b/Jellyfin.Data/Jellyfin.Data.csproj @@ -27,8 +27,12 @@ <PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" /> </ItemGroup> - <!-- Code analysers--> + <!-- Code Analyzers --> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="IDisposableAnalyzers"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> + </PackageReference> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> diff --git a/Jellyfin.Networking/Jellyfin.Networking.csproj b/Jellyfin.Networking/Jellyfin.Networking.csproj index 4cff5927fd..43d08c37a1 100644 --- a/Jellyfin.Networking/Jellyfin.Networking.csproj +++ b/Jellyfin.Networking/Jellyfin.Networking.csproj @@ -9,8 +9,12 @@ <Compile Include="..\SharedVersion.cs" /> </ItemGroup> - <!-- Code Analyzers--> + <!-- Code Analyzers --> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="IDisposableAnalyzers"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> + </PackageReference> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index fa6adb0220..df1d5a3e18 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -6,7 +6,7 @@ <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> - <!-- Code analysers--> + <!-- Code Analyzers --> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> <PackageReference Include="IDisposableAnalyzers"> <PrivateAssets>all</PrivateAssets> diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 62abb89355..5479d22965 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -22,8 +22,12 @@ <EmbeddedResource Include="Resources/Configuration/*" /> </ItemGroup> - <!-- Code Analyzers--> + <!-- Code Analyzers --> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="IDisposableAnalyzers"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> + </PackageReference> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index 7015d991fd..7d0d7a173b 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -43,8 +43,12 @@ <AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder> </PropertyGroup> - <!-- Code analyzers--> + <!-- Code Analyzers --> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="IDisposableAnalyzers"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> + </PackageReference> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 69c0d26b6b..f9468f6cdb 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -49,8 +49,12 @@ <AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder> </PropertyGroup> - <!-- Code Analyzers--> + <!-- Code Analyzers --> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="IDisposableAnalyzers"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> + </PackageReference> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> diff --git a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj index 71cdea5298..a39bc238a7 100644 --- a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj +++ b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj @@ -20,8 +20,12 @@ <Compile Include="..\SharedVersion.cs" /> </ItemGroup> - <!-- Code Analyzers--> + <!-- Code Analyzers --> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="IDisposableAnalyzers"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> + </PackageReference> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj index a0624fe76b..1f39e88cde 100644 --- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj +++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj @@ -29,8 +29,12 @@ <PackageReference Include="UTF.Unknown" /> </ItemGroup> - <!-- Code Analyzers--> + <!-- Code Analyzers --> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="IDisposableAnalyzers"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> + </PackageReference> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 21fa4468ed..8eea773d86 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -135,19 +135,17 @@ namespace MediaBrowser.MediaEncoding.Subtitles var subtitleStream = mediaSource.MediaStreams .First(i => i.Type == MediaStreamType.Subtitle && i.Index == subtitleStreamIndex); - var subtitle = await GetSubtitleStream(mediaSource, subtitleStream, cancellationToken) + var (stream, inputFormat) = await GetSubtitleStream(mediaSource, subtitleStream, cancellationToken) .ConfigureAwait(false); - var inputFormat = subtitle.Format; - // Return the original if the same format is being requested // Character encoding was already handled in GetSubtitleStream if (string.Equals(inputFormat, outputFormat, StringComparison.OrdinalIgnoreCase)) { - return subtitle.Stream; + return stream; } - using (var stream = subtitle.Stream) + using (stream) { return ConvertSubtitles(stream, inputFormat, outputFormat, startTimeTicks, endTimeTicks, preserveOriginalTimestamps, cancellationToken); } diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 58ba83a35f..75c5bc6f00 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -48,8 +48,12 @@ <Compile Include="..\SharedVersion.cs" /> </ItemGroup> - <!-- Code Analyzers--> + <!-- Code Analyzers --> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="IDisposableAnalyzers"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> + </PackageReference> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> @@ -58,6 +62,7 @@ <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" /> </ItemGroup> + <ItemGroup> <ProjectReference Include="../Jellyfin.Data/Jellyfin.Data.csproj" /> <ProjectReference Include="../src/Jellyfin.Extensions/Jellyfin.Extensions.csproj" /> diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index d0bb34d52a..4ba8844182 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -1096,13 +1096,13 @@ namespace MediaBrowser.Providers.Manager return; } - if (!_disposeCancellationTokenSource.IsCancellationRequested) - { - _disposeCancellationTokenSource.Cancel(); - } - if (disposing) { + if (!_disposeCancellationTokenSource.IsCancellationRequested) + { + _disposeCancellationTokenSource.Cancel(); + } + _disposeCancellationTokenSource.Dispose(); } diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 7ef70f4b08..8471f6fa10 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -33,8 +33,12 @@ <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> </PropertyGroup> - <!-- Code Analyzers--> + <!-- Code Analyzers --> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="IDisposableAnalyzers"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> + </PackageReference> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> diff --git a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj index 8072349152..d7e34fd226 100644 --- a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj +++ b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj @@ -22,6 +22,10 @@ <!-- Code Analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="IDisposableAnalyzers"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> + </PackageReference> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 0000000000..ac2726ed58 --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,21 @@ +<Project> + <!-- Sets defaults for all projects --> + + <Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" /> + + <!-- Code Analyzers --> + <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="IDisposableAnalyzers"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> + </PackageReference> + <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> + </PackageReference> + <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" /> + <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" /> + </ItemGroup> + +</Project> diff --git a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj index 09e103c6b5..3c417f8ff0 100644 --- a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj +++ b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj @@ -31,19 +31,4 @@ <ProjectReference Include="..\..\MediaBrowser.Common\MediaBrowser.Common.csproj" /> </ItemGroup> - <!-- Code Analyzers--> - <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="IDisposableAnalyzers"> - <PrivateAssets>all</PrivateAssets> - <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> - </PackageReference> - <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers"> - <PrivateAssets>all</PrivateAssets> - <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> - </PackageReference> - <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" /> - <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" /> - </ItemGroup> - </Project> diff --git a/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj index 2a5e24a449..d7ef6f8e77 100644 --- a/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj +++ b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj @@ -21,19 +21,4 @@ <Compile Include="..\..\SharedVersion.cs" /> </ItemGroup> - <!-- Code Analyzers--> - <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="IDisposableAnalyzers"> - <PrivateAssets>all</PrivateAssets> - <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> - </PackageReference> - <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers"> - <PrivateAssets>all</PrivateAssets> - <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> - </PackageReference> - <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" /> - <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" /> - </ItemGroup> - </Project> diff --git a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj index 36ae55ed2e..997df6dbed 100644 --- a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj +++ b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj @@ -27,24 +27,8 @@ <Compile Include="../../SharedVersion.cs" /> </ItemGroup> - <ItemGroup> <PackageReference Include="Diacritics" /> </ItemGroup> - <!-- Code Analyzers--> - <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="IDisposableAnalyzers"> - <PrivateAssets>all</PrivateAssets> - <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> - </PackageReference> - <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers"> - <PrivateAssets>all</PrivateAssets> - <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> - </PackageReference> - <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" /> - <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" /> - </ItemGroup> - </Project> diff --git a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj index b792e7ec64..76dde1cf6a 100644 --- a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj +++ b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj @@ -5,21 +5,6 @@ <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> - <!-- Code Analyzers--> - <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="IDisposableAnalyzers"> - <PrivateAssets>all</PrivateAssets> - <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> - </PackageReference> - <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers"> - <PrivateAssets>all</PrivateAssets> - <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> - </PackageReference> - <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" /> - <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" /> - </ItemGroup> - <ItemGroup> <ProjectReference Include="../../MediaBrowser.Common/MediaBrowser.Common.csproj" /> <ProjectReference Include="../../MediaBrowser.Controller/MediaBrowser.Controller.csproj" /> diff --git a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj index 09b1f8faa0..0d91a447bc 100644 --- a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj +++ b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj @@ -9,21 +9,6 @@ <PackageReference Include="NEbml" /> </ItemGroup> - <!-- Code Analyzers--> - <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="IDisposableAnalyzers"> - <PrivateAssets>all</PrivateAssets> - <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> - </PackageReference> - <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers"> - <PrivateAssets>all</PrivateAssets> - <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> - </PackageReference> - <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" /> - <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" /> - </ItemGroup> - <ItemGroup> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" /> </ItemGroup> From 9d4352789d6ff7a59cdda37e3bbb02aa740ed99f Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Mon, 6 Nov 2023 15:17:47 -0500 Subject: [PATCH 790/858] Backport pull request #10454 from jellyfin/release-10.8.z Add MALLOC_TRIM_THRESHOLD_ to default ENV Original-merge: 9d565bbb83ef5757fd995a5932422b740dbc6c93 Merged-by: Joshua M. Boniface <joshua@boniface.me> Backported-by: Joshua M. Boniface <joshua@boniface.me> --- debian/conf/jellyfin | 3 +++ fedora/jellyfin.env | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/debian/conf/jellyfin b/debian/conf/jellyfin index aec1d4d103..af460fedcf 100644 --- a/debian/conf/jellyfin +++ b/debian/conf/jellyfin @@ -24,6 +24,9 @@ JELLYFIN_WEB_OPT="--webdir=/usr/share/jellyfin/web" # ffmpeg binary paths, overriding the system values JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/lib/jellyfin-ffmpeg/ffmpeg" +# Disable glibc dynamic heap adjustment +MALLOC_TRIM_THRESHOLD_=131072 + # [OPTIONAL] run Jellyfin as a headless service #JELLYFIN_SERVICE_OPT="--service" diff --git a/fedora/jellyfin.env b/fedora/jellyfin.env index 1f79fac4f6..cee8f68544 100644 --- a/fedora/jellyfin.env +++ b/fedora/jellyfin.env @@ -23,6 +23,12 @@ JELLYFIN_CACHE_DIR="/var/cache/jellyfin" # web client path, installed by the jellyfin-web package # JELLYFIN_WEB_OPT="--webdir=/usr/share/jellyfin-web" +# In-App service control +JELLYFIN_RESTART_OPT="--restartpath=/usr/libexec/jellyfin/restart.sh" + +# Disable glibc dynamic heap adjustment +MALLOC_TRIM_THRESHOLD_=131072 + # [OPTIONAL] ffmpeg binary paths, overriding the UI-configured values #JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/bin/ffmpeg" From 7a3c89087f2117180cfd75d07f724ca22659e5c0 Mon Sep 17 00:00:00 2001 From: Justin Sleep <justin@midnightmechanism.com> Date: Tue, 7 Nov 2023 22:43:40 -0600 Subject: [PATCH 791/858] Revert "Update SkiaSharp to v2.88.5" This reverts commit 0be06f843b6d07f836e3bac390238b8eb5c8bbb8. --- Directory.Packages.props | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 5c1cd989eb..1f0dae6649 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -67,9 +67,9 @@ <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.0" /> <PackageVersion Include="SerilogAnalyzer" Version="0.15.0" /> <PackageVersion Include="SharpFuzz" Version="2.1.1" /> - <PackageVersion Include="SkiaSharp" Version="2.88.6" /> - <PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.6" /> - <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.6" /> + <PackageVersion Include="SkiaSharp" Version="2.88.5" /> + <PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.5" /> + <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.5" /> <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.507" /> <PackageVersion Include="Svg.Skia" Version="1.0.0.2" /> @@ -88,4 +88,4 @@ <PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" /> <PackageVersion Include="xunit" Version="2.6.1" /> </ItemGroup> -</Project> +</Project> \ No newline at end of file From dd6bda30e540a76e54d55e951036304bd67c9596 Mon Sep 17 00:00:00 2001 From: Justin Sleep <justin@midnightmechanism.com> Date: Tue, 7 Nov 2023 22:43:45 -0600 Subject: [PATCH 792/858] Revert "Update BlurHashSharp" This reverts commit 5e19459f943263253761d2b90d216dc8b29d667d. --- Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 1f0dae6649..1ddbf14be9 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,8 +8,8 @@ <PackageVersion Include="AutoFixture.Xunit2" Version="4.18.0" /> <PackageVersion Include="AutoFixture" Version="4.18.0" /> <PackageVersion Include="BDInfo" Version="0.7.6.2" /> - <PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.3.1" /> - <PackageVersion Include="BlurHashSharp" Version="1.3.1" /> + <PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.3.0" /> + <PackageVersion Include="BlurHashSharp" Version="1.3.0" /> <PackageVersion Include="CommandLineParser" Version="2.9.1" /> <PackageVersion Include="coverlet.collector" Version="6.0.0" /> <PackageVersion Include="Diacritics" Version="3.3.18" /> From b314b0b2675a8086ccb37a282880af13ec83fbce Mon Sep 17 00:00:00 2001 From: Vesel Karastoyanov <veselk@gmail.com> Date: Tue, 7 Nov 2023 06:39:09 +0000 Subject: [PATCH 793/858] Translated using Weblate (Bulgarian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/bg/ --- Emby.Server.Implementations/Localization/Core/bg-BG.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/bg-BG.json b/Emby.Server.Implementations/Localization/Core/bg-BG.json index 13b99cc997..e1cf1448be 100644 --- a/Emby.Server.Implementations/Localization/Core/bg-BG.json +++ b/Emby.Server.Implementations/Localization/Core/bg-BG.json @@ -124,5 +124,6 @@ "TaskKeyframeExtractorDescription": "Извличат се ключови кадри от видеофайловете ,за да се създаде по точен ХЛС списък . Задачата може да отнеме много време.", "TaskKeyframeExtractor": "Извличане на ключови кадри", "External": "Външен", - "HearingImpaired": "Увреден слух" + "HearingImpaired": "Увреден слух", + "TaskRefreshTrickplayImages": "Генерирай изображение" } From 501d7eed47fa87d04840eba5da3612a3b0a3a9e6 Mon Sep 17 00:00:00 2001 From: Retrial <giwrgosmant@gmail.com> Date: Mon, 6 Nov 2023 12:56:01 +0000 Subject: [PATCH 794/858] Translated using Weblate (Greek) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/el/ --- Emby.Server.Implementations/Localization/Core/el.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json index c6e2244cae..5ea6a22527 100644 --- a/Emby.Server.Implementations/Localization/Core/el.json +++ b/Emby.Server.Implementations/Localization/Core/el.json @@ -124,5 +124,7 @@ "TaskKeyframeExtractorDescription": "Εξάγει καρέ από αρχεία βίντεο για να δημιουργήσει πιο ακριβείς λίστες αναπαραγωγής HLS. Αυτή η διεργασία μπορεί να πάρει χρόνο.", "TaskKeyframeExtractor": "Εξαγωγέας βασικών καρέ βίντεο", "External": "Εξωτερικό", - "HearingImpaired": "Με προβλήματα ακοής" + "HearingImpaired": "Με προβλήματα ακοής", + "TaskRefreshTrickplayImages": "Δημιουργήστε εικόνες Trickplay", + "TaskRefreshTrickplayImagesDescription": "Δημιουργεί προεπισκοπήσεις trickplay για βίντεο σε ενεργοποιημένες βιβλιοθήκες." } From d542a3ab627d2ce33d7402b9af1a89b062fb5fb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luka=20Ili=C4=87?= <lukailic100@gmail.com> Date: Wed, 8 Nov 2023 08:08:23 +0000 Subject: [PATCH 795/858] Translated using Weblate (Croatian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hr/ --- Emby.Server.Implementations/Localization/Core/hr.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json index d01295419e..5bb2b7d4db 100644 --- a/Emby.Server.Implementations/Localization/Core/hr.json +++ b/Emby.Server.Implementations/Localization/Core/hr.json @@ -124,5 +124,7 @@ "TaskKeyframeExtractorDescription": "Izvlačenje ključnih okvira iz videozapisa za stvaranje objektivnije HLS liste za reprodukciju. Pokretanje ovog zadatka može potrajati.", "TaskKeyframeExtractor": "Izvoditelj ključnog okvira", "TaskOptimizeDatabaseDescription": "Sažima bazu podataka i uklanja prazan prostor. Pokretanje ovog zadatka, može poboljšati performanse nakon provođenja indeksiranja biblioteke ili provođenja drugih promjena koje utječu na bazu podataka.", - "HearingImpaired": "Oštećen sluh" + "HearingImpaired": "Oštećen sluh", + "TaskRefreshTrickplayImages": "Generiraj Trickplay Slike", + "TaskRefreshTrickplayImagesDescription": "Kreira trickplay pretpreglede za videe u omogućenim knjižnicama." } From de16d18289b34679f87045cf37ce1ff499cd6896 Mon Sep 17 00:00:00 2001 From: salvatore rizzu <rizzusalvatore95@gmail.com> Date: Tue, 7 Nov 2023 18:49:32 +0000 Subject: [PATCH 796/858] Translated using Weblate (Italian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/it/ --- Emby.Server.Implementations/Localization/Core/it.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json index 3710f03e07..a34bcc4907 100644 --- a/Emby.Server.Implementations/Localization/Core/it.json +++ b/Emby.Server.Implementations/Localization/Core/it.json @@ -124,5 +124,7 @@ "TaskKeyframeExtractor": "Estrattore di Keyframe", "TaskKeyframeExtractorDescription": "Estrae i keyframe dai video per creare migliori playlist HLS. Questa procedura potrebbe richiedere molto tempo.", "External": "Esterno", - "HearingImpaired": "con problemi di udito" + "HearingImpaired": "con problemi di udito", + "TaskRefreshTrickplayImages": "Genera immagini Trickplay", + "TaskRefreshTrickplayImagesDescription": "Crea anteprime trickplay per i video nelle librerie abilitate." } From c57c5f20228b2ec636ac1a2ff81f5bc78547d530 Mon Sep 17 00:00:00 2001 From: Luis Fonseca <luisfonsecagm1@gmail.com> Date: Wed, 8 Nov 2023 14:16:31 +0000 Subject: [PATCH 797/858] Translated using Weblate (Portuguese (Portugal)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt_PT/ --- Emby.Server.Implementations/Localization/Core/pt-PT.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json index a75182f220..dcfe46efc1 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-PT.json +++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json @@ -124,5 +124,7 @@ "TaskKeyframeExtractorDescription": "Extrai quadros-chave de ficheiros de video para criar listas de reprodução HLS mais precisas. Esta tarefa pode demorar algum tempo.", "TaskKeyframeExtractor": "Extrator de Quadros-chave", "External": "Externo", - "HearingImpaired": "Surdo" + "HearingImpaired": "Surdo", + "TaskRefreshTrickplayImages": "Gerar imagens de truques", + "TaskRefreshTrickplayImagesDescription": "Cria vizualizações de truques para videos nas librarias ativas." } From 9a23515719bb5b6d5d0de545d42f22edc413c23c Mon Sep 17 00:00:00 2001 From: Oskari Lavinto <olavinto@protonmail.com> Date: Mon, 6 Nov 2023 06:30:41 +0000 Subject: [PATCH 798/858] Translated using Weblate (Finnish) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fi/ --- Emby.Server.Implementations/Localization/Core/fi.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json index 08344abeb7..cba036ff47 100644 --- a/Emby.Server.Implementations/Localization/Core/fi.json +++ b/Emby.Server.Implementations/Localization/Core/fi.json @@ -123,5 +123,7 @@ "TaskKeyframeExtractorDescription": "Purkaa videotiedostojen avainkuvat tarkempien HLS-toistolistojen luomiseksi. Tehtävä saattaa kestää huomattavan pitkään.", "TaskKeyframeExtractor": "Avainkuvien purkain", "External": "Ulkoinen", - "HearingImpaired": "Kuulorajoitteinen" + "HearingImpaired": "Kuulorajoitteinen", + "TaskRefreshTrickplayImages": "Luo Trickplay-kuvat", + "TaskRefreshTrickplayImagesDescription": "Luo Trickplay-esikatselut käytössä olevien kirjastojen videoista." } From c2bfa17fcb5ae47f263c0d5512334af2b2f68429 Mon Sep 17 00:00:00 2001 From: Troja <d.iwowi.b@gmail.com> Date: Wed, 8 Nov 2023 09:41:52 +0000 Subject: [PATCH 799/858] Translated using Weblate (Belarusian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/be/ --- Emby.Server.Implementations/Localization/Core/be.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/be.json b/Emby.Server.Implementations/Localization/Core/be.json index 3af124678f..05af8d8a5a 100644 --- a/Emby.Server.Implementations/Localization/Core/be.json +++ b/Emby.Server.Implementations/Localization/Core/be.json @@ -123,5 +123,7 @@ "TaskCleanTranscodeDescription": "Выдаляе перакадзіраваныя файлы, старэйшыя за адзін дзень.", "TaskRefreshChannels": "Абнавіць каналы", "TaskDownloadMissingSubtitles": "Спампаваць адсутныя субтытры", - "TaskKeyframeExtractorDescription": "Выдае ключавыя кадры з відэафайлаў для стварэння больш дакладных спісаў прайгравання HLS. Гэта задача можа працаваць у працягу доўгага часу." + "TaskKeyframeExtractorDescription": "Выдае ключавыя кадры з відэафайлаў для стварэння больш дакладных спісаў прайгравання HLS. Гэта задача можа працаваць у працягу доўгага часу.", + "TaskRefreshTrickplayImages": "Стварыце выявы Trickplay", + "TaskRefreshTrickplayImagesDescription": "Стварае прагляд відэаролікаў для Trickplay у падключаных бібліятэках." } From 25d46724f2aad3c0a17743cab89cbd76f90ca3bb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 9 Nov 2023 07:12:38 +0000 Subject: [PATCH 800/858] chore(deps): update dependency serilog.sinks.console to v5 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 1ddbf14be9..e728182904 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -62,7 +62,7 @@ <PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" /> <PackageVersion Include="Serilog.Settings.Configuration" Version="7.0.1" /> <PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" /> - <PackageVersion Include="Serilog.Sinks.Console" Version="4.1.0" /> + <PackageVersion Include="Serilog.Sinks.Console" Version="5.0.0" /> <PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" /> <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.0" /> <PackageVersion Include="SerilogAnalyzer" Version="0.15.0" /> From c7a94d48ae019f41d5f06340bca7efe0788ad5ad Mon Sep 17 00:00:00 2001 From: Cody Robibero <cody@robibe.ro> Date: Thu, 9 Nov 2023 14:00:13 -0700 Subject: [PATCH 801/858] Convert ItemSortBy to enum (#9765) * Convert ItemSortBy to enum * Rename Unknown to Default --- Emby.Dlna/ContentDirectory/ControlHandler.cs | 18 +- .../Data/SqliteItemRepository.cs | 203 ++++-------------- .../Images/BaseFolderImageProvider.cs | 2 +- .../Library/LibraryManager.cs | 8 +- .../LiveTv/LiveTvManager.cs | 2 +- .../Sorting/AiredEpisodeOrderComparer.cs | 3 +- .../Sorting/AlbumArtistComparer.cs | 3 +- .../Sorting/AlbumComparer.cs | 3 +- .../Sorting/ArtistComparer.cs | 3 +- .../Sorting/CommunityRatingComparer.cs | 3 +- .../Sorting/CriticRatingComparer.cs | 3 +- .../Sorting/DateCreatedComparer.cs | 3 +- .../Sorting/DateLastMediaAddedComparer.cs | 3 +- .../Sorting/DatePlayedComparer.cs | 3 +- .../Sorting/IndexNumberComparer.cs | 3 +- .../Sorting/IsFavoriteOrLikeComparer.cs | 3 +- .../Sorting/IsFolderComparer.cs | 3 +- .../Sorting/IsPlayedComparer.cs | 3 +- .../Sorting/IsUnplayedComparer.cs | 3 +- .../Sorting/NameComparer.cs | 3 +- .../Sorting/OfficialRatingComparer.cs | 3 +- .../Sorting/ParentIndexNumberComparer.cs | 3 +- .../Sorting/PlayCountComparer.cs | 3 +- .../Sorting/PremiereDateComparer.cs | 3 +- .../Sorting/ProductionYearComparer.cs | 3 +- .../Sorting/RandomComparer.cs | 3 +- .../Sorting/RuntimeComparer.cs | 3 +- .../Sorting/SeriesSortNameComparer.cs | 3 +- .../Sorting/SortNameComparer.cs | 3 +- .../Sorting/StartDateComparer.cs | 3 +- .../Sorting/StudioComparer.cs | 3 +- Jellyfin.Api/Controllers/ArtistsController.cs | 4 +- .../Controllers/ChannelsController.cs | 2 +- Jellyfin.Api/Controllers/GenresController.cs | 2 +- Jellyfin.Api/Controllers/ItemsController.cs | 4 +- Jellyfin.Api/Controllers/LiveTvController.cs | 4 +- .../Controllers/MusicGenresController.cs | 2 +- .../Controllers/TrailersController.cs | 2 +- Jellyfin.Api/Controllers/TvShowsController.cs | 4 +- Jellyfin.Api/Controllers/YearsController.cs | 2 +- Jellyfin.Api/Helpers/RequestHelpers.cs | 6 +- .../Models/LiveTvDtos/GetProgramsDto.cs | 2 +- Jellyfin.Data/Enums/ItemSortBy.cs | 167 ++++++++++++++ .../Entities/IHasDisplayOrder.cs | 2 + .../Entities/InternalItemsQuery.cs | 4 +- .../Entities/Movies/BoxSet.cs | 8 +- .../Library/ILibraryManager.cs | 4 +- .../Providers/EpisodeInfo.cs | 1 + .../Sorting/IBaseItemComparer.cs | 6 +- .../Savers/BaseXmlSaver.cs | 1 + .../LiveTv/LiveTvChannelQuery.cs | 4 +- MediaBrowser.Model/Querying/ItemSortBy.cs | 163 -------------- .../Manager/MetadataService.cs | 1 + .../Plugins/Tmdb/TmdbClientManager.cs | 1 + .../Helpers/RequestHelpersTests.cs | 36 ++-- 55 files changed, 328 insertions(+), 415 deletions(-) create mode 100644 Jellyfin.Data/Enums/ItemSortBy.cs delete mode 100644 MediaBrowser.Model/Querying/ItemSortBy.cs diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs index abd594a3a1..e685d252e4 100644 --- a/Emby.Dlna/ContentDirectory/ControlHandler.cs +++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs @@ -917,7 +917,7 @@ namespace Emby.Dlna.ContentDirectory private QueryResult<ServerItem> GetGenres(BaseItem parent, InternalItemsQuery query) { // Don't sort - query.OrderBy = Array.Empty<(string, SortOrder)>(); + query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>(); query.AncestorIds = new[] { parent.Id }; var genresResult = _libraryManager.GetGenres(query); @@ -933,7 +933,7 @@ namespace Emby.Dlna.ContentDirectory private QueryResult<ServerItem> GetMusicGenres(BaseItem parent, InternalItemsQuery query) { // Don't sort - query.OrderBy = Array.Empty<(string, SortOrder)>(); + query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>(); query.AncestorIds = new[] { parent.Id }; var genresResult = _libraryManager.GetMusicGenres(query); @@ -949,7 +949,7 @@ namespace Emby.Dlna.ContentDirectory private QueryResult<ServerItem> GetMusicAlbumArtists(BaseItem parent, InternalItemsQuery query) { // Don't sort - query.OrderBy = Array.Empty<(string, SortOrder)>(); + query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>(); query.AncestorIds = new[] { parent.Id }; var artists = _libraryManager.GetAlbumArtists(query); @@ -965,7 +965,7 @@ namespace Emby.Dlna.ContentDirectory private QueryResult<ServerItem> GetMusicArtists(BaseItem parent, InternalItemsQuery query) { // Don't sort - query.OrderBy = Array.Empty<(string, SortOrder)>(); + query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>(); query.AncestorIds = new[] { parent.Id }; var artists = _libraryManager.GetArtists(query); return ToResult(query.StartIndex, artists); @@ -980,7 +980,7 @@ namespace Emby.Dlna.ContentDirectory private QueryResult<ServerItem> GetFavoriteArtists(BaseItem parent, InternalItemsQuery query) { // Don't sort - query.OrderBy = Array.Empty<(string, SortOrder)>(); + query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>(); query.AncestorIds = new[] { parent.Id }; query.IsFavorite = true; var artists = _libraryManager.GetArtists(query); @@ -1011,7 +1011,7 @@ namespace Emby.Dlna.ContentDirectory /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> private QueryResult<ServerItem> GetNextUp(BaseItem parent, InternalItemsQuery query) { - query.OrderBy = Array.Empty<(string, SortOrder)>(); + query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>(); var result = _tvSeriesManager.GetNextUp( new NextUpQuery @@ -1036,7 +1036,7 @@ namespace Emby.Dlna.ContentDirectory /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns> private QueryResult<ServerItem> GetLatest(BaseItem parent, InternalItemsQuery query, BaseItemKind itemType) { - query.OrderBy = Array.Empty<(string, SortOrder)>(); + query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>(); var items = _userViewManager.GetLatestItems( new LatestItemsQuery @@ -1203,9 +1203,9 @@ namespace Emby.Dlna.ContentDirectory /// </summary> /// <param name="sort">The <see cref="SortCriteria"/>.</param> /// <param name="isPreSorted">True if pre-sorted.</param> - private static (string SortName, SortOrder SortOrder)[] GetOrderBy(SortCriteria sort, bool isPreSorted) + private static (ItemSortBy SortName, SortOrder SortOrder)[] GetOrderBy(SortCriteria sort, bool isPreSorted) { - return isPreSorted ? Array.Empty<(string, SortOrder)>() : new[] { (ItemSortBy.SortName, sort.SortOrder) }; + return isPreSorted ? Array.Empty<(ItemSortBy, SortOrder)>() : new[] { (ItemSortBy.SortName, sort.SortOrder) }; } /// <summary> diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index e519364c22..fadd4f2f38 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -2042,7 +2042,7 @@ namespace Emby.Server.Implementations.Data return false; } - var sortingFields = new HashSet<string>(query.OrderBy.Select(i => i.OrderBy), StringComparer.OrdinalIgnoreCase); + var sortingFields = new HashSet<ItemSortBy>(query.OrderBy.Select(i => i.OrderBy)); return sortingFields.Contains(ItemSortBy.IsFavoriteOrLiked) || sortingFields.Contains(ItemSortBy.IsPlayed) @@ -2832,20 +2832,20 @@ namespace Emby.Server.Implementations.Data if (hasSimilar || hasSearch) { - List<(string, SortOrder)> prepend = new List<(string, SortOrder)>(4); + List<(ItemSortBy, SortOrder)> prepend = new List<(ItemSortBy, SortOrder)>(4); if (hasSearch) { - prepend.Add(("SearchScore", SortOrder.Descending)); + prepend.Add((ItemSortBy.SearchScore, SortOrder.Descending)); prepend.Add((ItemSortBy.SortName, SortOrder.Ascending)); } if (hasSimilar) { - prepend.Add(("SimilarityScore", SortOrder.Descending)); + prepend.Add((ItemSortBy.SimilarityScore, SortOrder.Descending)); prepend.Add((ItemSortBy.Random, SortOrder.Ascending)); } - var arr = new (string, SortOrder)[prepend.Count + orderBy.Count]; + var arr = new (ItemSortBy, SortOrder)[prepend.Count + orderBy.Count]; prepend.CopyTo(arr, 0); orderBy.CopyTo(arr, prepend.Count); orderBy = query.OrderBy = arr; @@ -2863,166 +2863,43 @@ namespace Emby.Server.Implementations.Data })); } - private string MapOrderByField(string name, InternalItemsQuery query) + private string MapOrderByField(ItemSortBy sortBy, InternalItemsQuery query) { - if (string.Equals(name, ItemSortBy.AirTime, StringComparison.OrdinalIgnoreCase)) + return sortBy switch { - // TODO - return "SortName"; - } - - if (string.Equals(name, ItemSortBy.Runtime, StringComparison.OrdinalIgnoreCase)) - { - return "RuntimeTicks"; - } - - if (string.Equals(name, ItemSortBy.Random, StringComparison.OrdinalIgnoreCase)) - { - return "RANDOM()"; - } - - if (string.Equals(name, ItemSortBy.DatePlayed, StringComparison.OrdinalIgnoreCase)) - { - if (query.GroupBySeriesPresentationUniqueKey) - { - return "MAX(LastPlayedDate)"; - } - - return "LastPlayedDate"; - } - - if (string.Equals(name, ItemSortBy.PlayCount, StringComparison.OrdinalIgnoreCase)) - { - return ItemSortBy.PlayCount; - } - - if (string.Equals(name, ItemSortBy.IsFavoriteOrLiked, StringComparison.OrdinalIgnoreCase)) - { - return "(Select Case When IsFavorite is null Then 0 Else IsFavorite End )"; - } - - if (string.Equals(name, ItemSortBy.IsFolder, StringComparison.OrdinalIgnoreCase)) - { - return ItemSortBy.IsFolder; - } - - if (string.Equals(name, ItemSortBy.IsPlayed, StringComparison.OrdinalIgnoreCase)) - { - return "played"; - } - - if (string.Equals(name, ItemSortBy.IsUnplayed, StringComparison.OrdinalIgnoreCase)) - { - return "played"; - } - - if (string.Equals(name, ItemSortBy.DateLastContentAdded, StringComparison.OrdinalIgnoreCase)) - { - return "DateLastMediaAdded"; - } - - if (string.Equals(name, ItemSortBy.Artist, StringComparison.OrdinalIgnoreCase)) - { - return "(select CleanValue from ItemValues where ItemId=Guid and Type=0 LIMIT 1)"; - } - - if (string.Equals(name, ItemSortBy.AlbumArtist, StringComparison.OrdinalIgnoreCase)) - { - return "(select CleanValue from ItemValues where ItemId=Guid and Type=1 LIMIT 1)"; - } - - if (string.Equals(name, ItemSortBy.OfficialRating, StringComparison.OrdinalIgnoreCase)) - { - return "InheritedParentalRatingValue"; - } - - if (string.Equals(name, ItemSortBy.Studio, StringComparison.OrdinalIgnoreCase)) - { - return "(select CleanValue from ItemValues where ItemId=Guid and Type=3 LIMIT 1)"; - } - - if (string.Equals(name, ItemSortBy.SeriesDatePlayed, StringComparison.OrdinalIgnoreCase)) - { - return "(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)"; - } - - if (string.Equals(name, ItemSortBy.SeriesSortName, StringComparison.OrdinalIgnoreCase)) - { - return "SeriesName"; - } - - if (string.Equals(name, ItemSortBy.AiredEpisodeOrder, StringComparison.OrdinalIgnoreCase)) - { - return ItemSortBy.AiredEpisodeOrder; - } - - if (string.Equals(name, ItemSortBy.Album, StringComparison.OrdinalIgnoreCase)) - { - return ItemSortBy.Album; - } - - if (string.Equals(name, ItemSortBy.DateCreated, StringComparison.OrdinalIgnoreCase)) - { - return ItemSortBy.DateCreated; - } - - if (string.Equals(name, ItemSortBy.PremiereDate, StringComparison.OrdinalIgnoreCase)) - { - return ItemSortBy.PremiereDate; - } - - if (string.Equals(name, ItemSortBy.StartDate, StringComparison.OrdinalIgnoreCase)) - { - return ItemSortBy.StartDate; - } - - if (string.Equals(name, ItemSortBy.Name, StringComparison.OrdinalIgnoreCase)) - { - return ItemSortBy.Name; - } - - if (string.Equals(name, ItemSortBy.CommunityRating, StringComparison.OrdinalIgnoreCase)) - { - return ItemSortBy.CommunityRating; - } - - if (string.Equals(name, ItemSortBy.ProductionYear, StringComparison.OrdinalIgnoreCase)) - { - return ItemSortBy.ProductionYear; - } - - if (string.Equals(name, ItemSortBy.CriticRating, StringComparison.OrdinalIgnoreCase)) - { - return ItemSortBy.CriticRating; - } - - if (string.Equals(name, ItemSortBy.VideoBitRate, StringComparison.OrdinalIgnoreCase)) - { - return ItemSortBy.VideoBitRate; - } - - if (string.Equals(name, ItemSortBy.ParentIndexNumber, StringComparison.OrdinalIgnoreCase)) - { - return ItemSortBy.ParentIndexNumber; - } - - if (string.Equals(name, ItemSortBy.IndexNumber, StringComparison.OrdinalIgnoreCase)) - { - return ItemSortBy.IndexNumber; - } - - if (string.Equals(name, ItemSortBy.SimilarityScore, StringComparison.OrdinalIgnoreCase)) - { - return ItemSortBy.SimilarityScore; - } - - if (string.Equals(name, ItemSortBy.SearchScore, StringComparison.OrdinalIgnoreCase)) - { - return ItemSortBy.SearchScore; - } - - // Unknown SortBy, just sort by the SortName. - return ItemSortBy.SortName; + ItemSortBy.AirTime => "SortName", // TODO + ItemSortBy.Runtime => "RuntimeTicks", + ItemSortBy.Random => "RANDOM()", + ItemSortBy.DatePlayed when query.GroupBySeriesPresentationUniqueKey => "MAX(LastPlayedDate)", + ItemSortBy.DatePlayed => "LastPlayedDate", + ItemSortBy.PlayCount => "PlayCount", + ItemSortBy.IsFavoriteOrLiked => "(Select Case When IsFavorite is null Then 0 Else IsFavorite End )", + ItemSortBy.IsFolder => "IsFolder", + ItemSortBy.IsPlayed => "played", + ItemSortBy.IsUnplayed => "played", + ItemSortBy.DateLastContentAdded => "DateLastMediaAdded", + ItemSortBy.Artist => "(select CleanValue from ItemValues where ItemId=Guid and Type=0 LIMIT 1)", + ItemSortBy.AlbumArtist => "(select CleanValue from ItemValues where ItemId=Guid and Type=1 LIMIT 1)", + ItemSortBy.OfficialRating => "InheritedParentalRatingValue", + ItemSortBy.Studio => "(select CleanValue from ItemValues where ItemId=Guid and Type=3 LIMIT 1)", + ItemSortBy.SeriesDatePlayed => "(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)", + ItemSortBy.SeriesSortName => "SeriesName", + ItemSortBy.AiredEpisodeOrder => "AiredEpisodeOrder", + ItemSortBy.Album => "Album", + ItemSortBy.DateCreated => "DateCreated", + ItemSortBy.PremiereDate => "PremiereDate", + ItemSortBy.StartDate => "StartDate", + ItemSortBy.Name => "Name", + ItemSortBy.CommunityRating => "CommunityRating", + ItemSortBy.ProductionYear => "ProductionYear", + ItemSortBy.CriticRating => "CriticRating", + ItemSortBy.VideoBitRate => "VideoBitRate", + ItemSortBy.ParentIndexNumber => "ParentIndexNumber", + ItemSortBy.IndexNumber => "IndexNumber", + ItemSortBy.SimilarityScore => "SimilarityScore", + ItemSortBy.SearchScore => "SearchScore", + _ => "SortName" + }; } public List<Guid> GetItemIdsList(InternalItemsQuery query) diff --git a/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs index 539d4a63af..04d90af3c3 100644 --- a/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs +++ b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs @@ -34,7 +34,7 @@ namespace Emby.Server.Implementations.Images Recursive = true, DtoOptions = new DtoOptions(true), ImageTypes = new ImageType[] { ImageType.Primary }, - OrderBy = new (string, SortOrder)[] + OrderBy = new (ItemSortBy, SortOrder)[] { (ItemSortBy.IsFolder, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 4f0983564d..5c76e77be1 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -1678,7 +1678,7 @@ namespace Emby.Server.Implementations.Library /// <param name="sortBy">The sort by.</param> /// <param name="sortOrder">The sort order.</param> /// <returns>IEnumerable{BaseItem}.</returns> - public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<string> sortBy, SortOrder sortOrder) + public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<ItemSortBy> sortBy, SortOrder sortOrder) { var isFirst = true; @@ -1701,7 +1701,7 @@ namespace Emby.Server.Implementations.Library return orderedItems ?? items; } - public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<(string OrderBy, SortOrder SortOrder)> orderBy) + public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<(ItemSortBy OrderBy, SortOrder SortOrder)> orderBy) { var isFirst = true; @@ -1736,9 +1736,9 @@ namespace Emby.Server.Implementations.Library /// <param name="name">The name.</param> /// <param name="user">The user.</param> /// <returns>IBaseItemComparer.</returns> - private IBaseItemComparer GetComparer(string name, User user) + private IBaseItemComparer GetComparer(ItemSortBy name, User user) { - var comparer = Comparers.FirstOrDefault(c => string.Equals(name, c.Name, StringComparison.OrdinalIgnoreCase)); + var comparer = Comparers.FirstOrDefault(c => name == c.Type); // If it requires a user, create a new one, and assign the user if (comparer is IUserBaseItemComparer) diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs index ee039ff0f7..dd427c7368 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs @@ -207,7 +207,7 @@ namespace Emby.Server.Implementations.LiveTv orderBy.Insert(0, (ItemSortBy.IsFavoriteOrLiked, SortOrder.Descending)); } - if (!internalQuery.OrderBy.Any(i => string.Equals(i.OrderBy, ItemSortBy.SortName, StringComparison.OrdinalIgnoreCase))) + if (internalQuery.OrderBy.All(i => i.OrderBy != ItemSortBy.SortName)) { orderBy.Add((ItemSortBy.SortName, SortOrder.Ascending)); } diff --git a/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs b/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs index 964004eccb..6d13c6d573 100644 --- a/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs +++ b/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using System; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Sorting; @@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// </summary> /// <value>The name.</value> - public string Name => ItemSortBy.AiredEpisodeOrder; + public ItemSortBy Type => ItemSortBy.AiredEpisodeOrder; /// <summary> /// Compares the specified x. diff --git a/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs b/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs index 67a9fbd3bc..65c8599e75 100644 --- a/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs +++ b/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Sorting; @@ -16,7 +17,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// </summary> /// <value>The name.</value> - public string Name => ItemSortBy.AlbumArtist; + public ItemSortBy Type => ItemSortBy.AlbumArtist; /// <summary> /// Compares the specified x. diff --git a/Emby.Server.Implementations/Sorting/AlbumComparer.cs b/Emby.Server.Implementations/Sorting/AlbumComparer.cs index 4bed0fca12..e071136551 100644 --- a/Emby.Server.Implementations/Sorting/AlbumComparer.cs +++ b/Emby.Server.Implementations/Sorting/AlbumComparer.cs @@ -1,4 +1,5 @@ using System; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Sorting; @@ -15,7 +16,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// </summary> /// <value>The name.</value> - public string Name => ItemSortBy.Album; + public ItemSortBy Type => ItemSortBy.Album; /// <summary> /// Compares the specified x. diff --git a/Emby.Server.Implementations/Sorting/ArtistComparer.cs b/Emby.Server.Implementations/Sorting/ArtistComparer.cs index a8bb55e2bc..f99977e5c5 100644 --- a/Emby.Server.Implementations/Sorting/ArtistComparer.cs +++ b/Emby.Server.Implementations/Sorting/ArtistComparer.cs @@ -1,4 +1,5 @@ using System; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Sorting; @@ -12,7 +13,7 @@ namespace Emby.Server.Implementations.Sorting public class ArtistComparer : IBaseItemComparer { /// <inheritdoc /> - public string Name => ItemSortBy.Artist; + public ItemSortBy Type => ItemSortBy.Artist; /// <inheritdoc /> public int Compare(BaseItem? x, BaseItem? y) diff --git a/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs b/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs index 5cb11ab465..9e02ea2ae8 100644 --- a/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs +++ b/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using System; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Querying; @@ -13,7 +14,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// </summary> /// <value>The name.</value> - public string Name => ItemSortBy.CommunityRating; + public ItemSortBy Type => ItemSortBy.CommunityRating; /// <summary> /// Compares the specified x. diff --git a/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs b/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs index ba1835e4f2..d4a8d46898 100644 --- a/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs +++ b/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs @@ -1,3 +1,4 @@ +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Querying; @@ -13,7 +14,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// </summary> /// <value>The name.</value> - public string Name => ItemSortBy.CriticRating; + public ItemSortBy Type => ItemSortBy.CriticRating; /// <summary> /// Compares the specified x. diff --git a/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs b/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs index 6133aaccc6..b86b4432f8 100644 --- a/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs +++ b/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs @@ -1,4 +1,5 @@ using System; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Querying; @@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// </summary> /// <value>The name.</value> - public string Name => ItemSortBy.DateCreated; + public ItemSortBy Type => ItemSortBy.DateCreated; /// <summary> /// Compares the specified x. diff --git a/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs b/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs index b1cb123ce1..e1c26d0121 100644 --- a/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs +++ b/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs @@ -3,6 +3,7 @@ using System; using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Sorting; @@ -34,7 +35,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// </summary> /// <value>The name.</value> - public string Name => ItemSortBy.DateLastContentAdded; + public ItemSortBy Type => ItemSortBy.DateLastContentAdded; /// <summary> /// Compares the specified x. diff --git a/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs b/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs index 453d817c76..d668c17bfc 100644 --- a/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs +++ b/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs @@ -2,6 +2,7 @@ using System; using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Sorting; @@ -36,7 +37,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// </summary> /// <value>The name.</value> - public string Name => ItemSortBy.DatePlayed; + public ItemSortBy Type => ItemSortBy.DatePlayed; /// <summary> /// Compares the specified x. diff --git a/Emby.Server.Implementations/Sorting/IndexNumberComparer.cs b/Emby.Server.Implementations/Sorting/IndexNumberComparer.cs index 1bcaccd8a3..11cad62567 100644 --- a/Emby.Server.Implementations/Sorting/IndexNumberComparer.cs +++ b/Emby.Server.Implementations/Sorting/IndexNumberComparer.cs @@ -1,4 +1,5 @@ using System; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Querying; @@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// </summary> /// <value>The name.</value> - public string Name => ItemSortBy.IndexNumber; + public ItemSortBy Type => ItemSortBy.IndexNumber; /// <summary> /// Compares the specified x. diff --git a/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs b/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs index 73e628cf75..622a341b6a 100644 --- a/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs +++ b/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs @@ -2,6 +2,7 @@ #pragma warning disable CS1591 using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Sorting; @@ -21,7 +22,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// </summary> /// <value>The name.</value> - public string Name => ItemSortBy.IsFavoriteOrLiked; + public ItemSortBy Type => ItemSortBy.IsFavoriteOrLiked; /// <summary> /// Gets or sets the user data repository. diff --git a/Emby.Server.Implementations/Sorting/IsFolderComparer.cs b/Emby.Server.Implementations/Sorting/IsFolderComparer.cs index 3c5ddeefaa..6f0ca59c53 100644 --- a/Emby.Server.Implementations/Sorting/IsFolderComparer.cs +++ b/Emby.Server.Implementations/Sorting/IsFolderComparer.cs @@ -1,5 +1,6 @@ #pragma warning disable CS1591 +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Querying; @@ -12,7 +13,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// </summary> /// <value>The name.</value> - public string Name => ItemSortBy.IsFolder; + public ItemSortBy Type => ItemSortBy.IsFolder; /// <summary> /// Compares the specified x. diff --git a/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs b/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs index 7d77a8bc5a..2a3e456c2d 100644 --- a/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs +++ b/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs @@ -3,6 +3,7 @@ #pragma warning disable CS1591 using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Sorting; @@ -22,7 +23,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// </summary> /// <value>The name.</value> - public string Name => ItemSortBy.IsUnplayed; + public ItemSortBy Type => ItemSortBy.IsUnplayed; /// <summary> /// Gets or sets the user data repository. diff --git a/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs b/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs index 926835f906..afd8ccf9f3 100644 --- a/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs +++ b/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs @@ -3,6 +3,7 @@ #pragma warning disable CS1591 using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Sorting; @@ -22,7 +23,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// </summary> /// <value>The name.</value> - public string Name => ItemSortBy.IsUnplayed; + public ItemSortBy Type => ItemSortBy.IsUnplayed; /// <summary> /// Gets or sets the user data repository. diff --git a/Emby.Server.Implementations/Sorting/NameComparer.cs b/Emby.Server.Implementations/Sorting/NameComparer.cs index 93bec4db99..72d9c79739 100644 --- a/Emby.Server.Implementations/Sorting/NameComparer.cs +++ b/Emby.Server.Implementations/Sorting/NameComparer.cs @@ -1,4 +1,5 @@ using System; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Querying; @@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// </summary> /// <value>The name.</value> - public string Name => ItemSortBy.Name; + public ItemSortBy Type => ItemSortBy.Name; /// <summary> /// Compares the specified x. diff --git a/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs b/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs index ce44f99a69..b4ee2c7234 100644 --- a/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs +++ b/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using System; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Globalization; @@ -21,7 +22,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// </summary> /// <value>The name.</value> - public string Name => ItemSortBy.OfficialRating; + public ItemSortBy Type => ItemSortBy.OfficialRating; /// <summary> /// Compares the specified x. diff --git a/Emby.Server.Implementations/Sorting/ParentIndexNumberComparer.cs b/Emby.Server.Implementations/Sorting/ParentIndexNumberComparer.cs index c54750843e..5aeac29be4 100644 --- a/Emby.Server.Implementations/Sorting/ParentIndexNumberComparer.cs +++ b/Emby.Server.Implementations/Sorting/ParentIndexNumberComparer.cs @@ -1,4 +1,5 @@ using System; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Querying; @@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// </summary> /// <value>The name.</value> - public string Name => ItemSortBy.ParentIndexNumber; + public ItemSortBy Type => ItemSortBy.ParentIndexNumber; /// <summary> /// Compares the specified x. diff --git a/Emby.Server.Implementations/Sorting/PlayCountComparer.cs b/Emby.Server.Implementations/Sorting/PlayCountComparer.cs index 16f1b79b3e..12f88bf4da 100644 --- a/Emby.Server.Implementations/Sorting/PlayCountComparer.cs +++ b/Emby.Server.Implementations/Sorting/PlayCountComparer.cs @@ -1,6 +1,7 @@ #nullable disable using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Sorting; @@ -23,7 +24,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// </summary> /// <value>The name.</value> - public string Name => ItemSortBy.PlayCount; + public ItemSortBy Type => ItemSortBy.PlayCount; /// <summary> /// Gets or sets the user data repository. diff --git a/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs b/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs index db86b8002d..8c8b8824f3 100644 --- a/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs +++ b/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs @@ -1,4 +1,5 @@ using System; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Querying; @@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// </summary> /// <value>The name.</value> - public string Name => ItemSortBy.PremiereDate; + public ItemSortBy Type => ItemSortBy.PremiereDate; /// <summary> /// Compares the specified x. diff --git a/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs b/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs index 7fd1e024d0..9aec87f183 100644 --- a/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs +++ b/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs @@ -1,3 +1,4 @@ +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Querying; @@ -13,7 +14,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// </summary> /// <value>The name.</value> - public string Name => ItemSortBy.ProductionYear; + public ItemSortBy Type => ItemSortBy.ProductionYear; /// <summary> /// Compares the specified x. diff --git a/Emby.Server.Implementations/Sorting/RandomComparer.cs b/Emby.Server.Implementations/Sorting/RandomComparer.cs index bf0168222d..6f8ea5b741 100644 --- a/Emby.Server.Implementations/Sorting/RandomComparer.cs +++ b/Emby.Server.Implementations/Sorting/RandomComparer.cs @@ -1,4 +1,5 @@ using System; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Querying; @@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// </summary> /// <value>The name.</value> - public string Name => ItemSortBy.Random; + public ItemSortBy Type => ItemSortBy.Random; /// <summary> /// Compares the specified x. diff --git a/Emby.Server.Implementations/Sorting/RuntimeComparer.cs b/Emby.Server.Implementations/Sorting/RuntimeComparer.cs index 753e58324c..3c096ab023 100644 --- a/Emby.Server.Implementations/Sorting/RuntimeComparer.cs +++ b/Emby.Server.Implementations/Sorting/RuntimeComparer.cs @@ -1,4 +1,5 @@ using System; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Querying; @@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// </summary> /// <value>The name.</value> - public string Name => ItemSortBy.Runtime; + public ItemSortBy Type => ItemSortBy.Runtime; /// <summary> /// Compares the specified x. diff --git a/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs index 5b6c64f63a..ed42fd6d55 100644 --- a/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs +++ b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using System; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Querying; @@ -13,7 +14,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// </summary> /// <value>The name.</value> - public string Name => ItemSortBy.SeriesSortName; + public ItemSortBy Type => ItemSortBy.SeriesSortName; /// <summary> /// Compares the specified x. diff --git a/Emby.Server.Implementations/Sorting/SortNameComparer.cs b/Emby.Server.Implementations/Sorting/SortNameComparer.cs index 19abafe192..314c25d128 100644 --- a/Emby.Server.Implementations/Sorting/SortNameComparer.cs +++ b/Emby.Server.Implementations/Sorting/SortNameComparer.cs @@ -1,4 +1,5 @@ using System; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Querying; @@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// </summary> /// <value>The name.</value> - public string Name => ItemSortBy.SortName; + public ItemSortBy Type => ItemSortBy.SortName; /// <summary> /// Compares the specified x. diff --git a/Emby.Server.Implementations/Sorting/StartDateComparer.cs b/Emby.Server.Implementations/Sorting/StartDateComparer.cs index 2759d20de8..e0b438ef1a 100644 --- a/Emby.Server.Implementations/Sorting/StartDateComparer.cs +++ b/Emby.Server.Implementations/Sorting/StartDateComparer.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using System; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Sorting; @@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// </summary> /// <value>The name.</value> - public string Name => ItemSortBy.StartDate; + public ItemSortBy Type => ItemSortBy.StartDate; /// <summary> /// Compares the specified x. diff --git a/Emby.Server.Implementations/Sorting/StudioComparer.cs b/Emby.Server.Implementations/Sorting/StudioComparer.cs index 89d10f3d23..0edffb783b 100644 --- a/Emby.Server.Implementations/Sorting/StudioComparer.cs +++ b/Emby.Server.Implementations/Sorting/StudioComparer.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using System; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; @@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// </summary> /// <value>The name.</value> - public string Name => ItemSortBy.Studio; + public ItemSortBy Type => ItemSortBy.Studio; /// <summary> /// Compares the specified x. diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs index c9d2f67f92..c3c43982bf 100644 --- a/Jellyfin.Api/Controllers/ArtistsController.cs +++ b/Jellyfin.Api/Controllers/ArtistsController.cs @@ -113,7 +113,7 @@ public class ArtistsController : BaseJellyfinApiController [FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWith, [FromQuery] string? nameLessThan, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) @@ -317,7 +317,7 @@ public class ArtistsController : BaseJellyfinApiController [FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWith, [FromQuery] string? nameLessThan, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs index 11c4ac3768..fdc16ee23f 100644 --- a/Jellyfin.Api/Controllers/ChannelsController.cs +++ b/Jellyfin.Api/Controllers/ChannelsController.cs @@ -122,7 +122,7 @@ public class ChannelsController : BaseJellyfinApiController [FromQuery] int? limit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields) { userId = RequestHelpers.GetUserId(User, userId); diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs index da60f2c60b..51f04fa276 100644 --- a/Jellyfin.Api/Controllers/GenresController.cs +++ b/Jellyfin.Api/Controllers/GenresController.cs @@ -85,7 +85,7 @@ public class GenresController : BaseJellyfinApiController [FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWith, [FromQuery] string? nameLessThan, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 80128536da..891cf88a7c 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -197,7 +197,7 @@ public class ItemsController : BaseJellyfinApiController [FromQuery] bool? isFavorite, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, [FromQuery] bool? isPlayed, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, @@ -654,7 +654,7 @@ public class ItemsController : BaseJellyfinApiController [FromQuery] bool? isFavorite, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, [FromQuery] bool? isPlayed, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 649397d68d..58159406a2 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -143,7 +143,7 @@ public class LiveTvController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery] bool? enableUserData, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, [FromQuery] SortOrder? sortOrder, [FromQuery] bool enableFavoriteSorting = false, [FromQuery] bool addCurrentProgram = true) @@ -547,7 +547,7 @@ public class LiveTvController : BaseJellyfinApiController [FromQuery] bool? isSports, [FromQuery] int? startIndex, [FromQuery] int? limit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs index 435457af67..94c8993575 100644 --- a/Jellyfin.Api/Controllers/MusicGenresController.cs +++ b/Jellyfin.Api/Controllers/MusicGenresController.cs @@ -85,7 +85,7 @@ public class MusicGenresController : BaseJellyfinApiController [FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWith, [FromQuery] string? nameLessThan, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs index b5b6406206..1246efd494 100644 --- a/Jellyfin.Api/Controllers/TrailersController.cs +++ b/Jellyfin.Api/Controllers/TrailersController.cs @@ -162,7 +162,7 @@ public class TrailersController : BaseJellyfinApiController [FromQuery] bool? isFavorite, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, [FromQuery] bool? isPlayed, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index af403fa80e..55a30d4692 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -219,7 +219,7 @@ public class TvShowsController : BaseJellyfinApiController [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery] bool? enableUserData, - [FromQuery] string? sortBy) + [FromQuery] ItemSortBy? sortBy) { userId = RequestHelpers.GetUserId(User, userId); var user = userId.Value.Equals(default) @@ -289,7 +289,7 @@ public class TvShowsController : BaseJellyfinApiController episodes = UserViewBuilder.FilterForAdjacency(episodes, adjacentTo.Value).ToList(); } - if (string.Equals(sortBy, ItemSortBy.Random, StringComparison.OrdinalIgnoreCase)) + if (sortBy == ItemSortBy.Random) { episodes.Shuffle(); } diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs index 74370db50b..641cdd81e7 100644 --- a/Jellyfin.Api/Controllers/YearsController.cs +++ b/Jellyfin.Api/Controllers/YearsController.cs @@ -77,7 +77,7 @@ public class YearsController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs index bc12ca3889..be3d4dfb67 100644 --- a/Jellyfin.Api/Helpers/RequestHelpers.cs +++ b/Jellyfin.Api/Helpers/RequestHelpers.cs @@ -30,14 +30,14 @@ public static class RequestHelpers /// <param name="sortBy">Sort By. Comma delimited string.</param> /// <param name="requestedSortOrder">Sort Order. Comma delimited string.</param> /// <returns>Order By.</returns> - public static (string, SortOrder)[] GetOrderBy(IReadOnlyList<string> sortBy, IReadOnlyList<SortOrder> requestedSortOrder) + public static (ItemSortBy, SortOrder)[] GetOrderBy(IReadOnlyList<ItemSortBy> sortBy, IReadOnlyList<SortOrder> requestedSortOrder) { if (sortBy.Count == 0) { - return Array.Empty<(string, SortOrder)>(); + return Array.Empty<(ItemSortBy, SortOrder)>(); } - var result = new (string, SortOrder)[sortBy.Count]; + var result = new (ItemSortBy, SortOrder)[sortBy.Count]; var i = 0; // Add elements which have a SortOrder specified for (; i < requestedSortOrder.Count; i++) diff --git a/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs index 5e7dd689e8..6a30de5e6c 100644 --- a/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs +++ b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs @@ -107,7 +107,7 @@ public class GetProgramsDto /// Optional. /// </summary> [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] - public IReadOnlyList<string> SortBy { get; set; } = Array.Empty<string>(); + public IReadOnlyList<ItemSortBy> SortBy { get; set; } = Array.Empty<ItemSortBy>(); /// <summary> /// Gets or sets sort Order - Ascending,Descending. diff --git a/Jellyfin.Data/Enums/ItemSortBy.cs b/Jellyfin.Data/Enums/ItemSortBy.cs new file mode 100644 index 0000000000..17bf1166de --- /dev/null +++ b/Jellyfin.Data/Enums/ItemSortBy.cs @@ -0,0 +1,167 @@ +namespace Jellyfin.Data.Enums; + +/// <summary> +/// These represent sort orders. +/// </summary> +public enum ItemSortBy +{ + /// <summary> + /// Default sort order. + /// </summary> + Default = 0, + + /// <summary> + /// The aired episode order. + /// </summary> + AiredEpisodeOrder = 1, + + /// <summary> + /// The album. + /// </summary> + Album = 2, + + /// <summary> + /// The album artist. + /// </summary> + AlbumArtist = 3, + + /// <summary> + /// The artist. + /// </summary> + Artist = 4, + + /// <summary> + /// The date created. + /// </summary> + DateCreated = 5, + + /// <summary> + /// The official rating. + /// </summary> + OfficialRating = 6, + + /// <summary> + /// The date played. + /// </summary> + DatePlayed = 7, + + /// <summary> + /// The premiere date. + /// </summary> + PremiereDate = 8, + + /// <summary> + /// The start date. + /// </summary> + StartDate = 9, + + /// <summary> + /// The sort name. + /// </summary> + SortName = 10, + + /// <summary> + /// The name. + /// </summary> + Name = 11, + + /// <summary> + /// The random. + /// </summary> + Random = 12, + + /// <summary> + /// The runtime. + /// </summary> + Runtime = 13, + + /// <summary> + /// The community rating. + /// </summary> + CommunityRating = 14, + + /// <summary> + /// The production year. + /// </summary> + ProductionYear = 15, + + /// <summary> + /// The play count. + /// </summary> + PlayCount = 16, + + /// <summary> + /// The critic rating. + /// </summary> + CriticRating = 17, + + /// <summary> + /// The IsFolder boolean. + /// </summary> + IsFolder = 18, + + /// <summary> + /// The IsUnplayed boolean. + /// </summary> + IsUnplayed = 19, + + /// <summary> + /// The IsPlayed boolean. + /// </summary> + IsPlayed = 20, + + /// <summary> + /// The series sort. + /// </summary> + SeriesSortName = 21, + + /// <summary> + /// The video bitrate. + /// </summary> + VideoBitRate = 22, + + /// <summary> + /// The air time. + /// </summary> + AirTime = 23, + + /// <summary> + /// The studio. + /// </summary> + Studio = 24, + + /// <summary> + /// The IsFavouriteOrLiked boolean. + /// </summary> + IsFavoriteOrLiked = 25, + + /// <summary> + /// The last content added date. + /// </summary> + DateLastContentAdded = 26, + + /// <summary> + /// The series last played date. + /// </summary> + SeriesDatePlayed = 27, + + /// <summary> + /// The parent index number. + /// </summary> + ParentIndexNumber = 28, + + /// <summary> + /// The index number. + /// </summary> + IndexNumber = 29, + + /// <summary> + /// The similarity score. + /// </summary> + SimilarityScore = 30, + + /// <summary> + /// The search score. + /// </summary> + SearchScore = 31, +} diff --git a/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs b/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs index 14459624e9..7a73f3eaf9 100644 --- a/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs +++ b/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs @@ -1,5 +1,7 @@ #nullable disable +using Jellyfin.Data.Enums; + namespace MediaBrowser.Controller.Entities { /// <summary> diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index a51299284b..fb50a45462 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -39,7 +39,7 @@ namespace MediaBrowser.Controller.Entities MediaTypes = Array.Empty<string>(); MinSimilarityScore = 20; OfficialRatings = Array.Empty<string>(); - OrderBy = Array.Empty<(string, SortOrder)>(); + OrderBy = Array.Empty<(ItemSortBy, SortOrder)>(); PersonIds = Array.Empty<Guid>(); PersonTypes = Array.Empty<string>(); PresetViews = Array.Empty<string>(); @@ -284,7 +284,7 @@ namespace MediaBrowser.Controller.Entities public bool? HasChapterImages { get; set; } - public IReadOnlyList<(string OrderBy, SortOrder SortOrder)> OrderBy { get; set; } + public IReadOnlyList<(ItemSortBy OrderBy, SortOrder SortOrder)> OrderBy { get; set; } public DateTime? MinDateCreated { get; set; } diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs index 66210cb6c4..d7ccfd8aee 100644 --- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs +++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs @@ -20,7 +20,7 @@ namespace MediaBrowser.Controller.Entities.Movies { public BoxSet() { - DisplayOrder = ItemSortBy.PremiereDate; + DisplayOrder = "PremiereDate"; } [JsonIgnore] @@ -116,13 +116,13 @@ namespace MediaBrowser.Controller.Entities.Movies { var children = base.GetChildren(user, includeLinkedChildren, query); - if (string.Equals(DisplayOrder, ItemSortBy.SortName, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(DisplayOrder, "SortName", StringComparison.OrdinalIgnoreCase)) { // Sort by name return LibraryManager.Sort(children, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).ToList(); } - if (string.Equals(DisplayOrder, ItemSortBy.PremiereDate, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(DisplayOrder, "PremiereDate", StringComparison.OrdinalIgnoreCase)) { // Sort by release date return LibraryManager.Sort(children, user, new[] { ItemSortBy.ProductionYear, ItemSortBy.PremiereDate, ItemSortBy.SortName }, SortOrder.Ascending).ToList(); @@ -136,7 +136,7 @@ namespace MediaBrowser.Controller.Entities.Movies { var children = base.GetRecursiveChildren(user, query); - if (string.Equals(DisplayOrder, ItemSortBy.PremiereDate, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(DisplayOrder, "PremiereDate", StringComparison.OrdinalIgnoreCase)) { // Sort by release date return LibraryManager.Sort(children, user, new[] { ItemSortBy.ProductionYear, ItemSortBy.PremiereDate, ItemSortBy.SortName }, SortOrder.Ascending).ToList(); diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index f34e3d68d9..52d546a4f1 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -199,9 +199,9 @@ namespace MediaBrowser.Controller.Library /// <param name="sortBy">The sort by.</param> /// <param name="sortOrder">The sort order.</param> /// <returns>IEnumerable{BaseItem}.</returns> - IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<string> sortBy, SortOrder sortOrder); + IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<ItemSortBy> sortBy, SortOrder sortOrder); - IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<(string OrderBy, SortOrder SortOrder)> orderBy); + IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<(ItemSortBy OrderBy, SortOrder SortOrder)> orderBy); /// <summary> /// Gets the user root folder. diff --git a/MediaBrowser.Controller/Providers/EpisodeInfo.cs b/MediaBrowser.Controller/Providers/EpisodeInfo.cs index c4ad352a3b..2f7ebb5cce 100644 --- a/MediaBrowser.Controller/Providers/EpisodeInfo.cs +++ b/MediaBrowser.Controller/Providers/EpisodeInfo.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using Jellyfin.Data.Enums; namespace MediaBrowser.Controller.Providers { diff --git a/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs b/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs index 07fe1ea8a9..96f8a2af5d 100644 --- a/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs +++ b/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; namespace MediaBrowser.Controller.Sorting @@ -9,9 +10,8 @@ namespace MediaBrowser.Controller.Sorting public interface IBaseItemComparer : IComparer<BaseItem?> { /// <summary> - /// Gets the name. + /// Gets the comparer type. /// </summary> - /// <value>The name.</value> - string Name { get; } + ItemSortBy Type { get; } } } diff --git a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs index f913b2320d..5a71930799 100644 --- a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs +++ b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs @@ -6,6 +6,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; diff --git a/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs b/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs index 673d97a9ed..d872572b77 100644 --- a/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs +++ b/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs @@ -14,7 +14,7 @@ namespace MediaBrowser.Model.LiveTv public LiveTvChannelQuery() { EnableUserData = true; - SortBy = Array.Empty<string>(); + SortBy = Array.Empty<ItemSortBy>(); } /// <summary> @@ -99,7 +99,7 @@ namespace MediaBrowser.Model.LiveTv public bool? IsSeries { get; set; } - public string[] SortBy { get; set; } + public ItemSortBy[] SortBy { get; set; } /// <summary> /// Gets or sets the sort order to return results with. diff --git a/MediaBrowser.Model/Querying/ItemSortBy.cs b/MediaBrowser.Model/Querying/ItemSortBy.cs deleted file mode 100644 index 1a7c9a63b0..0000000000 --- a/MediaBrowser.Model/Querying/ItemSortBy.cs +++ /dev/null @@ -1,163 +0,0 @@ -namespace MediaBrowser.Model.Querying -{ - /// <summary> - /// These represent sort orders that are known by the core. - /// </summary> - public static class ItemSortBy - { - /// <summary> - /// The aired episode order. - /// </summary> - public const string AiredEpisodeOrder = "AiredEpisodeOrder"; - - /// <summary> - /// The album. - /// </summary> - public const string Album = "Album"; - - /// <summary> - /// The album artist. - /// </summary> - public const string AlbumArtist = "AlbumArtist"; - - /// <summary> - /// The artist. - /// </summary> - public const string Artist = "Artist"; - - /// <summary> - /// The date created. - /// </summary> - public const string DateCreated = "DateCreated"; - - /// <summary> - /// The official rating. - /// </summary> - public const string OfficialRating = "OfficialRating"; - - /// <summary> - /// The date played. - /// </summary> - public const string DatePlayed = "DatePlayed"; - - /// <summary> - /// The premiere date. - /// </summary> - public const string PremiereDate = "PremiereDate"; - - /// <summary> - /// The start date. - /// </summary> - public const string StartDate = "StartDate"; - - /// <summary> - /// The sort name. - /// </summary> - public const string SortName = "SortName"; - - /// <summary> - /// The name. - /// </summary> - public const string Name = "Name"; - - /// <summary> - /// The random. - /// </summary> - public const string Random = "Random"; - - /// <summary> - /// The runtime. - /// </summary> - public const string Runtime = "Runtime"; - - /// <summary> - /// The community rating. - /// </summary> - public const string CommunityRating = "CommunityRating"; - - /// <summary> - /// The production year. - /// </summary> - public const string ProductionYear = "ProductionYear"; - - /// <summary> - /// The play count. - /// </summary> - public const string PlayCount = "PlayCount"; - - /// <summary> - /// The critic rating. - /// </summary> - public const string CriticRating = "CriticRating"; - - /// <summary> - /// The IsFolder boolean. - /// </summary> - public const string IsFolder = "IsFolder"; - - /// <summary> - /// The IsUnplayed boolean. - /// </summary> - public const string IsUnplayed = "IsUnplayed"; - - /// <summary> - /// The IsPlayed boolean. - /// </summary> - public const string IsPlayed = "IsPlayed"; - - /// <summary> - /// The series sort. - /// </summary> - public const string SeriesSortName = "SeriesSortName"; - - /// <summary> - /// The video bitrate. - /// </summary> - public const string VideoBitRate = "VideoBitRate"; - - /// <summary> - /// The air time. - /// </summary> - public const string AirTime = "AirTime"; - - /// <summary> - /// The studio. - /// </summary> - public const string Studio = "Studio"; - - /// <summary> - /// The IsFavouriteOrLiked boolean. - /// </summary> - public const string IsFavoriteOrLiked = "IsFavoriteOrLiked"; - - /// <summary> - /// The last content added date. - /// </summary> - public const string DateLastContentAdded = "DateLastContentAdded"; - - /// <summary> - /// The series last played date. - /// </summary> - public const string SeriesDatePlayed = "SeriesDatePlayed"; - - /// <summary> - /// The parent index number. - /// </summary> - public const string ParentIndexNumber = "ParentIndexNumber"; - - /// <summary> - /// The index number. - /// </summary> - public const string IndexNumber = "IndexNumber"; - - /// <summary> - /// The similarity score. - /// </summary> - public const string SimilarityScore = "SimilarityScore"; - - /// <summary> - /// The search score. - /// </summary> - public const string SearchScore = "SearchScore"; - } -} diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index e336c8825e..06445c90d9 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs index 500ebaf71c..72e59c9ac4 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; diff --git a/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs b/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs index 2d7741d81a..a2d1b3607b 100644 --- a/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs +++ b/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs @@ -14,7 +14,7 @@ namespace Jellyfin.Api.Tests.Helpers { [Theory] [MemberData(nameof(GetOrderBy_Success_TestData))] - public static void GetOrderBy_Success(IReadOnlyList<string> sortBy, IReadOnlyList<SortOrder> requestedSortOrder, (string, SortOrder)[] expected) + public static void GetOrderBy_Success(IReadOnlyList<ItemSortBy> sortBy, IReadOnlyList<SortOrder> requestedSortOrder, (ItemSortBy, SortOrder)[] expected) { Assert.Equal(expected, RequestHelpers.GetOrderBy(sortBy, requestedSortOrder)); } @@ -95,42 +95,42 @@ namespace Jellyfin.Api.Tests.Helpers Assert.Throws<SecurityException>(() => RequestHelpers.GetUserId(principal, requestUserId)); } - public static TheoryData<IReadOnlyList<string>, IReadOnlyList<SortOrder>, (string, SortOrder)[]> GetOrderBy_Success_TestData() + public static TheoryData<IReadOnlyList<ItemSortBy>, IReadOnlyList<SortOrder>, (ItemSortBy, SortOrder)[]> GetOrderBy_Success_TestData() { - var data = new TheoryData<IReadOnlyList<string>, IReadOnlyList<SortOrder>, (string, SortOrder)[]>(); + var data = new TheoryData<IReadOnlyList<ItemSortBy>, IReadOnlyList<SortOrder>, (ItemSortBy, SortOrder)[]>(); data.Add( - Array.Empty<string>(), + Array.Empty<ItemSortBy>(), Array.Empty<SortOrder>(), - Array.Empty<(string, SortOrder)>()); + Array.Empty<(ItemSortBy, SortOrder)>()); data.Add( - new string[] + new[] { - "IsFavoriteOrLiked", - "Random" + ItemSortBy.IsFavoriteOrLiked, + ItemSortBy.Random }, Array.Empty<SortOrder>(), - new (string, SortOrder)[] + new (ItemSortBy, SortOrder)[] { - ("IsFavoriteOrLiked", SortOrder.Ascending), - ("Random", SortOrder.Ascending), + (ItemSortBy.IsFavoriteOrLiked, SortOrder.Ascending), + (ItemSortBy.Random, SortOrder.Ascending), }); data.Add( - new string[] + new[] { - "SortName", - "ProductionYear" + ItemSortBy.SortName, + ItemSortBy.ProductionYear }, - new SortOrder[] + new[] { SortOrder.Descending }, - new (string, SortOrder)[] + new (ItemSortBy, SortOrder)[] { - ("SortName", SortOrder.Descending), - ("ProductionYear", SortOrder.Descending), + (ItemSortBy.SortName, SortOrder.Descending), + (ItemSortBy.ProductionYear, SortOrder.Descending), }); return data; From 906f701fa81c7cde1a9a01b066f3f29ff98552a5 Mon Sep 17 00:00:00 2001 From: Cody Robibero <cody@robibe.ro> Date: Thu, 9 Nov 2023 14:00:29 -0700 Subject: [PATCH 802/858] Convert CollectionType, SpecialFolderType to enum (#9764) * Convert CollectionType, SpecialFolderType to enum * Hide internal enum CollectionType values * Apply suggestions from code review Co-authored-by: Shadowghost <Shadowghost@users.noreply.github.com> * Fix recent change * Update Jellyfin.Data/Attributes/OpenApiIgnoreEnumAttribute.cs Co-authored-by: Patrick Barron <barronpm@gmail.com> --------- Co-authored-by: Shadowghost <Shadowghost@users.noreply.github.com> Co-authored-by: Patrick Barron <barronpm@gmail.com> --- Emby.Dlna/ContentDirectory/ControlHandler.cs | 34 ++-- .../Images/CollectionFolderImageProvider.cs | 60 +++---- .../Images/DynamicImageProvider.cs | 6 +- .../Library/LibraryManager.cs | 69 ++++---- .../Library/Resolvers/Audio/AudioResolver.cs | 14 +- .../Resolvers/Audio/MusicAlbumResolver.cs | 3 +- .../Resolvers/Audio/MusicArtistResolver.cs | 3 +- .../Library/Resolvers/Books/BookResolver.cs | 3 +- .../Library/Resolvers/Movies/MovieResolver.cs | 62 ++++--- .../Library/Resolvers/PhotoAlbumResolver.cs | 5 +- .../Library/Resolvers/PhotoResolver.cs | 5 +- .../Library/Resolvers/PlaylistResolver.cs | 7 +- .../Resolvers/SpecialFolderResolver.cs | 6 +- .../Library/Resolvers/TV/EpisodeResolver.cs | 7 +- .../Library/Resolvers/TV/SeriesResolver.cs | 7 +- .../Library/UserViewManager.cs | 35 ++-- .../Playlists/PlaylistsFolder.cs | 2 +- Jellyfin.Api/Controllers/GenresController.cs | 4 +- .../Controllers/ItemUpdateController.cs | 9 +- Jellyfin.Api/Controllers/ItemsController.cs | 4 +- Jellyfin.Api/Controllers/LibraryController.cs | 4 +- .../Controllers/UserViewsController.cs | 3 +- .../Attributes/OpenApiIgnoreEnumAttribute.cs | 11 ++ Jellyfin.Data/Enums/CollectionType.cs | 164 ++++++++++++++++++ .../ApiServiceCollectionExtensions.cs | 1 + .../Filters/IgnoreEnumSchemaFilter.cs | 42 +++++ MediaBrowser.Controller/Entities/BaseItem.cs | 2 +- .../Entities/BasePluginFolder.cs | 3 +- .../Entities/CollectionFolder.cs | 3 +- .../Entities/ICollectionFolder.cs | 3 +- .../Entities/InternalItemsQuery.cs | 4 +- MediaBrowser.Controller/Entities/UserView.cs | 37 ++-- .../Entities/UserViewBuilder.cs | 78 ++++----- .../Library/ILibraryManager.cs | 20 +-- .../Library/IUserViewManager.cs | 3 +- .../Library/ItemResolveArgs.cs | 7 +- .../Resolvers/IItemResolver.cs | 3 +- MediaBrowser.Model/Dto/BaseItemDto.cs | 2 +- MediaBrowser.Model/Dto/MetadataEditorInfo.cs | 3 +- MediaBrowser.Model/Entities/CollectionType.cs | 27 --- MediaBrowser.Model/Library/UserViewQuery.cs | 5 +- .../Library/AudioResolverTests.cs | 3 +- .../Library/EpisodeResolverTest.cs | 1 + 43 files changed, 486 insertions(+), 288 deletions(-) create mode 100644 Jellyfin.Data/Attributes/OpenApiIgnoreEnumAttribute.cs create mode 100644 Jellyfin.Data/Enums/CollectionType.cs create mode 100644 Jellyfin.Server/Filters/IgnoreEnumSchemaFilter.cs delete mode 100644 MediaBrowser.Model/Entities/CollectionType.cs diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs index e685d252e4..7d53263a66 100644 --- a/Emby.Dlna/ContentDirectory/ControlHandler.cs +++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs @@ -565,30 +565,18 @@ namespace Emby.Dlna.ContentDirectory if (stubType != StubType.Folder && item is IHasCollectionType collectionFolder) { - var collectionType = collectionFolder.CollectionType; - if (string.Equals(CollectionType.Music, collectionType, StringComparison.OrdinalIgnoreCase)) + switch (collectionFolder.CollectionType) { - return GetMusicFolders(item, user, stubType, sort, startIndex, limit); - } - - if (string.Equals(CollectionType.Movies, collectionType, StringComparison.OrdinalIgnoreCase)) - { - return GetMovieFolders(item, user, stubType, sort, startIndex, limit); - } - - if (string.Equals(CollectionType.TvShows, collectionType, StringComparison.OrdinalIgnoreCase)) - { - return GetTvFolders(item, user, stubType, sort, startIndex, limit); - } - - if (string.Equals(CollectionType.Folders, collectionType, StringComparison.OrdinalIgnoreCase)) - { - return GetFolders(user, startIndex, limit); - } - - if (string.Equals(CollectionType.LiveTv, collectionType, StringComparison.OrdinalIgnoreCase)) - { - return GetLiveTvChannels(user, sort, startIndex, limit); + case CollectionType.Music: + return GetMusicFolders(item, user, stubType, sort, startIndex, limit); + case CollectionType.Movies: + return GetMovieFolders(item, user, stubType, sort, startIndex, limit); + case CollectionType.TvShows: + return GetTvFolders(item, user, stubType, sort, startIndex, limit); + case CollectionType.Folders: + return GetFolders(user, startIndex, limit); + case CollectionType.LiveTv: + return GetLiveTvChannels(user, sort, startIndex, limit); } } diff --git a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs index 8a0e627b9c..6e8f77977e 100644 --- a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs +++ b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs @@ -30,47 +30,43 @@ namespace Emby.Server.Implementations.Images BaseItemKind[] includeItemTypes; - if (string.Equals(viewType, CollectionType.Movies, StringComparison.Ordinal)) + switch (viewType) { - includeItemTypes = new[] { BaseItemKind.Movie }; - } - else if (string.Equals(viewType, CollectionType.TvShows, StringComparison.Ordinal)) - { - includeItemTypes = new[] { BaseItemKind.Series }; - } - else if (string.Equals(viewType, CollectionType.Music, StringComparison.Ordinal)) - { - includeItemTypes = new[] { BaseItemKind.MusicAlbum }; - } - else if (string.Equals(viewType, CollectionType.MusicVideos, StringComparison.Ordinal)) - { - includeItemTypes = new[] { BaseItemKind.MusicVideo }; - } - else if (string.Equals(viewType, CollectionType.Books, StringComparison.Ordinal)) - { - includeItemTypes = new[] { BaseItemKind.Book, BaseItemKind.AudioBook }; - } - else if (string.Equals(viewType, CollectionType.BoxSets, StringComparison.Ordinal)) - { - includeItemTypes = new[] { BaseItemKind.BoxSet }; - } - else if (string.Equals(viewType, CollectionType.HomeVideos, StringComparison.Ordinal) || string.Equals(viewType, CollectionType.Photos, StringComparison.Ordinal)) - { - includeItemTypes = new[] { BaseItemKind.Video, BaseItemKind.Photo }; - } - else - { - includeItemTypes = new[] { BaseItemKind.Video, BaseItemKind.Audio, BaseItemKind.Photo, BaseItemKind.Movie, BaseItemKind.Series }; + case CollectionType.Movies: + includeItemTypes = new[] { BaseItemKind.Movie }; + break; + case CollectionType.TvShows: + includeItemTypes = new[] { BaseItemKind.Series }; + break; + case CollectionType.Music: + includeItemTypes = new[] { BaseItemKind.MusicAlbum }; + break; + case CollectionType.MusicVideos: + includeItemTypes = new[] { BaseItemKind.MusicVideo }; + break; + case CollectionType.Books: + includeItemTypes = new[] { BaseItemKind.Book, BaseItemKind.AudioBook }; + break; + case CollectionType.BoxSets: + includeItemTypes = new[] { BaseItemKind.BoxSet }; + break; + case CollectionType.HomeVideos: + case CollectionType.Photos: + includeItemTypes = new[] { BaseItemKind.Video, BaseItemKind.Photo }; + break; + default: + includeItemTypes = new[] { BaseItemKind.Video, BaseItemKind.Audio, BaseItemKind.Photo, BaseItemKind.Movie, BaseItemKind.Series }; + break; } - var recursive = !string.Equals(CollectionType.Playlists, viewType, StringComparison.OrdinalIgnoreCase); + var recursive = viewType != CollectionType.Playlists; return view.GetItemList(new InternalItemsQuery { CollapseBoxSetItems = false, Recursive = recursive, DtoOptions = new DtoOptions(false), - ImageTypes = new ImageType[] { ImageType.Primary }, + ImageTypes = new[] { ImageType.Primary }, Limit = 8, OrderBy = new[] { diff --git a/Emby.Server.Implementations/Images/DynamicImageProvider.cs b/Emby.Server.Implementations/Images/DynamicImageProvider.cs index 0bd5fdce0a..5de53df739 100644 --- a/Emby.Server.Implementations/Images/DynamicImageProvider.cs +++ b/Emby.Server.Implementations/Images/DynamicImageProvider.cs @@ -36,7 +36,7 @@ namespace Emby.Server.Implementations.Images var view = (UserView)item; var isUsingCollectionStrip = IsUsingCollectionStrip(view); - var recursive = isUsingCollectionStrip && !new[] { CollectionType.BoxSets, CollectionType.Playlists }.Contains(view.ViewType ?? string.Empty, StringComparison.OrdinalIgnoreCase); + var recursive = isUsingCollectionStrip && view?.ViewType is not null && view.ViewType != CollectionType.BoxSets && view.ViewType != CollectionType.Playlists; var result = view.GetItemList(new InternalItemsQuery { @@ -112,14 +112,14 @@ namespace Emby.Server.Implementations.Images private static bool IsUsingCollectionStrip(UserView view) { - string[] collectionStripViewTypes = + CollectionType[] collectionStripViewTypes = { CollectionType.Movies, CollectionType.TvShows, CollectionType.Playlists }; - return collectionStripViewTypes.Contains(view.ViewType ?? string.Empty); + return view?.ViewType is not null && collectionStripViewTypes.Contains(view.ViewType.Value); } protected override string CreateImage(BaseItem item, IReadOnlyCollection<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 5c76e77be1..f40177fa77 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -525,14 +525,14 @@ namespace Emby.Server.Implementations.Library IDirectoryService directoryService, IItemResolver[] resolvers, Folder parent = null, - string collectionType = null, + CollectionType? collectionType = null, LibraryOptions libraryOptions = null) { ArgumentNullException.ThrowIfNull(fileInfo); var fullPath = fileInfo.FullName; - if (string.IsNullOrEmpty(collectionType) && parent is not null) + if (collectionType is null && parent is not null) { collectionType = GetContentTypeOverride(fullPath, true); } @@ -635,7 +635,7 @@ namespace Emby.Server.Implementations.Library return !args.ContainsFileSystemEntryByName(".ignore"); } - public IEnumerable<BaseItem> ResolvePaths(IEnumerable<FileSystemMetadata> files, IDirectoryService directoryService, Folder parent, LibraryOptions libraryOptions, string collectionType = null) + public IEnumerable<BaseItem> ResolvePaths(IEnumerable<FileSystemMetadata> files, IDirectoryService directoryService, Folder parent, LibraryOptions libraryOptions, CollectionType? collectionType = null) { return ResolvePaths(files, directoryService, parent, libraryOptions, collectionType, EntityResolvers); } @@ -645,7 +645,7 @@ namespace Emby.Server.Implementations.Library IDirectoryService directoryService, Folder parent, LibraryOptions libraryOptions, - string collectionType, + CollectionType? collectionType, IItemResolver[] resolvers) { var fileList = files.Where(i => !IgnoreFile(i, parent)).ToList(); @@ -675,7 +675,7 @@ namespace Emby.Server.Implementations.Library IReadOnlyList<FileSystemMetadata> fileList, IDirectoryService directoryService, Folder parent, - string collectionType, + CollectionType? collectionType, IItemResolver[] resolvers, LibraryOptions libraryOptions) { @@ -1514,7 +1514,7 @@ namespace Emby.Server.Implementations.Library { if (item is UserView view) { - if (string.Equals(view.ViewType, CollectionType.LiveTv, StringComparison.Ordinal)) + if (view.ViewType == CollectionType.LiveTv) { return new[] { view.Id }; } @@ -1543,13 +1543,13 @@ namespace Emby.Server.Implementations.Library } // Handle grouping - if (user is not null && !string.IsNullOrEmpty(view.ViewType) && UserView.IsEligibleForGrouping(view.ViewType) + if (user is not null && view.ViewType != CollectionType.Unknown && UserView.IsEligibleForGrouping(view.ViewType) && user.GetPreference(PreferenceKind.GroupedFolders).Length > 0) { return GetUserRootFolder() .GetChildren(user, true) .OfType<CollectionFolder>() - .Where(i => string.IsNullOrEmpty(i.CollectionType) || string.Equals(i.CollectionType, view.ViewType, StringComparison.OrdinalIgnoreCase)) + .Where(i => i.CollectionType is null || i.CollectionType == view.ViewType) .Where(i => user.IsFolderGrouped(i.Id)) .SelectMany(i => GetTopParentIdsForQuery(i, user)); } @@ -2065,16 +2065,16 @@ namespace Emby.Server.Implementations.Library : collectionFolder.GetLibraryOptions(); } - public string GetContentType(BaseItem item) + public CollectionType? GetContentType(BaseItem item) { - string configuredContentType = GetConfiguredContentType(item, false); - if (!string.IsNullOrEmpty(configuredContentType)) + var configuredContentType = GetConfiguredContentType(item, false); + if (configuredContentType is not null) { return configuredContentType; } configuredContentType = GetConfiguredContentType(item, true); - if (!string.IsNullOrEmpty(configuredContentType)) + if (configuredContentType is not null) { return configuredContentType; } @@ -2082,31 +2082,31 @@ namespace Emby.Server.Implementations.Library return GetInheritedContentType(item); } - public string GetInheritedContentType(BaseItem item) + public CollectionType? GetInheritedContentType(BaseItem item) { var type = GetTopFolderContentType(item); - if (!string.IsNullOrEmpty(type)) + if (type is not null) { return type; } return item.GetParents() .Select(GetConfiguredContentType) - .LastOrDefault(i => !string.IsNullOrEmpty(i)); + .LastOrDefault(i => i is not null); } - public string GetConfiguredContentType(BaseItem item) + public CollectionType? GetConfiguredContentType(BaseItem item) { return GetConfiguredContentType(item, false); } - public string GetConfiguredContentType(string path) + public CollectionType? GetConfiguredContentType(string path) { return GetContentTypeOverride(path, false); } - public string GetConfiguredContentType(BaseItem item, bool inheritConfiguredPath) + public CollectionType? GetConfiguredContentType(BaseItem item, bool inheritConfiguredPath) { if (item is ICollectionFolder collectionFolder) { @@ -2116,16 +2116,21 @@ namespace Emby.Server.Implementations.Library return GetContentTypeOverride(item.ContainingFolderPath, inheritConfiguredPath); } - private string GetContentTypeOverride(string path, bool inherit) + private CollectionType? GetContentTypeOverride(string path, bool inherit) { var nameValuePair = _configurationManager.Configuration.ContentTypes .FirstOrDefault(i => _fileSystem.AreEqual(i.Name, path) || (inherit && !string.IsNullOrEmpty(i.Name) && _fileSystem.ContainsSubPath(i.Name, path))); - return nameValuePair?.Value; + if (Enum.TryParse<CollectionType>(nameValuePair?.Value, out var collectionType)) + { + return collectionType; + } + + return null; } - private string GetTopFolderContentType(BaseItem item) + private CollectionType? GetTopFolderContentType(BaseItem item) { if (item is null) { @@ -2147,13 +2152,13 @@ namespace Emby.Server.Implementations.Library .OfType<ICollectionFolder>() .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.Contains(item.Path)) .Select(i => i.CollectionType) - .FirstOrDefault(i => !string.IsNullOrEmpty(i)); + .FirstOrDefault(i => i is not null); } public UserView GetNamedView( User user, string name, - string viewType, + CollectionType? viewType, string sortName) { return GetNamedView(user, name, Guid.Empty, viewType, sortName); @@ -2161,13 +2166,13 @@ namespace Emby.Server.Implementations.Library public UserView GetNamedView( string name, - string viewType, + CollectionType viewType, string sortName) { var path = Path.Combine( _configurationManager.ApplicationPaths.InternalMetadataPath, "views", - _fileSystem.GetValidFilename(viewType)); + _fileSystem.GetValidFilename(viewType.ToString())); var id = GetNewItemId(path + "_namedview_" + name, typeof(UserView)); @@ -2207,13 +2212,13 @@ namespace Emby.Server.Implementations.Library User user, string name, Guid parentId, - string viewType, + CollectionType? viewType, string sortName) { var parentIdString = parentId.Equals(default) ? null : parentId.ToString("N", CultureInfo.InvariantCulture); - var idValues = "38_namedview_" + name + user.Id.ToString("N", CultureInfo.InvariantCulture) + (parentIdString ?? string.Empty) + (viewType ?? string.Empty); + var idValues = "38_namedview_" + name + user.Id.ToString("N", CultureInfo.InvariantCulture) + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.Empty); var id = GetNewItemId(idValues, typeof(UserView)); @@ -2269,7 +2274,7 @@ namespace Emby.Server.Implementations.Library public UserView GetShadowView( BaseItem parent, - string viewType, + CollectionType? viewType, string sortName) { ArgumentNullException.ThrowIfNull(parent); @@ -2277,7 +2282,7 @@ namespace Emby.Server.Implementations.Library var name = parent.Name; var parentId = parent.Id; - var idValues = "38_namedview_" + name + parentId + (viewType ?? string.Empty); + var idValues = "38_namedview_" + name + parentId + (viewType?.ToString() ?? string.Empty); var id = GetNewItemId(idValues, typeof(UserView)); @@ -2334,7 +2339,7 @@ namespace Emby.Server.Implementations.Library public UserView GetNamedView( string name, Guid parentId, - string viewType, + CollectionType? viewType, string sortName, string uniqueId) { @@ -2343,7 +2348,7 @@ namespace Emby.Server.Implementations.Library var parentIdString = parentId.Equals(default) ? null : parentId.ToString("N", CultureInfo.InvariantCulture); - var idValues = "37_namedview_" + name + (parentIdString ?? string.Empty) + (viewType ?? string.Empty); + var idValues = "37_namedview_" + name + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.Empty); if (!string.IsNullOrEmpty(uniqueId)) { idValues += uniqueId; @@ -2378,7 +2383,7 @@ namespace Emby.Server.Implementations.Library isNew = true; } - if (!string.Equals(viewType, item.ViewType, StringComparison.OrdinalIgnoreCase)) + if (viewType != item.ViewType) { item.ViewType = viewType; item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult(); diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs index 862f144e68..ac423ed091 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs @@ -10,11 +10,11 @@ using Emby.Naming.Audio; using Emby.Naming.AudioBook; using Emby.Naming.Common; using Emby.Naming.Video; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Resolvers; -using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; namespace Emby.Server.Implementations.Library.Resolvers.Audio @@ -40,7 +40,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio public MultiItemResolverResult ResolveMultiple( Folder parent, List<FileSystemMetadata> files, - string collectionType, + CollectionType? collectionType, IDirectoryService directoryService) { var result = ResolveMultipleInternal(parent, files, collectionType); @@ -59,9 +59,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio private MultiItemResolverResult ResolveMultipleInternal( Folder parent, List<FileSystemMetadata> files, - string collectionType) + CollectionType? collectionType) { - if (string.Equals(collectionType, CollectionType.Books, StringComparison.OrdinalIgnoreCase)) + if (collectionType == CollectionType.Books) { return ResolveMultipleAudio(parent, files, true); } @@ -80,7 +80,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio var collectionType = args.GetCollectionType(); - var isBooksCollectionType = string.Equals(collectionType, CollectionType.Books, StringComparison.OrdinalIgnoreCase); + var isBooksCollectionType = collectionType == CollectionType.Books; if (args.IsDirectory) { @@ -102,7 +102,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio return null; } - var isMixedCollectionType = string.IsNullOrEmpty(collectionType); + var isMixedCollectionType = collectionType is null; // For conflicting extensions, give priority to videos if (isMixedCollectionType && VideoResolver.IsVideoFile(args.Path, _namingOptions)) @@ -112,7 +112,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio MediaBrowser.Controller.Entities.Audio.Audio item = null; - var isMusicCollectionType = string.Equals(collectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase); + var isMusicCollectionType = collectionType == CollectionType.Music; // Use regular audio type for mixed libraries, owned items and music if (isMixedCollectionType || diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs index bbc70701cb..06e292f4cf 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using Emby.Naming.Audio; using Emby.Naming.Common; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; @@ -54,7 +55,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio protected override MusicAlbum Resolve(ItemResolveArgs args) { var collectionType = args.GetCollectionType(); - var isMusicMediaFolder = string.Equals(collectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase); + var isMusicMediaFolder = collectionType == CollectionType.Music; // If there's a collection type and it's not music, don't allow it. if (!isMusicMediaFolder) diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs index c858dc53d9..7d6f97b121 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using System.Threading.Tasks; using Emby.Naming.Common; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; @@ -64,7 +65,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio var collectionType = args.GetCollectionType(); - var isMusicMediaFolder = string.Equals(collectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase); + var isMusicMediaFolder = collectionType == CollectionType.Music; // If there's a collection type and it's not music, it can't be a music artist if (!isMusicMediaFolder) diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs index 73861ff599..b76bfe4274 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs @@ -5,6 +5,7 @@ using System; using System.IO; using System.Linq; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -22,7 +23,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books var collectionType = args.GetCollectionType(); // Only process items that are in a collection folder containing books - if (!string.Equals(collectionType, CollectionType.Books, StringComparison.OrdinalIgnoreCase)) + if (collectionType != CollectionType.Books) { return null; } diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index 0b65bf921e..50fd8b8779 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Text.RegularExpressions; using Emby.Naming.Common; using Emby.Naming.Video; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; @@ -28,13 +29,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies { private readonly IImageProcessor _imageProcessor; - private string[] _validCollectionTypes = new[] + private static readonly CollectionType[] _validCollectionTypes = new[] { - CollectionType.Movies, - CollectionType.HomeVideos, - CollectionType.MusicVideos, - CollectionType.TvShows, - CollectionType.Photos + CollectionType.Movies, + CollectionType.HomeVideos, + CollectionType.MusicVideos, + CollectionType.TvShows, + CollectionType.Photos }; /// <summary> @@ -63,7 +64,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies public MultiItemResolverResult ResolveMultiple( Folder parent, List<FileSystemMetadata> files, - string collectionType, + CollectionType? collectionType, IDirectoryService directoryService) { var result = ResolveMultipleInternal(parent, files, collectionType); @@ -99,17 +100,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies Video movie = null; var files = args.GetActualFileSystemChildren().ToList(); - if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase)) + if (collectionType == CollectionType.MusicVideos) { movie = FindMovie<MusicVideo>(args, args.Path, args.Parent, files, DirectoryService, collectionType, false); } - if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase)) + if (collectionType == CollectionType.HomeVideos) { movie = FindMovie<Video>(args, args.Path, args.Parent, files, DirectoryService, collectionType, false); } - if (string.IsNullOrEmpty(collectionType)) + if (collectionType is null) { // Owned items will be caught by the video extra resolver if (args.Parent is null) @@ -125,7 +126,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies movie = FindMovie<Movie>(args, args.Path, args.Parent, files, DirectoryService, collectionType, true); } - if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase)) + if (collectionType == CollectionType.Movies) { movie = FindMovie<Movie>(args, args.Path, args.Parent, files, DirectoryService, collectionType, true); } @@ -146,22 +147,21 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies Video item = null; - if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase)) + if (collectionType == CollectionType.MusicVideos) { item = ResolveVideo<MusicVideo>(args, false); } // To find a movie file, the collection type must be movies or boxsets - else if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase)) + else if (collectionType == CollectionType.Movies) { item = ResolveVideo<Movie>(args, true); } - else if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) || - string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase)) + else if (collectionType == CollectionType.HomeVideos || collectionType == CollectionType.Photos) { item = ResolveVideo<Video>(args, false); } - else if (string.IsNullOrEmpty(collectionType)) + else if (collectionType is null) { if (args.HasParent<Series>()) { @@ -188,25 +188,24 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies private MultiItemResolverResult ResolveMultipleInternal( Folder parent, List<FileSystemMetadata> files, - string collectionType) + CollectionType? collectionType) { if (IsInvalid(parent, collectionType)) { return null; } - if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase)) + if (collectionType is CollectionType.MusicVideos) { return ResolveVideos<MusicVideo>(parent, files, true, collectionType, false); } - if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) || - string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase)) + if (collectionType == CollectionType.HomeVideos || collectionType == CollectionType.Photos) { return ResolveVideos<Video>(parent, files, false, collectionType, false); } - if (string.IsNullOrEmpty(collectionType)) + if (collectionType is null) { // Owned items should just use the plain video type if (parent is null) @@ -222,12 +221,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies return ResolveVideos<Movie>(parent, files, false, collectionType, true); } - if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase)) + if (collectionType == CollectionType.Movies) { return ResolveVideos<Movie>(parent, files, true, collectionType, true); } - if (string.Equals(collectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) + if (collectionType == CollectionType.TvShows) { return ResolveVideos<Episode>(parent, files, false, collectionType, true); } @@ -239,13 +238,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies Folder parent, IEnumerable<FileSystemMetadata> fileSystemEntries, bool supportMultiEditions, - string collectionType, + CollectionType? collectionType, bool parseName) where T : Video, new() { var files = new List<FileSystemMetadata>(); var leftOver = new List<FileSystemMetadata>(); - var hasCollectionType = !string.IsNullOrEmpty(collectionType); + var hasCollectionType = collectionType is not null; // Loop through each child file/folder and see if we find a video foreach (var child in fileSystemEntries) @@ -398,13 +397,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies /// Finds a movie based on a child file system entries. /// </summary> /// <returns>Movie.</returns> - private T FindMovie<T>(ItemResolveArgs args, string path, Folder parent, List<FileSystemMetadata> fileSystemEntries, IDirectoryService directoryService, string collectionType, bool parseName) + private T FindMovie<T>(ItemResolveArgs args, string path, Folder parent, List<FileSystemMetadata> fileSystemEntries, IDirectoryService directoryService, CollectionType? collectionType, bool parseName) where T : Video, new() { var multiDiscFolders = new List<FileSystemMetadata>(); var libraryOptions = args.LibraryOptions; - var supportPhotos = string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && libraryOptions.EnablePhotos; + var supportPhotos = collectionType == CollectionType.HomeVideos && libraryOptions.EnablePhotos; var photos = new List<FileSystemMetadata>(); // Search for a folder rip @@ -460,8 +459,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies var result = ResolveVideos<T>(parent, fileSystemEntries, SupportsMultiVersion, collectionType, parseName) ?? new MultiItemResolverResult(); - var isPhotosCollection = string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) - || string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase); + var isPhotosCollection = collectionType == CollectionType.HomeVideos || collectionType == CollectionType.Photos; if (!isPhotosCollection && result.Items.Count == 1) { var videoPath = result.Items[0].Path; @@ -562,7 +560,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies return returnVideo; } - private bool IsInvalid(Folder parent, ReadOnlySpan<char> collectionType) + private bool IsInvalid(Folder parent, CollectionType? collectionType) { if (parent is not null) { @@ -572,12 +570,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies } } - if (collectionType.IsEmpty) + if (collectionType is null) { return false; } - return !_validCollectionTypes.Contains(collectionType, StringComparison.OrdinalIgnoreCase); + return !_validCollectionTypes.Contains(collectionType.Value); } } } diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs index 7dd0ab1853..29d5407003 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs @@ -2,6 +2,7 @@ using System; using Emby.Naming.Common; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -45,8 +46,8 @@ namespace Emby.Server.Implementations.Library.Resolvers // Must be an image file within a photo collection var collectionType = args.GetCollectionType(); - if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase) - || (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.LibraryOptions.EnablePhotos)) + if (collectionType == CollectionType.Photos + || (collectionType == CollectionType.HomeVideos && args.LibraryOptions.EnablePhotos)) { if (HasPhotos(args)) { diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs index c860391fc2..d166ac37fb 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using Emby.Naming.Common; using Emby.Naming.Video; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; @@ -60,8 +61,8 @@ namespace Emby.Server.Implementations.Library.Resolvers // Must be an image file within a photo collection var collectionType = args.CollectionType; - if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase) - || (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.LibraryOptions.EnablePhotos)) + if (collectionType == CollectionType.Photos + || (collectionType == CollectionType.HomeVideos && args.LibraryOptions.EnablePhotos)) { if (IsImageFile(args.Path, _imageProcessor)) { diff --git a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs index 5d569009d3..d4b3722c9a 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs @@ -5,6 +5,7 @@ using System; using System.IO; using System.Linq; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Playlists; @@ -19,9 +20,9 @@ namespace Emby.Server.Implementations.Library.Resolvers /// </summary> public class PlaylistResolver : GenericFolderResolver<Playlist> { - private string[] _musicPlaylistCollectionTypes = + private CollectionType?[] _musicPlaylistCollectionTypes = { - string.Empty, + null, CollectionType.Music }; @@ -62,7 +63,7 @@ namespace Emby.Server.Implementations.Library.Resolvers // Check if this is a music playlist file // It should have the correct collection type and a supported file extension - else if (_musicPlaylistCollectionTypes.Contains(args.CollectionType ?? string.Empty, StringComparison.OrdinalIgnoreCase)) + else if (_musicPlaylistCollectionTypes.Contains(args.CollectionType)) { var extension = Path.GetExtension(args.Path.AsSpan()); if (Playlist.SupportedExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) diff --git a/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs b/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs index 6bb9996415..3d91ed242b 100644 --- a/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs @@ -5,6 +5,7 @@ using System; using System.IO; using System.Linq; +using Jellyfin.Data.Enums; using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -62,7 +63,7 @@ namespace Emby.Server.Implementations.Library.Resolvers return null; } - private string GetCollectionType(ItemResolveArgs args) + private CollectionType? GetCollectionType(ItemResolveArgs args) { return args.FileSystemChildren .Where(i => @@ -78,7 +79,8 @@ namespace Emby.Server.Implementations.Library.Resolvers } }) .Select(i => _fileSystem.GetFileNameWithoutExtension(i)) - .FirstOrDefault(); + .Select(i => Enum.TryParse<CollectionType>(i, out var collectionType) ? collectionType : (CollectionType?)null) + .FirstOrDefault(i => i is not null); } } } diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs index 392ee4c771..8274881be1 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs @@ -3,6 +3,7 @@ using System; using System.Linq; using Emby.Naming.Common; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; @@ -48,9 +49,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV // If the parent is a Season or Series and the parent is not an extras folder, then this is an Episode if the VideoResolver returns something // Also handle flat tv folders - if (season is not null || - string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) || - args.HasParent<Series>()) + if (season is not null + || args.GetCollectionType() == CollectionType.TvShows + || args.HasParent<Series>()) { var episode = ResolveVideo<Episode>(args, false); diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs index d4f275bed4..2ae1138a53 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs @@ -8,6 +8,7 @@ using System.IO; using Emby.Naming.Common; using Emby.Naming.TV; using Emby.Naming.Video; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Resolvers; @@ -59,11 +60,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV var seriesInfo = Naming.TV.SeriesResolver.Resolve(_namingOptions, args.Path); var collectionType = args.GetCollectionType(); - if (string.Equals(collectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) + if (collectionType == CollectionType.TvShows) { // TODO refactor into separate class or something, this is copied from LibraryManager.GetConfiguredContentType var configuredContentType = args.GetConfiguredContentType(); - if (!string.Equals(configuredContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) + if (configuredContentType != CollectionType.TvShows) { return new Series { @@ -72,7 +73,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV }; } } - else if (string.IsNullOrEmpty(collectionType)) + else if (collectionType is null) { if (args.ContainsFileSystemEntryByName("tvshow.nfo")) { diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs index 2c3dc18574..df56109962 100644 --- a/Emby.Server.Implementations/Library/UserViewManager.cs +++ b/Emby.Server.Implementations/Library/UserViewManager.cs @@ -8,7 +8,6 @@ using System.Linq; using System.Threading; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; -using Jellyfin.Extensions; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; @@ -64,8 +63,8 @@ namespace Emby.Server.Implementations.Library var collectionFolder = folder as ICollectionFolder; var folderViewType = collectionFolder?.CollectionType; - // Playlist library requires special handling because the folder only refrences user playlists - if (string.Equals(folderViewType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase)) + // Playlist library requires special handling because the folder only references user playlists + if (folderViewType == CollectionType.Playlists) { var items = folder.GetItemList(new InternalItemsQuery(user) { @@ -90,7 +89,7 @@ namespace Emby.Server.Implementations.Library continue; } - if (query.PresetViews.Contains(folderViewType ?? string.Empty, StringComparison.OrdinalIgnoreCase)) + if (query.PresetViews.Contains(folderViewType)) { list.Add(GetUserView(folder, folderViewType, string.Empty)); } @@ -102,14 +101,14 @@ namespace Emby.Server.Implementations.Library foreach (var viewType in new[] { CollectionType.Movies, CollectionType.TvShows }) { - var parents = groupedFolders.Where(i => string.Equals(i.CollectionType, viewType, StringComparison.OrdinalIgnoreCase) || string.IsNullOrEmpty(i.CollectionType)) + var parents = groupedFolders.Where(i => i.CollectionType == viewType || i.CollectionType is null) .ToList(); if (parents.Count > 0) { - var localizationKey = string.Equals(viewType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) ? - "TvShows" : - "Movies"; + var localizationKey = viewType == CollectionType.TvShows + ? "TvShows" + : "Movies"; list.Add(GetUserView(parents, viewType, localizationKey, string.Empty, user, query.PresetViews)); } @@ -164,14 +163,14 @@ namespace Emby.Server.Implementations.Library .ToArray(); } - public UserView GetUserSubViewWithName(string name, Guid parentId, string type, string sortName) + public UserView GetUserSubViewWithName(string name, Guid parentId, CollectionType? type, string sortName) { var uniqueId = parentId + "subview" + type; return _libraryManager.GetNamedView(name, parentId, type, sortName, uniqueId); } - public UserView GetUserSubView(Guid parentId, string type, string localizationKey, string sortName) + public UserView GetUserSubView(Guid parentId, CollectionType? type, string localizationKey, string sortName) { var name = _localizationManager.GetLocalizedString(localizationKey); @@ -180,15 +179,15 @@ namespace Emby.Server.Implementations.Library private Folder GetUserView( List<ICollectionFolder> parents, - string viewType, + CollectionType? viewType, string localizationKey, string sortName, User user, - string[] presetViews) + CollectionType?[] presetViews) { - if (parents.Count == 1 && parents.All(i => string.Equals(i.CollectionType, viewType, StringComparison.OrdinalIgnoreCase))) + if (parents.Count == 1 && parents.All(i => i.CollectionType == viewType)) { - if (!presetViews.Contains(viewType, StringComparison.OrdinalIgnoreCase)) + if (!presetViews.Contains(viewType)) { return (Folder)parents[0]; } @@ -200,7 +199,7 @@ namespace Emby.Server.Implementations.Library return _libraryManager.GetNamedView(user, name, viewType, sortName); } - public UserView GetUserView(Folder parent, string viewType, string sortName) + public UserView GetUserView(Folder parent, CollectionType? viewType, string sortName) { return _libraryManager.GetShadowView(parent, viewType, sortName); } @@ -280,7 +279,7 @@ namespace Emby.Server.Implementations.Library var isPlayed = request.IsPlayed; - if (parents.OfType<ICollectionFolder>().Any(i => string.Equals(i.CollectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase))) + if (parents.OfType<ICollectionFolder>().Any(i => i.CollectionType == CollectionType.Music)) { isPlayed = null; } @@ -306,11 +305,11 @@ namespace Emby.Server.Implementations.Library var hasCollectionType = parents.OfType<UserView>().ToArray(); if (hasCollectionType.Length > 0) { - if (hasCollectionType.All(i => string.Equals(i.CollectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))) + if (hasCollectionType.All(i => i.CollectionType == CollectionType.Movies)) { includeItemTypes = new[] { BaseItemKind.Movie }; } - else if (hasCollectionType.All(i => string.Equals(i.CollectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))) + else if (hasCollectionType.All(i => i.CollectionType == CollectionType.TvShows)) { includeItemTypes = new[] { BaseItemKind.Episode }; } diff --git a/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs b/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs index d67caa52dc..5c616d5349 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs @@ -25,7 +25,7 @@ namespace Emby.Server.Implementations.Playlists public override bool SupportsInheritedParentImages => false; [JsonIgnore] - public override string CollectionType => MediaBrowser.Model.Entities.CollectionType.Playlists; + public override CollectionType? CollectionType => Jellyfin.Data.Enums.CollectionType.Playlists; protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user) { diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs index 51f04fa276..062e1062d7 100644 --- a/Jellyfin.Api/Controllers/GenresController.cs +++ b/Jellyfin.Api/Controllers/GenresController.cs @@ -131,8 +131,8 @@ public class GenresController : BaseJellyfinApiController QueryResult<(BaseItem, ItemCounts)> result; if (parentItem is ICollectionFolder parentCollectionFolder - && (string.Equals(parentCollectionFolder.CollectionType, CollectionType.Music, StringComparison.Ordinal) - || string.Equals(parentCollectionFolder.CollectionType, CollectionType.MusicVideos, StringComparison.Ordinal))) + && (parentCollectionFolder.CollectionType == CollectionType.Music + || parentCollectionFolder.CollectionType == CollectionType.MusicVideos)) { result = _libraryManager.GetMusicGenres(query); } diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs index 504f2fa1d7..3be891b930 100644 --- a/Jellyfin.Api/Controllers/ItemUpdateController.cs +++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Constants; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -164,18 +165,16 @@ public class ItemUpdateController : BaseJellyfinApiController var inheritedContentType = _libraryManager.GetInheritedContentType(item); var configuredContentType = _libraryManager.GetConfiguredContentType(item); - if (string.IsNullOrWhiteSpace(inheritedContentType) || - !string.IsNullOrWhiteSpace(configuredContentType)) + if (inheritedContentType is null || configuredContentType is not null) { info.ContentTypeOptions = GetContentTypeOptions(true).ToArray(); info.ContentType = configuredContentType; - if (string.IsNullOrWhiteSpace(inheritedContentType) - || string.Equals(inheritedContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) + if (inheritedContentType is null || inheritedContentType == CollectionType.TvShows) { info.ContentTypeOptions = info.ContentTypeOptions .Where(i => string.IsNullOrWhiteSpace(i.Value) - || string.Equals(i.Value, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) + || string.Equals(i.Value, "TvShows", StringComparison.OrdinalIgnoreCase)) .ToArray(); } } diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 891cf88a7c..3920d65992 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -269,13 +269,13 @@ public class ItemsController : BaseJellyfinApiController folder = _libraryManager.GetUserRootFolder(); } - string? collectionType = null; + CollectionType? collectionType = null; if (folder is IHasCollectionType hasCollectionType) { collectionType = hasCollectionType.CollectionType; } - if (string.Equals(collectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase)) + if (collectionType == CollectionType.Playlists) { recursive = true; includeItemTypes = new[] { BaseItemKind.Playlist }; diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index 21941ff942..3cd78b0863 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -788,7 +788,7 @@ public class LibraryController : BaseJellyfinApiController [Authorize(Policy = Policies.FirstTimeSetupOrDefault)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<LibraryOptionsResultDto> GetLibraryOptionsInfo( - [FromQuery] string? libraryContentType, + [FromQuery] CollectionType? libraryContentType, [FromQuery] bool isNewLibrary = false) { var result = new LibraryOptionsResultDto(); @@ -922,7 +922,7 @@ public class LibraryController : BaseJellyfinApiController } } - private static string[] GetRepresentativeItemTypes(string? contentType) + private static string[] GetRepresentativeItemTypes(CollectionType? contentType) { return contentType switch { diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs index 838b432340..0ffa3ab1ad 100644 --- a/Jellyfin.Api/Controllers/UserViewsController.cs +++ b/Jellyfin.Api/Controllers/UserViewsController.cs @@ -6,6 +6,7 @@ using System.Linq; using Jellyfin.Api.Extensions; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.UserViewDtos; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -63,7 +64,7 @@ public class UserViewsController : BaseJellyfinApiController public QueryResult<BaseItemDto> GetUserViews( [FromRoute, Required] Guid userId, [FromQuery] bool? includeExternalContent, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] presetViews, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] CollectionType?[] presetViews, [FromQuery] bool includeHidden = false) { var query = new UserViewQuery diff --git a/Jellyfin.Data/Attributes/OpenApiIgnoreEnumAttribute.cs b/Jellyfin.Data/Attributes/OpenApiIgnoreEnumAttribute.cs new file mode 100644 index 0000000000..ff613d9f8d --- /dev/null +++ b/Jellyfin.Data/Attributes/OpenApiIgnoreEnumAttribute.cs @@ -0,0 +1,11 @@ +using System; + +namespace Jellyfin.Data.Attributes; + +/// <summary> +/// Attribute to specify that the enum value is to be ignored when generating the openapi spec. +/// </summary> +[AttributeUsage(AttributeTargets.Field)] +public sealed class OpenApiIgnoreEnumAttribute : Attribute +{ +} diff --git a/Jellyfin.Data/Enums/CollectionType.cs b/Jellyfin.Data/Enums/CollectionType.cs new file mode 100644 index 0000000000..e2044a0bc8 --- /dev/null +++ b/Jellyfin.Data/Enums/CollectionType.cs @@ -0,0 +1,164 @@ +using Jellyfin.Data.Attributes; + +namespace Jellyfin.Data.Enums; + +/// <summary> +/// Collection type. +/// </summary> +public enum CollectionType +{ + /// <summary> + /// Unknown collection. + /// </summary> + Unknown = 0, + + /// <summary> + /// Movies collection. + /// </summary> + Movies = 1, + + /// <summary> + /// Tv shows collection. + /// </summary> + TvShows = 2, + + /// <summary> + /// Music collection. + /// </summary> + Music = 3, + + /// <summary> + /// Music videos collection. + /// </summary> + MusicVideos = 4, + + /// <summary> + /// Trailers collection. + /// </summary> + Trailers = 5, + + /// <summary> + /// Home videos collection. + /// </summary> + HomeVideos = 6, + + /// <summary> + /// Box sets collection. + /// </summary> + BoxSets = 7, + + /// <summary> + /// Books collection. + /// </summary> + Books = 8, + + /// <summary> + /// Photos collection. + /// </summary> + Photos = 9, + + /// <summary> + /// Live tv collection. + /// </summary> + LiveTv = 10, + + /// <summary> + /// Playlists collection. + /// </summary> + Playlists = 11, + + /// <summary> + /// Folders collection. + /// </summary> + Folders = 12, + + /// <summary> + /// Tv show series collection. + /// </summary> + [OpenApiIgnoreEnum] + TvShowSeries = 101, + + /// <summary> + /// Tv genres collection. + /// </summary> + [OpenApiIgnoreEnum] + TvGenres = 102, + + /// <summary> + /// Tv genre collection. + /// </summary> + [OpenApiIgnoreEnum] + TvGenre = 103, + + /// <summary> + /// Tv latest collection. + /// </summary> + [OpenApiIgnoreEnum] + TvLatest = 104, + + /// <summary> + /// Tv next up collection. + /// </summary> + [OpenApiIgnoreEnum] + TvNextUp = 105, + + /// <summary> + /// Tv resume collection. + /// </summary> + [OpenApiIgnoreEnum] + TvResume = 106, + + /// <summary> + /// Tv favorite series collection. + /// </summary> + [OpenApiIgnoreEnum] + TvFavoriteSeries = 107, + + /// <summary> + /// Tv favorite episodes collection. + /// </summary> + [OpenApiIgnoreEnum] + TvFavoriteEpisodes = 108, + + /// <summary> + /// Latest movies collection. + /// </summary> + [OpenApiIgnoreEnum] + MovieLatest = 109, + + /// <summary> + /// Movies to resume collection. + /// </summary> + [OpenApiIgnoreEnum] + MovieResume = 110, + + /// <summary> + /// Movie movie collection. + /// </summary> + [OpenApiIgnoreEnum] + MovieMovies = 111, + + /// <summary> + /// Movie collections collection. + /// </summary> + [OpenApiIgnoreEnum] + MovieCollections = 112, + + /// <summary> + /// Movie favorites collection. + /// </summary> + [OpenApiIgnoreEnum] + MovieFavorites = 113, + + /// <summary> + /// Movie genres collection. + /// </summary> + [OpenApiIgnoreEnum] + MovieGenres = 114, + + /// <summary> + /// Movie genre collection. + /// </summary> + [OpenApiIgnoreEnum] + MovieGenre = 115 +} diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index b7e71a81db..16b58808f3 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -246,6 +246,7 @@ namespace Jellyfin.Server.Extensions // TODO - remove when all types are supported in System.Text.Json c.AddSwaggerTypeMappings(); + c.SchemaFilter<IgnoreEnumSchemaFilter>(); c.OperationFilter<SecurityRequirementsOperationFilter>(); c.OperationFilter<FileResponseFilter>(); c.OperationFilter<FileRequestFilter>(); diff --git a/Jellyfin.Server/Filters/IgnoreEnumSchemaFilter.cs b/Jellyfin.Server/Filters/IgnoreEnumSchemaFilter.cs new file mode 100644 index 0000000000..eb9ad03c21 --- /dev/null +++ b/Jellyfin.Server/Filters/IgnoreEnumSchemaFilter.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Jellyfin.Data.Attributes; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace Jellyfin.Server.Filters; + +/// <summary> +/// Filter to remove ignored enum values. +/// </summary> +public class IgnoreEnumSchemaFilter : ISchemaFilter +{ + /// <inheritdoc /> + public void Apply(OpenApiSchema schema, SchemaFilterContext context) + { + if (context.Type.IsEnum || (Nullable.GetUnderlyingType(context.Type)?.IsEnum ?? false)) + { + var type = context.Type.IsEnum ? context.Type : Nullable.GetUnderlyingType(context.Type); + if (type is null) + { + return; + } + + var enumOpenApiStrings = new List<IOpenApiAny>(); + + foreach (var enumName in Enum.GetNames(type)) + { + var member = type.GetMember(enumName)[0]; + if (!member.GetCustomAttributes<OpenApiIgnoreEnumAttribute>().Any()) + { + enumOpenApiStrings.Add(new OpenApiString(enumName)); + } + } + + schema.Enum = enumOpenApiStrings; + } + } +} diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 9f3e8eec96..87a021d418 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -724,7 +724,7 @@ namespace MediaBrowser.Controller.Entities if (this is IHasCollectionType view) { - if (string.Equals(view.CollectionType, CollectionType.LiveTv, StringComparison.OrdinalIgnoreCase)) + if (view.CollectionType == CollectionType.LiveTv) { return true; } diff --git a/MediaBrowser.Controller/Entities/BasePluginFolder.cs b/MediaBrowser.Controller/Entities/BasePluginFolder.cs index afafaf1c24..4bf21061ca 100644 --- a/MediaBrowser.Controller/Entities/BasePluginFolder.cs +++ b/MediaBrowser.Controller/Entities/BasePluginFolder.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using System.Text.Json.Serialization; +using Jellyfin.Data.Enums; namespace MediaBrowser.Controller.Entities { @@ -11,7 +12,7 @@ namespace MediaBrowser.Controller.Entities public abstract class BasePluginFolder : Folder, ICollectionFolder { [JsonIgnore] - public virtual string? CollectionType => null; + public virtual CollectionType? CollectionType => null; [JsonIgnore] public override bool SupportsInheritedParentImages => false; diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs index f51162f9d2..992bb19bb1 100644 --- a/MediaBrowser.Controller/Entities/CollectionFolder.cs +++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs @@ -11,6 +11,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using Jellyfin.Extensions.Json; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; @@ -69,7 +70,7 @@ namespace MediaBrowser.Controller.Entities [JsonIgnore] public override bool SupportsInheritedParentImages => false; - public string CollectionType { get; set; } + public CollectionType? CollectionType { get; set; } /// <summary> /// Gets the item's children. diff --git a/MediaBrowser.Controller/Entities/ICollectionFolder.cs b/MediaBrowser.Controller/Entities/ICollectionFolder.cs index 89e494ebc3..742691b003 100644 --- a/MediaBrowser.Controller/Entities/ICollectionFolder.cs +++ b/MediaBrowser.Controller/Entities/ICollectionFolder.cs @@ -3,6 +3,7 @@ #pragma warning disable CA1819, CS1591 using System; +using Jellyfin.Data.Enums; namespace MediaBrowser.Controller.Entities { @@ -27,6 +28,6 @@ namespace MediaBrowser.Controller.Entities public interface IHasCollectionType { - string CollectionType { get; } + CollectionType? CollectionType { get; } } } diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index fb50a45462..c0bba60ebc 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -42,7 +42,7 @@ namespace MediaBrowser.Controller.Entities OrderBy = Array.Empty<(ItemSortBy, SortOrder)>(); PersonIds = Array.Empty<Guid>(); PersonTypes = Array.Empty<string>(); - PresetViews = Array.Empty<string>(); + PresetViews = Array.Empty<CollectionType?>(); SeriesStatuses = Array.Empty<SeriesStatus>(); SourceTypes = Array.Empty<SourceType>(); StudioIds = Array.Empty<Guid>(); @@ -248,7 +248,7 @@ namespace MediaBrowser.Controller.Entities public Guid[] TopParentIds { get; set; } - public string[] PresetViews { get; set; } + public CollectionType?[] PresetViews { get; set; } public TrailerType[] TrailerTypes { get; set; } diff --git a/MediaBrowser.Controller/Entities/UserView.cs b/MediaBrowser.Controller/Entities/UserView.cs index 47432ee93e..1f94cf767d 100644 --- a/MediaBrowser.Controller/Entities/UserView.cs +++ b/MediaBrowser.Controller/Entities/UserView.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Text.Json.Serialization; using System.Threading.Tasks; using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.TV; using MediaBrowser.Model.Querying; @@ -16,21 +17,21 @@ namespace MediaBrowser.Controller.Entities { public class UserView : Folder, IHasCollectionType { - private static readonly string[] _viewTypesEligibleForGrouping = new string[] + private static readonly CollectionType?[] _viewTypesEligibleForGrouping = { - Model.Entities.CollectionType.Movies, - Model.Entities.CollectionType.TvShows, - string.Empty + Jellyfin.Data.Enums.CollectionType.Movies, + Jellyfin.Data.Enums.CollectionType.TvShows, + null }; - private static readonly string[] _originalFolderViewTypes = new string[] + private static readonly CollectionType?[] _originalFolderViewTypes = { - Model.Entities.CollectionType.Books, - Model.Entities.CollectionType.MusicVideos, - Model.Entities.CollectionType.HomeVideos, - Model.Entities.CollectionType.Photos, - Model.Entities.CollectionType.Music, - Model.Entities.CollectionType.BoxSets + Jellyfin.Data.Enums.CollectionType.Books, + Jellyfin.Data.Enums.CollectionType.MusicVideos, + Jellyfin.Data.Enums.CollectionType.HomeVideos, + Jellyfin.Data.Enums.CollectionType.Photos, + Jellyfin.Data.Enums.CollectionType.Music, + Jellyfin.Data.Enums.CollectionType.BoxSets }; public static ITVSeriesManager TVSeriesManager { get; set; } @@ -38,7 +39,7 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// Gets or sets the view type. /// </summary> - public string ViewType { get; set; } + public CollectionType? ViewType { get; set; } /// <summary> /// Gets or sets the display parent id. @@ -52,7 +53,7 @@ namespace MediaBrowser.Controller.Entities /// <inheritdoc /> [JsonIgnore] - public string CollectionType => ViewType; + public CollectionType? CollectionType => ViewType; /// <inheritdoc /> [JsonIgnore] @@ -160,7 +161,7 @@ namespace MediaBrowser.Controller.Entities return true; } - return string.Equals(Model.Entities.CollectionType.Playlists, collectionFolder.CollectionType, StringComparison.OrdinalIgnoreCase); + return collectionFolder.CollectionType == Jellyfin.Data.Enums.CollectionType.Playlists; } public static bool IsEligibleForGrouping(Folder folder) @@ -169,14 +170,14 @@ namespace MediaBrowser.Controller.Entities && IsEligibleForGrouping(collectionFolder.CollectionType); } - public static bool IsEligibleForGrouping(string viewType) + public static bool IsEligibleForGrouping(CollectionType? viewType) { - return _viewTypesEligibleForGrouping.Contains(viewType ?? string.Empty, StringComparison.OrdinalIgnoreCase); + return _viewTypesEligibleForGrouping.Contains(viewType); } - public static bool EnableOriginalFolder(string viewType) + public static bool EnableOriginalFolder(CollectionType? viewType) { - return _originalFolderViewTypes.Contains(viewType ?? string.Empty, StringComparison.OrdinalIgnoreCase); + return _originalFolderViewTypes.Contains(viewType); } protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, Providers.MetadataRefreshOptions refreshOptions, Providers.IDirectoryService directoryService, System.Threading.CancellationToken cancellationToken) diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs index c276ab463a..a134735b23 100644 --- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -42,7 +42,7 @@ namespace MediaBrowser.Controller.Entities _tvSeriesManager = tvSeriesManager; } - public QueryResult<BaseItem> GetUserItems(Folder queryParent, Folder displayParent, string viewType, InternalItemsQuery query) + public QueryResult<BaseItem> GetUserItems(Folder queryParent, Folder displayParent, CollectionType? viewType, InternalItemsQuery query) { var user = query.User; @@ -67,49 +67,49 @@ namespace MediaBrowser.Controller.Entities case CollectionType.Movies: return GetMovieFolders(queryParent, user, query); - case SpecialFolder.TvShowSeries: + case CollectionType.TvShowSeries: return GetTvSeries(queryParent, user, query); - case SpecialFolder.TvGenres: + case CollectionType.TvGenres: return GetTvGenres(queryParent, user, query); - case SpecialFolder.TvGenre: + case CollectionType.TvGenre: return GetTvGenreItems(queryParent, displayParent, user, query); - case SpecialFolder.TvResume: + case CollectionType.TvResume: return GetTvResume(queryParent, user, query); - case SpecialFolder.TvNextUp: + case CollectionType.TvNextUp: return GetTvNextUp(queryParent, query); - case SpecialFolder.TvLatest: + case CollectionType.TvLatest: return GetTvLatest(queryParent, user, query); - case SpecialFolder.MovieFavorites: + case CollectionType.MovieFavorites: return GetFavoriteMovies(queryParent, user, query); - case SpecialFolder.MovieLatest: + case CollectionType.MovieLatest: return GetMovieLatest(queryParent, user, query); - case SpecialFolder.MovieGenres: + case CollectionType.MovieGenres: return GetMovieGenres(queryParent, user, query); - case SpecialFolder.MovieGenre: + case CollectionType.MovieGenre: return GetMovieGenreItems(queryParent, displayParent, user, query); - case SpecialFolder.MovieResume: + case CollectionType.MovieResume: return GetMovieResume(queryParent, user, query); - case SpecialFolder.MovieMovies: + case CollectionType.MovieMovies: return GetMovieMovies(queryParent, user, query); - case SpecialFolder.MovieCollections: + case CollectionType.MovieCollections: return GetMovieCollections(user, query); - case SpecialFolder.TvFavoriteEpisodes: + case CollectionType.TvFavoriteEpisodes: return GetFavoriteEpisodes(queryParent, user, query); - case SpecialFolder.TvFavoriteSeries: + case CollectionType.TvFavoriteSeries: return GetFavoriteSeries(queryParent, user, query); default: @@ -146,12 +146,12 @@ namespace MediaBrowser.Controller.Entities var list = new List<BaseItem> { - GetUserView(SpecialFolder.MovieResume, "HeaderContinueWatching", "0", parent), - GetUserView(SpecialFolder.MovieLatest, "Latest", "1", parent), - GetUserView(SpecialFolder.MovieMovies, "Movies", "2", parent), - GetUserView(SpecialFolder.MovieCollections, "Collections", "3", parent), - GetUserView(SpecialFolder.MovieFavorites, "Favorites", "4", parent), - GetUserView(SpecialFolder.MovieGenres, "Genres", "5", parent) + GetUserView(CollectionType.MovieResume, "HeaderContinueWatching", "0", parent), + GetUserView(CollectionType.MovieLatest, "Latest", "1", parent), + GetUserView(CollectionType.MovieMovies, "Movies", "2", parent), + GetUserView(CollectionType.MovieCollections, "Collections", "3", parent), + GetUserView(CollectionType.MovieFavorites, "Favorites", "4", parent), + GetUserView(CollectionType.MovieGenres, "Genres", "5", parent) }; return GetResult(list, query); @@ -264,7 +264,7 @@ namespace MediaBrowser.Controller.Entities } }) .Where(i => i is not null) - .Select(i => GetUserViewWithName(SpecialFolder.MovieGenre, i.SortName, parent)); + .Select(i => GetUserViewWithName(CollectionType.MovieGenre, i.SortName, parent)); return GetResult(genres, query); } @@ -303,13 +303,13 @@ namespace MediaBrowser.Controller.Entities var list = new List<BaseItem> { - GetUserView(SpecialFolder.TvResume, "HeaderContinueWatching", "0", parent), - GetUserView(SpecialFolder.TvNextUp, "HeaderNextUp", "1", parent), - GetUserView(SpecialFolder.TvLatest, "Latest", "2", parent), - GetUserView(SpecialFolder.TvShowSeries, "Shows", "3", parent), - GetUserView(SpecialFolder.TvFavoriteSeries, "HeaderFavoriteShows", "4", parent), - GetUserView(SpecialFolder.TvFavoriteEpisodes, "HeaderFavoriteEpisodes", "5", parent), - GetUserView(SpecialFolder.TvGenres, "Genres", "6", parent) + GetUserView(CollectionType.TvResume, "HeaderContinueWatching", "0", parent), + GetUserView(CollectionType.TvNextUp, "HeaderNextUp", "1", parent), + GetUserView(CollectionType.TvLatest, "Latest", "2", parent), + GetUserView(CollectionType.TvShowSeries, "Shows", "3", parent), + GetUserView(CollectionType.TvFavoriteSeries, "HeaderFavoriteShows", "4", parent), + GetUserView(CollectionType.TvFavoriteEpisodes, "HeaderFavoriteEpisodes", "5", parent), + GetUserView(CollectionType.TvGenres, "Genres", "6", parent) }; return GetResult(list, query); @@ -330,7 +330,7 @@ namespace MediaBrowser.Controller.Entities private QueryResult<BaseItem> GetTvNextUp(Folder parent, InternalItemsQuery query) { - var parentFolders = GetMediaFolders(parent, query.User, new[] { CollectionType.TvShows, string.Empty }); + var parentFolders = GetMediaFolders(parent, query.User, new[] { CollectionType.TvShows }); var result = _tvSeriesManager.GetNextUp( new NextUpQuery @@ -392,7 +392,7 @@ namespace MediaBrowser.Controller.Entities } }) .Where(i => i is not null) - .Select(i => GetUserViewWithName(SpecialFolder.TvGenre, i.SortName, parent)); + .Select(i => GetUserViewWithName(CollectionType.TvGenre, i.SortName, parent)); return GetResult(genres, query); } @@ -943,7 +943,7 @@ namespace MediaBrowser.Controller.Entities .Where(i => user.IsFolderGrouped(i.Id) && UserView.IsEligibleForGrouping(i)); } - private BaseItem[] GetMediaFolders(User user, IEnumerable<string> viewTypes) + private BaseItem[] GetMediaFolders(User user, IEnumerable<CollectionType> viewTypes) { if (user is null) { @@ -952,7 +952,7 @@ namespace MediaBrowser.Controller.Entities { var folder = i as ICollectionFolder; - return folder is not null && viewTypes.Contains(folder.CollectionType ?? string.Empty, StringComparison.OrdinalIgnoreCase); + return folder?.CollectionType is not null && viewTypes.Contains(folder.CollectionType.Value); }).ToArray(); } @@ -961,11 +961,11 @@ namespace MediaBrowser.Controller.Entities { var folder = i as ICollectionFolder; - return folder is not null && viewTypes.Contains(folder.CollectionType ?? string.Empty, StringComparison.OrdinalIgnoreCase); + return folder?.CollectionType is not null && viewTypes.Contains(folder.CollectionType.Value); }).ToArray(); } - private BaseItem[] GetMediaFolders(Folder parent, User user, IEnumerable<string> viewTypes) + private BaseItem[] GetMediaFolders(Folder parent, User user, IEnumerable<CollectionType> viewTypes) { if (parent is null || parent is UserView) { @@ -975,12 +975,12 @@ namespace MediaBrowser.Controller.Entities return new BaseItem[] { parent }; } - private UserView GetUserViewWithName(string type, string sortName, BaseItem parent) + private UserView GetUserViewWithName(CollectionType? type, string sortName, BaseItem parent) { - return _userViewManager.GetUserSubView(parent.Id, parent.Id.ToString("N", CultureInfo.InvariantCulture), type, sortName); + return _userViewManager.GetUserSubView(parent.Id, type, parent.Id.ToString("N", CultureInfo.InvariantCulture), sortName); } - private UserView GetUserView(string type, string localizationKey, string sortName, BaseItem parent) + private UserView GetUserView(CollectionType? type, string localizationKey, string sortName, BaseItem parent) { return _userViewManager.GetUserSubView(parent.Id, type, localizationKey, sortName); } diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index 52d546a4f1..9ec22324f2 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -79,7 +79,7 @@ namespace MediaBrowser.Controller.Library IDirectoryService directoryService, Folder parent, LibraryOptions libraryOptions, - string collectionType = null); + CollectionType? collectionType = null); /// <summary> /// Gets a Person. @@ -256,28 +256,28 @@ namespace MediaBrowser.Controller.Library /// </summary> /// <param name="item">The item.</param> /// <returns>System.String.</returns> - string GetContentType(BaseItem item); + CollectionType? GetContentType(BaseItem item); /// <summary> /// Gets the type of the inherited content. /// </summary> /// <param name="item">The item.</param> /// <returns>System.String.</returns> - string GetInheritedContentType(BaseItem item); + CollectionType? GetInheritedContentType(BaseItem item); /// <summary> /// Gets the type of the configured content. /// </summary> /// <param name="item">The item.</param> /// <returns>System.String.</returns> - string GetConfiguredContentType(BaseItem item); + CollectionType? GetConfiguredContentType(BaseItem item); /// <summary> /// Gets the type of the configured content. /// </summary> /// <param name="path">The path.</param> /// <returns>System.String.</returns> - string GetConfiguredContentType(string path); + CollectionType? GetConfiguredContentType(string path); /// <summary> /// Normalizes the root path list. @@ -329,7 +329,7 @@ namespace MediaBrowser.Controller.Library User user, string name, Guid parentId, - string viewType, + CollectionType? viewType, string sortName); /// <summary> @@ -343,7 +343,7 @@ namespace MediaBrowser.Controller.Library UserView GetNamedView( User user, string name, - string viewType, + CollectionType? viewType, string sortName); /// <summary> @@ -355,7 +355,7 @@ namespace MediaBrowser.Controller.Library /// <returns>The named view.</returns> UserView GetNamedView( string name, - string viewType, + CollectionType viewType, string sortName); /// <summary> @@ -370,7 +370,7 @@ namespace MediaBrowser.Controller.Library UserView GetNamedView( string name, Guid parentId, - string viewType, + CollectionType? viewType, string sortName, string uniqueId); @@ -383,7 +383,7 @@ namespace MediaBrowser.Controller.Library /// <returns>The shadow view.</returns> UserView GetShadowView( BaseItem parent, - string viewType, + CollectionType? viewType, string sortName); /// <summary> diff --git a/MediaBrowser.Controller/Library/IUserViewManager.cs b/MediaBrowser.Controller/Library/IUserViewManager.cs index 055627d3e3..a565dc88b0 100644 --- a/MediaBrowser.Controller/Library/IUserViewManager.cs +++ b/MediaBrowser.Controller/Library/IUserViewManager.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Library; @@ -28,7 +29,7 @@ namespace MediaBrowser.Controller.Library /// <param name="localizationKey">Localization key to use.</param> /// <param name="sortName">Sort to use.</param> /// <returns>User view.</returns> - UserView GetUserSubView(Guid parentId, string type, string localizationKey, string sortName); + UserView GetUserSubView(Guid parentId, CollectionType? type, string localizationKey, string sortName); /// <summary> /// Gets latest items. diff --git a/MediaBrowser.Controller/Library/ItemResolveArgs.cs b/MediaBrowser.Controller/Library/ItemResolveArgs.cs index dcd0110fb5..6202f92f56 100644 --- a/MediaBrowser.Controller/Library/ItemResolveArgs.cs +++ b/MediaBrowser.Controller/Library/ItemResolveArgs.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.IO; @@ -120,7 +121,7 @@ namespace MediaBrowser.Controller.Library } } - public string CollectionType { get; set; } + public CollectionType? CollectionType { get; set; } public bool HasParent<T>() where T : Folder @@ -220,7 +221,7 @@ namespace MediaBrowser.Controller.Library return GetFileSystemEntryByName(name) is not null; } - public string GetCollectionType() + public CollectionType? GetCollectionType() { return CollectionType; } @@ -229,7 +230,7 @@ namespace MediaBrowser.Controller.Library /// Gets the configured content type for the path. /// </summary> /// <returns>The configured content type.</returns> - public string GetConfiguredContentType() + public CollectionType? GetConfiguredContentType() { return _libraryManager.GetConfiguredContentType(Path); } diff --git a/MediaBrowser.Controller/Resolvers/IItemResolver.cs b/MediaBrowser.Controller/Resolvers/IItemResolver.cs index 282aa721e8..0699734c4b 100644 --- a/MediaBrowser.Controller/Resolvers/IItemResolver.cs +++ b/MediaBrowser.Controller/Resolvers/IItemResolver.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using System.Collections.Generic; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; @@ -32,7 +33,7 @@ namespace MediaBrowser.Controller.Resolvers MultiItemResolverResult ResolveMultiple( Folder parent, List<FileSystemMetadata> files, - string collectionType, + CollectionType? collectionType, IDirectoryService directoryService); } diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs index 287966dd0e..709039fc5b 100644 --- a/MediaBrowser.Model/Dto/BaseItemDto.cs +++ b/MediaBrowser.Model/Dto/BaseItemDto.cs @@ -420,7 +420,7 @@ namespace MediaBrowser.Model.Dto /// Gets or sets the type of the collection. /// </summary> /// <value>The type of the collection.</value> - public string CollectionType { get; set; } + public CollectionType? CollectionType { get; set; } /// <summary> /// Gets or sets the display order. diff --git a/MediaBrowser.Model/Dto/MetadataEditorInfo.cs b/MediaBrowser.Model/Dto/MetadataEditorInfo.cs index d098669baa..a3035bf612 100644 --- a/MediaBrowser.Model/Dto/MetadataEditorInfo.cs +++ b/MediaBrowser.Model/Dto/MetadataEditorInfo.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using Jellyfin.Data.Enums; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Providers; @@ -27,7 +28,7 @@ namespace MediaBrowser.Model.Dto public IReadOnlyList<ExternalIdInfo> ExternalIdInfos { get; set; } - public string? ContentType { get; set; } + public CollectionType? ContentType { get; set; } public IReadOnlyList<NameValuePair> ContentTypeOptions { get; set; } } diff --git a/MediaBrowser.Model/Entities/CollectionType.cs b/MediaBrowser.Model/Entities/CollectionType.cs deleted file mode 100644 index 60b69d4b01..0000000000 --- a/MediaBrowser.Model/Entities/CollectionType.cs +++ /dev/null @@ -1,27 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Model.Entities -{ - public static class CollectionType - { - public const string Movies = "movies"; - - public const string TvShows = "tvshows"; - - public const string Music = "music"; - - public const string MusicVideos = "musicvideos"; - - public const string Trailers = "trailers"; - - public const string HomeVideos = "homevideos"; - - public const string BoxSets = "boxsets"; - - public const string Books = "books"; - public const string Photos = "photos"; - public const string LiveTv = "livetv"; - public const string Playlists = "playlists"; - public const string Folders = "folders"; - } -} diff --git a/MediaBrowser.Model/Library/UserViewQuery.cs b/MediaBrowser.Model/Library/UserViewQuery.cs index 8a49b68637..e20d6af49e 100644 --- a/MediaBrowser.Model/Library/UserViewQuery.cs +++ b/MediaBrowser.Model/Library/UserViewQuery.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using System; +using Jellyfin.Data.Enums; namespace MediaBrowser.Model.Library { @@ -9,7 +10,7 @@ namespace MediaBrowser.Model.Library public UserViewQuery() { IncludeExternalContent = true; - PresetViews = Array.Empty<string>(); + PresetViews = Array.Empty<CollectionType?>(); } /// <summary> @@ -30,6 +31,6 @@ namespace MediaBrowser.Model.Library /// <value><c>true</c> if [include hidden]; otherwise, <c>false</c>.</value> public bool IncludeHidden { get; set; } - public string[] PresetViews { get; set; } + public CollectionType?[] PresetViews { get; set; } } } diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/AudioResolverTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/AudioResolverTests.cs index d136c1bc68..16202aea90 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/AudioResolverTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/AudioResolverTests.cs @@ -1,6 +1,7 @@ using System.Linq; using Emby.Naming.Common; using Emby.Server.Implementations.Library.Resolvers.Audio; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Model.IO; @@ -62,7 +63,7 @@ public class AudioResolverTests null, Mock.Of<ILibraryManager>()) { - CollectionType = "books", + CollectionType = CollectionType.Books, FileInfo = new FileSystemMetadata { FullName = parent, diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs index 6d0ed7bbba..92bac722bf 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs @@ -1,5 +1,6 @@ using Emby.Naming.Common; using Emby.Server.Implementations.Library.Resolvers.TV; +using Jellyfin.Data.Enums; using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; From 44b771bfb4b6412360b18c80621c90902c0a43a7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 9 Nov 2023 14:00:45 -0700 Subject: [PATCH 803/858] chore(deps): update dependency microsoft.net.test.sdk to v17.8.0 (#10551) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index e728182904..9109a5a18a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -47,7 +47,7 @@ <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" /> <PackageVersion Include="Microsoft.Extensions.Logging" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Options" Version="7.0.1" /> - <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.7.2" /> + <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.8.0" /> <PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" /> <PackageVersion Include="MimeTypes" Version="2.4.0" /> <PackageVersion Include="Mono.Nat" Version="3.0.4" /> From 2b742be38e08afea1a1a417c258ac9469dd48a3b Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Thu, 26 Oct 2023 10:03:04 -0400 Subject: [PATCH 804/858] Use IHostedService for DLNA --- .../DlnaServiceCollectionExtensions.cs | 3 + Emby.Dlna/Main/DlnaEntryPoint.cs | 363 ---------------- Emby.Dlna/Main/DlnaHost.cs | 389 ++++++++++++++++++ .../ApplicationHost.cs | 2 +- 4 files changed, 393 insertions(+), 364 deletions(-) delete mode 100644 Emby.Dlna/Main/DlnaEntryPoint.cs create mode 100644 Emby.Dlna/Main/DlnaHost.cs diff --git a/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs b/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs index 87ec14d958..82c80070a5 100644 --- a/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs +++ b/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs @@ -5,6 +5,7 @@ using System.Net.Http; using System.Text; using Emby.Dlna.ConnectionManager; using Emby.Dlna.ContentDirectory; +using Emby.Dlna.Main; using Emby.Dlna.MediaReceiverRegistrar; using Emby.Dlna.Ssdp; using MediaBrowser.Common.Net; @@ -65,5 +66,7 @@ public static class DlnaServiceCollectionExtensions { IsShared = true }); + + services.AddHostedService<DlnaHost>(); } } diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs deleted file mode 100644 index aa70124870..0000000000 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ /dev/null @@ -1,363 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System; -using System.Globalization; -using System.Linq; -using System.Net.Http; -using System.Net.Sockets; -using System.Threading.Tasks; -using Emby.Dlna.PlayTo; -using Emby.Dlna.Ssdp; -using Jellyfin.Networking.Configuration; -using Jellyfin.Networking.Extensions; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Dlna; -using MediaBrowser.Controller.Drawing; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Plugins; -using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Globalization; -using Microsoft.Extensions.Logging; -using Rssdp; -using Rssdp.Infrastructure; - -namespace Emby.Dlna.Main -{ - public sealed class DlnaEntryPoint : IServerEntryPoint, IRunBeforeStartup - { - private readonly IServerConfigurationManager _config; - private readonly ILogger<DlnaEntryPoint> _logger; - private readonly IServerApplicationHost _appHost; - private readonly ISessionManager _sessionManager; - private readonly IHttpClientFactory _httpClientFactory; - private readonly ILibraryManager _libraryManager; - private readonly IUserManager _userManager; - private readonly IDlnaManager _dlnaManager; - private readonly IImageProcessor _imageProcessor; - private readonly IUserDataManager _userDataManager; - private readonly ILocalizationManager _localization; - private readonly IMediaSourceManager _mediaSourceManager; - private readonly IMediaEncoder _mediaEncoder; - private readonly IDeviceDiscovery _deviceDiscovery; - private readonly ISsdpCommunicationsServer _communicationsServer; - private readonly INetworkManager _networkManager; - private readonly object _syncLock = new(); - private readonly bool _disabled; - - private PlayToManager _manager; - private SsdpDevicePublisher _publisher; - - private bool _disposed; - - public DlnaEntryPoint( - IServerConfigurationManager config, - ILoggerFactory loggerFactory, - IServerApplicationHost appHost, - ISessionManager sessionManager, - IHttpClientFactory httpClientFactory, - ILibraryManager libraryManager, - IUserManager userManager, - IDlnaManager dlnaManager, - IImageProcessor imageProcessor, - IUserDataManager userDataManager, - ILocalizationManager localizationManager, - IMediaSourceManager mediaSourceManager, - IDeviceDiscovery deviceDiscovery, - IMediaEncoder mediaEncoder, - ISsdpCommunicationsServer communicationsServer, - INetworkManager networkManager) - { - _config = config; - _appHost = appHost; - _sessionManager = sessionManager; - _httpClientFactory = httpClientFactory; - _libraryManager = libraryManager; - _userManager = userManager; - _dlnaManager = dlnaManager; - _imageProcessor = imageProcessor; - _userDataManager = userDataManager; - _localization = localizationManager; - _mediaSourceManager = mediaSourceManager; - _deviceDiscovery = deviceDiscovery; - _mediaEncoder = mediaEncoder; - _communicationsServer = communicationsServer; - _networkManager = networkManager; - _logger = loggerFactory.CreateLogger<DlnaEntryPoint>(); - - var netConfig = config.GetConfiguration<NetworkConfiguration>(NetworkConfigurationStore.StoreKey); - _disabled = appHost.ListenWithHttps && netConfig.RequireHttps; - - if (_disabled && _config.GetDlnaConfiguration().EnableServer) - { - _logger.LogError("The DLNA specification does not support HTTPS."); - } - } - - public async Task RunAsync() - { - await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false); - - if (_disabled) - { - // No use starting as dlna won't work, as we're running purely on HTTPS. - return; - } - - ReloadComponents(); - - _config.NamedConfigurationUpdated += OnNamedConfigurationUpdated; - } - - private void OnNamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e) - { - if (string.Equals(e.Key, "dlna", StringComparison.OrdinalIgnoreCase)) - { - ReloadComponents(); - } - } - - private void ReloadComponents() - { - var options = _config.GetDlnaConfiguration(); - StartDeviceDiscovery(); - - if (options.EnableServer) - { - StartDevicePublisher(options); - } - else - { - DisposeDevicePublisher(); - } - - if (options.EnablePlayTo) - { - StartPlayToManager(); - } - else - { - DisposePlayToManager(); - } - } - - private void StartDeviceDiscovery() - { - try - { - ((DeviceDiscovery)_deviceDiscovery).Start(_communicationsServer); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error starting device discovery"); - } - } - - public void StartDevicePublisher(Configuration.DlnaOptions options) - { - if (_publisher is not null) - { - return; - } - - try - { - _publisher = new SsdpDevicePublisher( - _communicationsServer, - Environment.OSVersion.Platform.ToString(), - // Can not use VersionString here since that includes OS and version - Environment.OSVersion.Version.ToString(), - _config.GetDlnaConfiguration().SendOnlyMatchedHost) - { - LogFunction = (msg) => _logger.LogDebug("{Msg}", msg), - SupportPnpRootDevice = false - }; - - RegisterServerEndpoints(); - - if (options.BlastAliveMessages) - { - _publisher.StartSendingAliveNotifications(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds)); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error registering endpoint"); - } - } - - private void RegisterServerEndpoints() - { - var udn = CreateUuid(_appHost.SystemId); - var descriptorUri = "/dlna/" + udn + "/description.xml"; - - // Only get bind addresses in LAN - // IPv6 is currently unsupported - var validInterfaces = _networkManager.GetInternalBindAddresses() - .Where(x => x.Address is not null) - .Where(x => x.AddressFamily != AddressFamily.InterNetworkV6) - .ToList(); - - if (validInterfaces.Count == 0) - { - // No interfaces returned, fall back to loopback - validInterfaces = _networkManager.GetLoopbacks().ToList(); - } - - foreach (var intf in validInterfaces) - { - var fullService = "urn:schemas-upnp-org:device:MediaServer:1"; - - _logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, intf.Address); - - var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(intf.Address, false) + descriptorUri); - - var device = new SsdpRootDevice - { - CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info. - Location = uri.Uri, // Must point to the URL that serves your devices UPnP description document. - Address = intf.Address, - PrefixLength = NetworkExtensions.MaskToCidr(intf.Subnet.Prefix), - FriendlyName = "Jellyfin", - Manufacturer = "Jellyfin", - ModelName = "Jellyfin Server", - Uuid = udn - // This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc. - }; - - SetProperties(device, fullService); - _publisher.AddDevice(device); - - var embeddedDevices = new[] - { - "urn:schemas-upnp-org:service:ContentDirectory:1", - "urn:schemas-upnp-org:service:ConnectionManager:1", - // "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1" - }; - - foreach (var subDevice in embeddedDevices) - { - var embeddedDevice = new SsdpEmbeddedDevice - { - FriendlyName = device.FriendlyName, - Manufacturer = device.Manufacturer, - ModelName = device.ModelName, - Uuid = udn - // This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc. - }; - - SetProperties(embeddedDevice, subDevice); - device.AddDevice(embeddedDevice); - } - } - } - - private static string CreateUuid(string text) - { - if (!Guid.TryParse(text, out var guid)) - { - guid = text.GetMD5(); - } - - return guid.ToString("D", CultureInfo.InvariantCulture); - } - - private static void SetProperties(SsdpDevice device, string fullDeviceType) - { - var serviceParts = fullDeviceType - .Replace("urn:", string.Empty, StringComparison.OrdinalIgnoreCase) - .Replace(":1", string.Empty, StringComparison.OrdinalIgnoreCase) - .Split(':'); - - device.DeviceTypeNamespace = serviceParts[0].Replace('.', '-'); - device.DeviceClass = serviceParts[1]; - device.DeviceType = serviceParts[2]; - } - - private void StartPlayToManager() - { - lock (_syncLock) - { - if (_manager is not null) - { - return; - } - - try - { - _manager = new PlayToManager( - _logger, - _sessionManager, - _libraryManager, - _userManager, - _dlnaManager, - _appHost, - _imageProcessor, - _deviceDiscovery, - _httpClientFactory, - _userDataManager, - _localization, - _mediaSourceManager, - _mediaEncoder); - - _manager.Start(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error starting PlayTo manager"); - } - } - } - - private void DisposePlayToManager() - { - lock (_syncLock) - { - if (_manager is not null) - { - try - { - _logger.LogInformation("Disposing PlayToManager"); - _manager.Dispose(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error disposing PlayTo manager"); - } - - _manager = null; - } - } - } - - public void DisposeDevicePublisher() - { - if (_publisher is not null) - { - _logger.LogInformation("Disposing SsdpDevicePublisher"); - _publisher.Dispose(); - _publisher = null; - } - } - - /// <inheritdoc /> - public void Dispose() - { - if (_disposed) - { - return; - } - - DisposeDevicePublisher(); - DisposePlayToManager(); - _disposed = true; - } - } -} diff --git a/Emby.Dlna/Main/DlnaHost.cs b/Emby.Dlna/Main/DlnaHost.cs new file mode 100644 index 0000000000..3896b74a1b --- /dev/null +++ b/Emby.Dlna/Main/DlnaHost.cs @@ -0,0 +1,389 @@ +#pragma warning disable CA1031 // Do not catch general exception types. + +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.Configuration; +using Jellyfin.Networking.Extensions; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Dlna; +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Globalization; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Rssdp; +using Rssdp.Infrastructure; + +namespace Emby.Dlna.Main; + +/// <summary> +/// A <see cref="IHostedService"/> that manages a DLNA server. +/// </summary> +public sealed class DlnaHost : IHostedService, IDisposable +{ + private readonly ILogger<DlnaHost> _logger; + private readonly IServerConfigurationManager _config; + private readonly IServerApplicationHost _appHost; + private readonly ISessionManager _sessionManager; + private readonly IHttpClientFactory _httpClientFactory; + private readonly ILibraryManager _libraryManager; + private readonly IUserManager _userManager; + private readonly IDlnaManager _dlnaManager; + private readonly IImageProcessor _imageProcessor; + private readonly IUserDataManager _userDataManager; + private readonly ILocalizationManager _localization; + private readonly IMediaSourceManager _mediaSourceManager; + private readonly IMediaEncoder _mediaEncoder; + private readonly IDeviceDiscovery _deviceDiscovery; + private readonly ISsdpCommunicationsServer _communicationsServer; + private readonly INetworkManager _networkManager; + private readonly object _syncLock = new(); + + private SsdpDevicePublisher? _publisher; + private PlayToManager? _manager; + private bool _disposed; + + /// <summary> + /// Initializes a new instance of the <see cref="DlnaHost"/> class. + /// </summary> + /// <param name="config">The <see cref="IServerConfigurationManager"/>.</param> + /// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param> + /// <param name="appHost">The <see cref="IServerApplicationHost"/>.</param> + /// <param name="sessionManager">The <see cref="ISessionManager"/>.</param> + /// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/>.</param> + /// <param name="libraryManager">The <see cref="ILibraryManager"/>.</param> + /// <param name="userManager">The <see cref="IUserManager"/>.</param> + /// <param name="dlnaManager">The <see cref="IDlnaManager"/>.</param> + /// <param name="imageProcessor">The <see cref="IImageProcessor"/>.</param> + /// <param name="userDataManager">The <see cref="IUserDataManager"/>.</param> + /// <param name="localizationManager">The <see cref="ILocalizationManager"/>.</param> + /// <param name="mediaSourceManager">The <see cref="IMediaSourceManager"/>.</param> + /// <param name="deviceDiscovery">The <see cref="IDeviceDiscovery"/>.</param> + /// <param name="mediaEncoder">The <see cref="IMediaEncoder"/>.</param> + /// <param name="communicationsServer">The <see cref="ISsdpCommunicationsServer"/>.</param> + /// <param name="networkManager">The <see cref="INetworkManager"/>.</param> + public DlnaHost( + IServerConfigurationManager config, + ILoggerFactory loggerFactory, + IServerApplicationHost appHost, + ISessionManager sessionManager, + IHttpClientFactory httpClientFactory, + ILibraryManager libraryManager, + IUserManager userManager, + IDlnaManager dlnaManager, + IImageProcessor imageProcessor, + IUserDataManager userDataManager, + ILocalizationManager localizationManager, + IMediaSourceManager mediaSourceManager, + IDeviceDiscovery deviceDiscovery, + IMediaEncoder mediaEncoder, + ISsdpCommunicationsServer communicationsServer, + INetworkManager networkManager) + { + _config = config; + _appHost = appHost; + _sessionManager = sessionManager; + _httpClientFactory = httpClientFactory; + _libraryManager = libraryManager; + _userManager = userManager; + _dlnaManager = dlnaManager; + _imageProcessor = imageProcessor; + _userDataManager = userDataManager; + _localization = localizationManager; + _mediaSourceManager = mediaSourceManager; + _deviceDiscovery = deviceDiscovery; + _mediaEncoder = mediaEncoder; + _communicationsServer = communicationsServer; + _networkManager = networkManager; + _logger = loggerFactory.CreateLogger<DlnaHost>(); + } + + /// <inheritdoc /> + public async Task StartAsync(CancellationToken cancellationToken) + { + var netConfig = _config.GetConfiguration<NetworkConfiguration>(NetworkConfigurationStore.StoreKey); + if (_appHost.ListenWithHttps && netConfig.RequireHttps) + { + if (_config.GetDlnaConfiguration().EnableServer) + { + _logger.LogError("The DLNA specification does not support HTTPS."); + } + + // No use starting as dlna won't work, as we're running purely on HTTPS. + return; + } + + await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false); + ReloadComponents(); + + _config.NamedConfigurationUpdated += OnNamedConfigurationUpdated; + } + + /// <inheritdoc /> + public Task StopAsync(CancellationToken cancellationToken) + { + Stop(); + + return Task.CompletedTask; + } + + /// <inheritdoc /> + public void Dispose() + { + if (!_disposed) + { + Stop(); + _disposed = true; + } + } + + private void OnNamedConfigurationUpdated(object? sender, ConfigurationUpdateEventArgs e) + { + if (string.Equals(e.Key, "dlna", StringComparison.OrdinalIgnoreCase)) + { + ReloadComponents(); + } + } + + private void ReloadComponents() + { + var options = _config.GetDlnaConfiguration(); + StartDeviceDiscovery(); + + if (options.EnableServer) + { + StartDevicePublisher(options); + } + else + { + DisposeDevicePublisher(); + } + + if (options.EnablePlayTo) + { + StartPlayToManager(); + } + else + { + DisposePlayToManager(); + } + } + + private static string CreateUuid(string text) + { + if (!Guid.TryParse(text, out var guid)) + { + guid = text.GetMD5(); + } + + return guid.ToString("D", CultureInfo.InvariantCulture); + } + + private static void SetProperties(SsdpDevice device, string fullDeviceType) + { + var serviceParts = fullDeviceType + .Replace("urn:", string.Empty, StringComparison.OrdinalIgnoreCase) + .Replace(":1", string.Empty, StringComparison.OrdinalIgnoreCase) + .Split(':'); + + device.DeviceTypeNamespace = serviceParts[0].Replace('.', '-'); + device.DeviceClass = serviceParts[1]; + device.DeviceType = serviceParts[2]; + } + + private void StartDeviceDiscovery() + { + try + { + ((DeviceDiscovery)_deviceDiscovery).Start(_communicationsServer); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error starting device discovery"); + } + } + + private void StartDevicePublisher(Configuration.DlnaOptions options) + { + if (_publisher is not null) + { + return; + } + + try + { + _publisher = new SsdpDevicePublisher( + _communicationsServer, + Environment.OSVersion.Platform.ToString(), + // Can not use VersionString here since that includes OS and version + Environment.OSVersion.Version.ToString(), + _config.GetDlnaConfiguration().SendOnlyMatchedHost) + { + LogFunction = msg => _logger.LogDebug("{Msg}", msg), + SupportPnpRootDevice = false + }; + + RegisterServerEndpoints(); + + if (options.BlastAliveMessages) + { + _publisher.StartSendingAliveNotifications(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds)); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error registering endpoint"); + } + } + + private void RegisterServerEndpoints() + { + var udn = CreateUuid(_appHost.SystemId); + var descriptorUri = "/dlna/" + udn + "/description.xml"; + + // Only get bind addresses in LAN + // IPv6 is currently unsupported + var validInterfaces = _networkManager.GetInternalBindAddresses() + .Where(x => x.AddressFamily != AddressFamily.InterNetworkV6) + .ToList(); + + if (validInterfaces.Count == 0) + { + // No interfaces returned, fall back to loopback + validInterfaces = _networkManager.GetLoopbacks().ToList(); + } + + foreach (var intf in validInterfaces) + { + var fullService = "urn:schemas-upnp-org:device:MediaServer:1"; + + _logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, intf.Address); + + var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(intf.Address, false) + descriptorUri); + + var device = new SsdpRootDevice + { + CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info. + Location = uri.Uri, // Must point to the URL that serves your devices UPnP description document. + Address = intf.Address, + PrefixLength = NetworkExtensions.MaskToCidr(intf.Subnet.Prefix), + FriendlyName = "Jellyfin", + Manufacturer = "Jellyfin", + ModelName = "Jellyfin Server", + Uuid = udn + // This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc. + }; + + SetProperties(device, fullService); + _publisher!.AddDevice(device); + + var embeddedDevices = new[] + { + "urn:schemas-upnp-org:service:ContentDirectory:1", + "urn:schemas-upnp-org:service:ConnectionManager:1", + // "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1" + }; + + foreach (var subDevice in embeddedDevices) + { + var embeddedDevice = new SsdpEmbeddedDevice + { + FriendlyName = device.FriendlyName, + Manufacturer = device.Manufacturer, + ModelName = device.ModelName, + Uuid = udn + // This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc. + }; + + SetProperties(embeddedDevice, subDevice); + device.AddDevice(embeddedDevice); + } + } + } + + private void StartPlayToManager() + { + lock (_syncLock) + { + if (_manager is not null) + { + return; + } + + try + { + _manager = new PlayToManager( + _logger, + _sessionManager, + _libraryManager, + _userManager, + _dlnaManager, + _appHost, + _imageProcessor, + _deviceDiscovery, + _httpClientFactory, + _userDataManager, + _localization, + _mediaSourceManager, + _mediaEncoder); + + _manager.Start(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error starting PlayTo manager"); + } + } + } + + private void DisposePlayToManager() + { + lock (_syncLock) + { + if (_manager is not null) + { + try + { + _logger.LogInformation("Disposing PlayToManager"); + _manager.Dispose(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error disposing PlayTo manager"); + } + + _manager = null; + } + } + } + + private void DisposeDevicePublisher() + { + if (_publisher is not null) + { + _logger.LogInformation("Disposing SsdpDevicePublisher"); + _publisher.Dispose(); + _publisher = null; + } + } + + private void Stop() + { + DisposeDevicePublisher(); + DisposePlayToManager(); + } +} diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index c9bf7f085e..a1f1cd6490 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -866,7 +866,7 @@ namespace Emby.Server.Implementations yield return typeof(MediaBrowser.MediaEncoding.Encoder.MediaEncoder).Assembly; // Dlna - yield return typeof(DlnaEntryPoint).Assembly; + yield return typeof(DlnaHost).Assembly; // Local metadata yield return typeof(BoxSetXmlSaver).Assembly; From 1e1e1560a47439c02931e67736bcd87b606cf35c Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Thu, 9 Nov 2023 15:09:51 -0500 Subject: [PATCH 805/858] Add IServerApplicationHost parameter to IPluginServiceRegistrator --- .../Plugins/PluginManager.cs | 11 ++++++----- .../Plugins/IPluginServiceRegistrator.cs | 19 ------------------- .../Plugins/IPluginServiceRegistrator.cs | 19 +++++++++++++++++++ 3 files changed, 25 insertions(+), 24 deletions(-) delete mode 100644 MediaBrowser.Common/Plugins/IPluginServiceRegistrator.cs create mode 100644 MediaBrowser.Controller/Plugins/IPluginServiceRegistrator.cs diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs index 20793ee394..db82a2900a 100644 --- a/Emby.Server.Implementations/Plugins/PluginManager.cs +++ b/Emby.Server.Implementations/Plugins/PluginManager.cs @@ -12,10 +12,11 @@ using System.Threading.Tasks; using Emby.Server.Implementations.Library; using Jellyfin.Extensions.Json; using Jellyfin.Extensions.Json.Converters; -using MediaBrowser.Common; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Common.Plugins; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.IO; using MediaBrowser.Model.Plugins; @@ -37,7 +38,7 @@ namespace Emby.Server.Implementations.Plugins private readonly List<AssemblyLoadContext> _assemblyLoadContexts; private readonly JsonSerializerOptions _jsonOptions; private readonly ILogger<PluginManager> _logger; - private readonly IApplicationHost _appHost; + private readonly IServerApplicationHost _appHost; private readonly ServerConfiguration _config; private readonly List<LocalPlugin> _plugins; private readonly Version _minimumVersion; @@ -48,13 +49,13 @@ namespace Emby.Server.Implementations.Plugins /// Initializes a new instance of the <see cref="PluginManager"/> class. /// </summary> /// <param name="logger">The <see cref="ILogger{PluginManager}"/>.</param> - /// <param name="appHost">The <see cref="IApplicationHost"/>.</param> + /// <param name="appHost">The <see cref="IServerApplicationHost"/>.</param> /// <param name="config">The <see cref="ServerConfiguration"/>.</param> /// <param name="pluginsPath">The plugin path.</param> /// <param name="appVersion">The application version.</param> public PluginManager( ILogger<PluginManager> logger, - IApplicationHost appHost, + IServerApplicationHost appHost, ServerConfiguration config, string pluginsPath, Version appVersion) @@ -222,7 +223,7 @@ namespace Emby.Server.Implementations.Plugins try { var instance = (IPluginServiceRegistrator?)Activator.CreateInstance(pluginServiceRegistrator); - instance?.RegisterServices(serviceCollection); + instance?.RegisterServices(serviceCollection, _appHost); } #pragma warning disable CA1031 // Do not catch general exception types catch (Exception ex) diff --git a/MediaBrowser.Common/Plugins/IPluginServiceRegistrator.cs b/MediaBrowser.Common/Plugins/IPluginServiceRegistrator.cs deleted file mode 100644 index 3afe874c52..0000000000 --- a/MediaBrowser.Common/Plugins/IPluginServiceRegistrator.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace MediaBrowser.Common.Plugins -{ - using Microsoft.Extensions.DependencyInjection; - - /// <summary> - /// Defines the <see cref="IPluginServiceRegistrator" />. - /// </summary> - public interface IPluginServiceRegistrator - { - /// <summary> - /// Registers the plugin's services with the service collection. - /// </summary> - /// <remarks> - /// This interface is only used for service registration and requires a parameterless constructor. - /// </remarks> - /// <param name="serviceCollection">The service collection.</param> - void RegisterServices(IServiceCollection serviceCollection); - } -} diff --git a/MediaBrowser.Controller/Plugins/IPluginServiceRegistrator.cs b/MediaBrowser.Controller/Plugins/IPluginServiceRegistrator.cs new file mode 100644 index 0000000000..8b62f38085 --- /dev/null +++ b/MediaBrowser.Controller/Plugins/IPluginServiceRegistrator.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace MediaBrowser.Controller.Plugins; + +/// <summary> +/// Defines the <see cref="IPluginServiceRegistrator" />. +/// </summary> +/// <remarks> +/// This interface is only used for service registration and requires a parameterless constructor. +/// </remarks> +public interface IPluginServiceRegistrator +{ + /// <summary> + /// Registers the plugin's services with the service collection. + /// </summary> + /// <param name="serviceCollection">The service collection.</param> + /// <param name="applicationHost">The server application host.</param> + void RegisterServices(IServiceCollection serviceCollection, IServerApplicationHost applicationHost); +} From 7a98c990abd6420fdd54930e0f71764e6cc06c51 Mon Sep 17 00:00:00 2001 From: Simon-Pierre Corriveau <spccorriveau@gmail.com> Date: Fri, 10 Nov 2023 03:52:55 +0000 Subject: [PATCH 806/858] Translated using Weblate (French (Canada)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fr_CA/ --- Emby.Server.Implementations/Localization/Core/fr-CA.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/fr-CA.json b/Emby.Server.Implementations/Localization/Core/fr-CA.json index 3ee045d89e..b816738c2c 100644 --- a/Emby.Server.Implementations/Localization/Core/fr-CA.json +++ b/Emby.Server.Implementations/Localization/Core/fr-CA.json @@ -124,5 +124,7 @@ "TaskKeyframeExtractorDescription": "Extrait les images clés des fichiers vidéo pour créer des listes de lecture HLS plus précises. Cette tâche peut durer très longtemps.", "TaskKeyframeExtractor": "Extracteur d'image clé", "External": "Externe", - "HearingImpaired": "Malentendants" + "HearingImpaired": "Malentendants", + "TaskRefreshTrickplayImages": "Générer des images Trickplay", + "TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées." } From b1acde54fbde30729b54ad50c88b4ec59655dc3d Mon Sep 17 00:00:00 2001 From: Ruben Teixeira <rubentrteixeira@gmail.com> Date: Thu, 9 Nov 2023 10:23:52 +0000 Subject: [PATCH 807/858] Translated using Weblate (Portuguese (Portugal)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt_PT/ --- Emby.Server.Implementations/Localization/Core/pt-PT.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json index dcfe46efc1..92ac2681e4 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-PT.json +++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json @@ -104,8 +104,8 @@ "TaskRefreshPeople": "Atualizar Pessoas", "TaskCleanLogsDescription": "Apagar ficheiros de log que têm mais de {0} dias.", "TaskCleanLogs": "Limpar a Diretoria de Logs", - "TaskRefreshLibraryDescription": "Scannear a biblioteca de música para novos ficheiros e atualizar os metadados.", - "TaskRefreshLibrary": "Scannear Biblioteca de Música", + "TaskRefreshLibraryDescription": "Analisar a biblioteca de música para novos ficheiros e atualizar os metadados.", + "TaskRefreshLibrary": "Analisar Biblioteca de Música", "TaskRefreshChapterImagesDescription": "Criar thumbnails para os vídeos que têm capítulos.", "TaskRefreshChapterImages": "Extrair Imagens dos Capítulos", "TaskCleanCacheDescription": "Apagar ficheiros em cache que já não são necessários.", From b0120d5d4ce787c2a44f2d172d0760b545804e0f Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Fri, 10 Nov 2023 08:51:26 -0500 Subject: [PATCH 808/858] Fix integration tests --- .../JellyfinApplicationFactory.cs | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs index 1c87d11f18..a078eff77c 100644 --- a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs +++ b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs @@ -8,9 +8,9 @@ using Jellyfin.Server.Helpers; using MediaBrowser.Common; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; -using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Serilog; @@ -39,9 +39,9 @@ namespace Jellyfin.Server.Integration.Tests } /// <inheritdoc/> - protected override IWebHostBuilder CreateWebHostBuilder() + protected override IHostBuilder CreateHostBuilder() { - return new WebHostBuilder(); + return new HostBuilder(); } /// <inheritdoc/> @@ -95,18 +95,17 @@ namespace Jellyfin.Server.Integration.Tests } /// <inheritdoc/> - protected override TestServer CreateServer(IWebHostBuilder builder) + protected override IHost CreateHost(IHostBuilder builder) { - // Create the test server using the base implementation - var testServer = base.CreateServer(builder); - - // Finish initializing the app host - var appHost = (TestAppHost)testServer.Services.GetRequiredService<IApplicationHost>(); - appHost.ServiceProvider = testServer.Services; + var host = builder.Build(); + var appHost = (TestAppHost)host.Services.GetRequiredService<IApplicationHost>(); + appHost.ServiceProvider = host.Services; appHost.InitializeServices().GetAwaiter().GetResult(); + host.Start(); + appHost.RunStartupTasksAsync().GetAwaiter().GetResult(); - return testServer; + return host; } /// <inheritdoc/> From 3fd505a4543a4ee42ead01793a91e0410032321b Mon Sep 17 00:00:00 2001 From: Chris H <70915190+Chris-Codes-It@users.noreply.github.com> Date: Fri, 10 Nov 2023 14:51:44 +0000 Subject: [PATCH 809/858] Validate AuthenticationProviderId and PasswordResetProviderId (#10553) --- CONTRIBUTORS.md | 3 +- MediaBrowser.Model/Users/UserPolicy.cs | 3 + .../Controllers/UserControllerTests.cs | 120 ++++++++++++++++++ 3 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 tests/Jellyfin.Api.Tests/Controllers/UserControllerTests.cs diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 74f1a89651..fff7136b8f 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -171,6 +171,7 @@ - [tallbl0nde](https://github.com/tallbl0nde) - [sleepycatcoding](https://github.com/sleepycatcoding) - [scampower3](https://github.com/scampower3) + - [Chris-Codes-It] (https://github.com/Chris-Codes-It) # Emby Contributors @@ -241,4 +242,4 @@ - [Jakob Kukla](https://github.com/jakobkukla) - [Utku Özdemir](https://github.com/utkuozdemir) - [JPUC1143](https://github.com/Jpuc1143/) - - [0x25CBFC4F](https://github.com/0x25CBFC4F) + - [0x25CBFC4F](https://github.com/0x25CBFC4F) \ No newline at end of file diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs index f5aff07db4..219ed5d5f7 100644 --- a/MediaBrowser.Model/Users/UserPolicy.cs +++ b/MediaBrowser.Model/Users/UserPolicy.cs @@ -3,6 +3,7 @@ using System; using System.ComponentModel; +using System.ComponentModel.DataAnnotations; using System.Xml.Serialization; using Jellyfin.Data.Enums; using AccessSchedule = Jellyfin.Data.Entities.AccessSchedule; @@ -174,8 +175,10 @@ namespace MediaBrowser.Model.Users public int RemoteClientBitrateLimit { get; set; } [XmlElement(ElementName = "AuthenticationProviderId")] + [Required(AllowEmptyStrings = false)] public string AuthenticationProviderId { get; set; } + [Required(AllowEmptyStrings= false)] public string PasswordResetProviderId { get; set; } /// <summary> diff --git a/tests/Jellyfin.Api.Tests/Controllers/UserControllerTests.cs b/tests/Jellyfin.Api.Tests/Controllers/UserControllerTests.cs new file mode 100644 index 0000000000..3f965d0ff0 --- /dev/null +++ b/tests/Jellyfin.Api.Tests/Controllers/UserControllerTests.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using AutoFixture.Xunit2; +using Jellyfin.Api.Controllers; +using Jellyfin.Data.Entities; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Devices; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Playlists; +using MediaBrowser.Controller.QuickConnect; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Users; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Moq; +using Nikse.SubtitleEdit.Core.Common; +using Xunit; + +namespace Jellyfin.Api.Tests.Controllers; + +public class UserControllerTests +{ + private readonly UserController _subject; + private readonly Mock<IUserManager> _mockUserManager; + private readonly Mock<ISessionManager> _mockSessionManager; + private readonly Mock<INetworkManager> _mockNetworkManager; + private readonly Mock<IDeviceManager> _mockDeviceManager; + private readonly Mock<IAuthorizationContext> _mockAuthorizationContext; + private readonly Mock<IServerConfigurationManager> _mockServerConfigurationManager; + private readonly Mock<ILogger<UserController>> _mockLogger; + private readonly Mock<IQuickConnect> _mockQuickConnect; + private readonly Mock<IPlaylistManager> _mockPlaylistManager; + + public UserControllerTests() + { + _mockUserManager = new Mock<IUserManager>(); + _mockSessionManager = new Mock<ISessionManager>(); + _mockNetworkManager = new Mock<INetworkManager>(); + _mockDeviceManager = new Mock<IDeviceManager>(); + _mockAuthorizationContext = new Mock<IAuthorizationContext>(); + _mockServerConfigurationManager = new Mock<IServerConfigurationManager>(); + _mockLogger = new Mock<ILogger<UserController>>(); + _mockQuickConnect = new Mock<IQuickConnect>(); + _mockPlaylistManager = new Mock<IPlaylistManager>(); + + _subject = new UserController( + _mockUserManager.Object, + _mockSessionManager.Object, + _mockNetworkManager.Object, + _mockDeviceManager.Object, + _mockAuthorizationContext.Object, + _mockServerConfigurationManager.Object, + _mockLogger.Object, + _mockQuickConnect.Object, + _mockPlaylistManager.Object); + } + + [Theory] + [AutoData] + public async Task UpdateUserPolicy_WhenUserNotFound_ReturnsNotFound(Guid userId, UserPolicy userPolicy) + { + User? nullUser = null; + _mockUserManager. + Setup(m => m.GetUserById(userId)) + .Returns(nullUser); + + Assert.IsType<NotFoundResult>(await _subject.UpdateUserPolicy(userId, userPolicy)); + } + + [Theory] + [InlineAutoData(null)] + [InlineAutoData("")] + [InlineAutoData(" ")] + public void UpdateUserPolicy_WhenPasswordResetProviderIdNotSupplied_ReturnsBadRequest(string? passwordResetProviderId) + { + var userPolicy = new UserPolicy + { + PasswordResetProviderId = passwordResetProviderId, + AuthenticationProviderId = "AuthenticationProviderId" + }; + + Assert.Contains( + Validate(userPolicy), v => + v.MemberNames.Contains("PasswordResetProviderId") && + v.ErrorMessage != null && + v.ErrorMessage.Contains("required", StringComparison.CurrentCultureIgnoreCase)); + } + + [Theory] + [InlineAutoData(null)] + [InlineAutoData("")] + [InlineAutoData(" ")] + public void UpdateUserPolicy_WhenAuthenticationProviderIdNotSupplied_ReturnsBadRequest(string? authenticationProviderId) + { + var userPolicy = new UserPolicy + { + AuthenticationProviderId = authenticationProviderId, + PasswordResetProviderId = "PasswordResetProviderId" + }; + + Assert.Contains(Validate(userPolicy), v => + v.MemberNames.Contains("AuthenticationProviderId") && + v.ErrorMessage != null && + v.ErrorMessage.Contains("required", StringComparison.CurrentCultureIgnoreCase)); + } + + private IList<ValidationResult> Validate(object model) + { + var result = new List<ValidationResult>(); + var context = new ValidationContext(model, null, null); + Validator.TryValidateObject(model, context, result, true); + + return result; + } +} From 453c65d6193ff9745d03e043725fd67712aaec62 Mon Sep 17 00:00:00 2001 From: Cody Robibero <cody@robibe.ro> Date: Fri, 10 Nov 2023 08:01:39 -0700 Subject: [PATCH 810/858] Fix build after merge --- MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs index 69f10b43bd..90c2ff8ddf 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs +++ b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs @@ -3,14 +3,13 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Trickplay; -using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; -using TagLib.Ape; namespace MediaBrowser.Providers.Trickplay; From 9b5930d7d86722cc8a173db4592bdcc8792f2431 Mon Sep 17 00:00:00 2001 From: DavidFair <DavidFair@users.noreply.github.com> Date: Fri, 10 Nov 2023 15:12:21 +0000 Subject: [PATCH 811/858] Add GH Workflow for CI Tests (#10392) Co-authored-by: Cody Robibero <cody@robibe.ro> --- ...ql-analysis.yml => ci-codeql-analysis.yml} | 0 .../workflows/{openapi.yml => ci-openapi.yml} | 0 .github/workflows/ci-tests.yml | 50 +++++++++++++++++++ .../{repo-stale.yaml => issue-stale.yml} | 23 +-------- ...{automation.yml => project-automation.yml} | 15 +----- .github/workflows/pull-request-conflict.yml | 23 +++++++++ .github/workflows/pull-request-stale.yaml | 30 +++++++++++ ...version.yaml => release-bump-version.yaml} | 0 8 files changed, 106 insertions(+), 35 deletions(-) rename .github/workflows/{codeql-analysis.yml => ci-codeql-analysis.yml} (100%) rename .github/workflows/{openapi.yml => ci-openapi.yml} (100%) create mode 100644 .github/workflows/ci-tests.yml rename .github/workflows/{repo-stale.yaml => issue-stale.yml} (59%) rename .github/workflows/{automation.yml => project-automation.yml} (82%) create mode 100644 .github/workflows/pull-request-conflict.yml create mode 100644 .github/workflows/pull-request-stale.yaml rename .github/workflows/{repo-bump-version.yaml => release-bump-version.yaml} (100%) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml similarity index 100% rename from .github/workflows/codeql-analysis.yml rename to .github/workflows/ci-codeql-analysis.yml diff --git a/.github/workflows/openapi.yml b/.github/workflows/ci-openapi.yml similarity index 100% rename from .github/workflows/openapi.yml rename to .github/workflows/ci-openapi.yml diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml new file mode 100644 index 0000000000..f6cf859bcd --- /dev/null +++ b/.github/workflows/ci-tests.yml @@ -0,0 +1,50 @@ +name: Tests +on: + push: + branches: + - master + # Run tests against the forked branch, but + # do not allow access to secrets + # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflows-in-forked-repositories + pull_request: + +env: + SDK_VERSION: "7.0.x" + +jobs: + run-tests: + strategy: + matrix: + os: ["ubuntu-latest", "macos-latest", "windows-latest"] + + runs-on: "${{ matrix.os }}" + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-dotnet@v3 + with: + dotnet-version: ${{ env.SDK_VERSION }} + + - name: Run DotNet CLI Tests + run: > + dotnet test Jellyfin.sln + --configuration Release + --collect:"XPlat Code Coverage" + --settings tests/coverletArgs.runsettings + --verbosity minimal + + - name: Merge code coverage results + uses: danielpalme/ReportGenerator-GitHub-Action@5 + with: + reports: "**/coverage.cobertura.xml" + targetdir: "merged/" + reporttypes: "Cobertura" + + # TODO - which action / tool to use to publish code coverage results? + # - name: Publish code coverage results + + - name: Publish OpenAPI Artifact + uses: actions/upload-artifact@v3 + with: + name: "OpenAPI Spec" + path: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net*/openapi.json" diff --git a/.github/workflows/repo-stale.yaml b/.github/workflows/issue-stale.yml similarity index 59% rename from .github/workflows/repo-stale.yaml rename to .github/workflows/issue-stale.yml index f9075ba03a..926a7fbfb0 100644 --- a/.github/workflows/repo-stale.yaml +++ b/.github/workflows/issue-stale.yml @@ -1,4 +1,4 @@ -name: Stale Check +name: Stale Issue Labeler on: schedule: @@ -28,27 +28,8 @@ jobs: exempt-issue-labels: regression,security,roadmap,future,feature,enhancement,confirmed stale-issue-label: stale stale-issue-message: |- - This issue has gone 120 days without an update and will be closed within 21 days if there is no new activity. To prevent this issue from being closed, please confirm the issue has not already been fixed by providing updated examples or logs. + This issue has gone 120 days without an update and will be closed within 21 days if there is no new activity. To prevent this issue from being closed, please confirm the issue has not already been fixed by providing updated examples or logs. If you have any questions you can use one of several ways to [contact us](https://jellyfin.org/contact). close-issue-message: |- This issue was closed due to inactivity. - - prs-conflicts: - name: Check PRs with merge conflicts - runs-on: ubuntu-latest - if: ${{ contains(github.repository, 'jellyfin/') }} - steps: - - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0 - with: - repo-token: ${{ secrets.JF_BOT_TOKEN }} - ascending: true - operations-per-run: 150 - # The merge conflict action will remove the label when updated - remove-stale-when-updated: false - days-before-stale: -1 - days-before-close: 90 - days-before-issue-close: -1 - stale-pr-label: merge conflict - close-pr-message: |- - This PR has been closed due to having unresolved merge conflicts. diff --git a/.github/workflows/automation.yml b/.github/workflows/project-automation.yml similarity index 82% rename from .github/workflows/automation.yml rename to .github/workflows/project-automation.yml index 47abce02a3..3637eb16ad 100644 --- a/.github/workflows/automation.yml +++ b/.github/workflows/project-automation.yml @@ -1,4 +1,4 @@ -name: Automation +name: Project Automation on: push: @@ -9,19 +9,6 @@ on: permissions: {} jobs: - label: - name: Labeling - runs-on: ubuntu-latest - if: ${{ github.repository == 'jellyfin/jellyfin' }} - steps: - - name: Apply label - uses: eps1lon/actions-label-merge-conflict@fd1f295ee7443d13745804bc49fe158e240f6c6e # tag=v2.1.0 - if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}} - with: - dirtyLabel: 'merge conflict' - commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.' - repoToken: ${{ secrets.JF_BOT_TOKEN }} - project: name: Project board runs-on: ubuntu-latest diff --git a/.github/workflows/pull-request-conflict.yml b/.github/workflows/pull-request-conflict.yml new file mode 100644 index 0000000000..05517bb030 --- /dev/null +++ b/.github/workflows/pull-request-conflict.yml @@ -0,0 +1,23 @@ +name: Merge Conflict Labeler + +on: + push: + branches: + - master + pull_request_target: + issue_comment: + +permissions: {} +jobs: + label: + name: Labeling + runs-on: ubuntu-latest + if: ${{ github.repository == 'jellyfin/jellyfin' }} + steps: + - name: Apply label + uses: eps1lon/actions-label-merge-conflict@fd1f295ee7443d13745804bc49fe158e240f6c6e # tag=v2.1.0 + if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}} + with: + dirtyLabel: 'merge conflict' + commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.' + repoToken: ${{ secrets.JF_BOT_TOKEN }} diff --git a/.github/workflows/pull-request-stale.yaml b/.github/workflows/pull-request-stale.yaml new file mode 100644 index 0000000000..de093a9887 --- /dev/null +++ b/.github/workflows/pull-request-stale.yaml @@ -0,0 +1,30 @@ +name: Stale PR Check + +on: + schedule: + - cron: '30 */12 * * *' + workflow_dispatch: + +permissions: + pull-requests: write + actions: write + +jobs: + prs-stale-conflicts: + name: Check PRs with merge conflicts + runs-on: ubuntu-latest + if: ${{ contains(github.repository, 'jellyfin/') }} + steps: + - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0 + with: + repo-token: ${{ secrets.JF_BOT_TOKEN }} + ascending: true + operations-per-run: 150 + # The merge conflict action will remove the label when updated + remove-stale-when-updated: false + days-before-stale: -1 + days-before-close: 90 + days-before-issue-close: -1 + stale-pr-label: merge conflict + close-pr-message: |- + This PR has been closed due to having unresolved merge conflicts. diff --git a/.github/workflows/repo-bump-version.yaml b/.github/workflows/release-bump-version.yaml similarity index 100% rename from .github/workflows/repo-bump-version.yaml rename to .github/workflows/release-bump-version.yaml From 88873b6e9eedd1b600bd17c56ee66109d2ef566d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 10 Nov 2023 08:34:15 -0700 Subject: [PATCH 812/858] chore(deps): pin dependencies (#10563) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci-tests.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index f6cf859bcd..36686e64ba 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -19,9 +19,9 @@ jobs: runs-on: "${{ matrix.os }}" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - - uses: actions/setup-dotnet@v3 + - uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3 with: dotnet-version: ${{ env.SDK_VERSION }} @@ -34,7 +34,7 @@ jobs: --verbosity minimal - name: Merge code coverage results - uses: danielpalme/ReportGenerator-GitHub-Action@5 + uses: danielpalme/ReportGenerator-GitHub-Action@873ee34c88a6234bdab7fd264d3666fd1ab417f7 # 5 with: reports: "**/coverage.cobertura.xml" targetdir: "merged/" @@ -44,7 +44,7 @@ jobs: # - name: Publish code coverage results - name: Publish OpenAPI Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3 with: name: "OpenAPI Spec" path: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net*/openapi.json" From 223b15627029054c4d319e113bb0d2a39af890e0 Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Fri, 10 Nov 2023 10:49:40 -0500 Subject: [PATCH 813/858] Move network constants to MediaBrowser.Common --- .../Extensions/NetworkExtensions.cs | 12 ++--- Jellyfin.Networking/Manager/NetworkManager.cs | 45 +++++++++---------- .../ApiServiceCollectionExtensions.cs | 8 ++-- .../Net/NetworkConstants.cs | 4 +- 4 files changed, 34 insertions(+), 35 deletions(-) rename Jellyfin.Networking/Constants/Network.cs => MediaBrowser.Common/Net/NetworkConstants.cs (96%) diff --git a/Jellyfin.Networking/Extensions/NetworkExtensions.cs b/Jellyfin.Networking/Extensions/NetworkExtensions.cs index a1e1140f18..eb0cc81f09 100644 --- a/Jellyfin.Networking/Extensions/NetworkExtensions.cs +++ b/Jellyfin.Networking/Extensions/NetworkExtensions.cs @@ -5,7 +5,7 @@ using System.Net; using System.Net.Sockets; using System.Text.RegularExpressions; using Jellyfin.Extensions; -using Jellyfin.Networking.Constants; +using MediaBrowser.Common.Net; using Microsoft.AspNetCore.HttpOverrides; namespace Jellyfin.Networking.Extensions; @@ -59,7 +59,7 @@ public static partial class NetworkExtensions /// <returns>String value of the subnet mask in dotted decimal notation.</returns> public static IPAddress CidrToMask(byte cidr, AddressFamily family) { - uint addr = 0xFFFFFFFF << ((family == AddressFamily.InterNetwork ? Network.MinimumIPv4PrefixSize : Network.MinimumIPv6PrefixSize) - cidr); + uint addr = 0xFFFFFFFF << ((family == AddressFamily.InterNetwork ? NetworkConstants.MinimumIPv4PrefixSize : NetworkConstants.MinimumIPv6PrefixSize) - cidr); addr = ((addr & 0xff000000) >> 24) | ((addr & 0x00ff0000) >> 8) | ((addr & 0x0000ff00) << 8) @@ -75,7 +75,7 @@ public static partial class NetworkExtensions /// <returns>String value of the subnet mask in dotted decimal notation.</returns> public static IPAddress CidrToMask(int cidr, AddressFamily family) { - uint addr = 0xFFFFFFFF << ((family == AddressFamily.InterNetwork ? Network.MinimumIPv4PrefixSize : Network.MinimumIPv6PrefixSize) - cidr); + uint addr = 0xFFFFFFFF << ((family == AddressFamily.InterNetwork ? NetworkConstants.MinimumIPv4PrefixSize : NetworkConstants.MinimumIPv6PrefixSize) - cidr); addr = ((addr & 0xff000000) >> 24) | ((addr & 0x00ff0000) >> 8) | ((addr & 0x0000ff00) << 8) @@ -100,7 +100,7 @@ public static partial class NetworkExtensions } // GetAddressBytes - Span<byte> bytes = stackalloc byte[mask.AddressFamily == AddressFamily.InterNetwork ? Network.IPv4MaskBytes : Network.IPv6MaskBytes]; + Span<byte> bytes = stackalloc byte[mask.AddressFamily == AddressFamily.InterNetwork ? NetworkConstants.IPv4MaskBytes : NetworkConstants.IPv6MaskBytes]; if (!mask.TryWriteBytes(bytes, out var bytesWritten)) { Console.WriteLine("Unable to write address bytes, only ${bytesWritten} bytes written."); @@ -230,12 +230,12 @@ public static partial class NetworkExtensions } else if (address.AddressFamily == AddressFamily.InterNetwork) { - result = address.Equals(IPAddress.Any) ? Network.IPv4Any : new IPNetwork(address, Network.MinimumIPv4PrefixSize); + result = address.Equals(IPAddress.Any) ? NetworkConstants.IPv4Any : new IPNetwork(address, NetworkConstants.MinimumIPv4PrefixSize); return true; } else if (address.AddressFamily == AddressFamily.InterNetworkV6) { - result = address.Equals(IPAddress.IPv6Any) ? Network.IPv6Any : new IPNetwork(address, Network.MinimumIPv6PrefixSize); + result = address.Equals(IPAddress.IPv6Any) ? NetworkConstants.IPv6Any : new IPNetwork(address, NetworkConstants.MinimumIPv6PrefixSize); return true; } } diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 9c59500d77..1656c3688d 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -8,7 +8,6 @@ using System.Net.NetworkInformation; using System.Net.Sockets; using System.Threading; using Jellyfin.Networking.Configuration; -using Jellyfin.Networking.Constants; using Jellyfin.Networking.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; @@ -289,12 +288,12 @@ namespace Jellyfin.Networking.Manager if (IsIPv4Enabled) { - interfaces.Add(new IPData(IPAddress.Loopback, Network.IPv4RFC5735Loopback, "lo")); + interfaces.Add(new IPData(IPAddress.Loopback, NetworkConstants.IPv4RFC5735Loopback, "lo")); } if (IsIPv6Enabled) { - interfaces.Add(new IPData(IPAddress.IPv6Loopback, Network.IPv6RFC4291Loopback, "lo")); + interfaces.Add(new IPData(IPAddress.IPv6Loopback, NetworkConstants.IPv6RFC4291Loopback, "lo")); } } @@ -326,17 +325,17 @@ namespace Jellyfin.Networking.Manager var fallbackLanSubnets = new List<IPNetwork>(); if (IsIPv6Enabled) { - fallbackLanSubnets.Add(Network.IPv6RFC4291Loopback); // RFC 4291 (Loopback) - fallbackLanSubnets.Add(Network.IPv6RFC4291SiteLocal); // RFC 4291 (Site local) - fallbackLanSubnets.Add(Network.IPv6RFC4193UniqueLocal); // RFC 4193 (Unique local) + fallbackLanSubnets.Add(NetworkConstants.IPv6RFC4291Loopback); // RFC 4291 (Loopback) + fallbackLanSubnets.Add(NetworkConstants.IPv6RFC4291SiteLocal); // RFC 4291 (Site local) + fallbackLanSubnets.Add(NetworkConstants.IPv6RFC4193UniqueLocal); // RFC 4193 (Unique local) } if (IsIPv4Enabled) { - fallbackLanSubnets.Add(Network.IPv4RFC5735Loopback); // RFC 5735 (Loopback) - fallbackLanSubnets.Add(Network.IPv4RFC1918PrivateClassA); // RFC 1918 (private Class A) - fallbackLanSubnets.Add(Network.IPv4RFC1918PrivateClassB); // RFC 1918 (private Class B) - fallbackLanSubnets.Add(Network.IPv4RFC1918PrivateClassC); // RFC 1918 (private Class C) + fallbackLanSubnets.Add(NetworkConstants.IPv4RFC5735Loopback); // RFC 5735 (Loopback) + fallbackLanSubnets.Add(NetworkConstants.IPv4RFC1918PrivateClassA); // RFC 1918 (private Class A) + fallbackLanSubnets.Add(NetworkConstants.IPv4RFC1918PrivateClassB); // RFC 1918 (private Class B) + fallbackLanSubnets.Add(NetworkConstants.IPv4RFC1918PrivateClassC); // RFC 1918 (private Class C) } _lanSubnets = fallbackLanSubnets; @@ -375,12 +374,12 @@ namespace Jellyfin.Networking.Manager if (bindAddresses.Contains(IPAddress.Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.Loopback))) { - interfaces.Add(new IPData(IPAddress.Loopback, Network.IPv4RFC5735Loopback, "lo")); + interfaces.Add(new IPData(IPAddress.Loopback, NetworkConstants.IPv4RFC5735Loopback, "lo")); } if (bindAddresses.Contains(IPAddress.IPv6Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.IPv6Loopback))) { - interfaces.Add(new IPData(IPAddress.IPv6Loopback, Network.IPv6RFC4291Loopback, "lo")); + interfaces.Add(new IPData(IPAddress.IPv6Loopback, NetworkConstants.IPv6RFC4291Loopback, "lo")); } } @@ -442,7 +441,7 @@ namespace Jellyfin.Networking.Manager { if (IPAddress.TryParse(ip, out var ipp)) { - remoteAddressFilter.Add(new IPNetwork(ipp, ipp.AddressFamily == AddressFamily.InterNetwork ? Network.MinimumIPv4PrefixSize : Network.MinimumIPv6PrefixSize)); + remoteAddressFilter.Add(new IPNetwork(ipp, ipp.AddressFamily == AddressFamily.InterNetwork ? NetworkConstants.MinimumIPv4PrefixSize : NetworkConstants.MinimumIPv6PrefixSize)); } } @@ -470,13 +469,13 @@ namespace Jellyfin.Networking.Manager { publishedServerUrls.Add( new PublishedServerUriOverride( - new IPData(IPAddress.Any, Network.IPv4Any), + new IPData(IPAddress.Any, NetworkConstants.IPv4Any), startupOverrideKey, true, true)); publishedServerUrls.Add( new PublishedServerUriOverride( - new IPData(IPAddress.IPv6Any, Network.IPv6Any), + new IPData(IPAddress.IPv6Any, NetworkConstants.IPv6Any), startupOverrideKey, true, true)); @@ -502,13 +501,13 @@ namespace Jellyfin.Networking.Manager publishedServerUrls.Clear(); publishedServerUrls.Add( new PublishedServerUriOverride( - new IPData(IPAddress.Any, Network.IPv4Any), + new IPData(IPAddress.Any, NetworkConstants.IPv4Any), replacement, true, true)); publishedServerUrls.Add( new PublishedServerUriOverride( - new IPData(IPAddress.IPv6Any, Network.IPv6Any), + new IPData(IPAddress.IPv6Any, NetworkConstants.IPv6Any), replacement, true, true)); @@ -518,13 +517,13 @@ namespace Jellyfin.Networking.Manager { publishedServerUrls.Add( new PublishedServerUriOverride( - new IPData(IPAddress.Any, Network.IPv4Any), + new IPData(IPAddress.Any, NetworkConstants.IPv4Any), replacement, false, true)); publishedServerUrls.Add( new PublishedServerUriOverride( - new IPData(IPAddress.IPv6Any, Network.IPv6Any), + new IPData(IPAddress.IPv6Any, NetworkConstants.IPv6Any), replacement, false, true)); @@ -724,12 +723,12 @@ namespace Jellyfin.Networking.Manager var loopbackNetworks = new List<IPData>(); if (IsIPv4Enabled) { - loopbackNetworks.Add(new IPData(IPAddress.Loopback, Network.IPv4RFC5735Loopback, "lo")); + loopbackNetworks.Add(new IPData(IPAddress.Loopback, NetworkConstants.IPv4RFC5735Loopback, "lo")); } if (IsIPv6Enabled) { - loopbackNetworks.Add(new IPData(IPAddress.IPv6Loopback, Network.IPv6RFC4291Loopback, "lo")); + loopbackNetworks.Add(new IPData(IPAddress.IPv6Loopback, NetworkConstants.IPv6RFC4291Loopback, "lo")); } return loopbackNetworks; @@ -748,11 +747,11 @@ namespace Jellyfin.Networking.Manager if (IsIPv4Enabled && IsIPv6Enabled) { // Kestrel source code shows it uses Sockets.DualMode - so this also covers IPAddress.Any by default - result.Add(new IPData(IPAddress.IPv6Any, Network.IPv6Any)); + result.Add(new IPData(IPAddress.IPv6Any, NetworkConstants.IPv6Any)); } else if (IsIPv4Enabled) { - result.Add(new IPData(IPAddress.Any, Network.IPv4Any)); + result.Add(new IPData(IPAddress.Any, NetworkConstants.IPv4Any)); } else if (IsIPv6Enabled) { diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 16b58808f3..28916e916c 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -21,10 +21,10 @@ using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; using Jellyfin.Extensions.Json; using Jellyfin.Networking.Configuration; -using Jellyfin.Networking.Constants; using Jellyfin.Networking.Extensions; using Jellyfin.Server.Configuration; using Jellyfin.Server.Filters; +using MediaBrowser.Common.Net; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Session; using Microsoft.AspNetCore.Authentication; @@ -275,7 +275,7 @@ namespace Jellyfin.Server.Extensions { if (IPAddress.TryParse(allowedProxies[i], out var addr)) { - AddIPAddress(config, options, addr, addr.AddressFamily == AddressFamily.InterNetwork ? Network.MinimumIPv4PrefixSize : Network.MinimumIPv6PrefixSize); + AddIPAddress(config, options, addr, addr.AddressFamily == AddressFamily.InterNetwork ? NetworkConstants.MinimumIPv4PrefixSize : NetworkConstants.MinimumIPv6PrefixSize); } else if (NetworkExtensions.TryParseToSubnet(allowedProxies[i], out var subnet)) { @@ -288,7 +288,7 @@ namespace Jellyfin.Server.Extensions { foreach (var address in addresses) { - AddIPAddress(config, options, address, address.AddressFamily == AddressFamily.InterNetwork ? Network.MinimumIPv4PrefixSize : Network.MinimumIPv6PrefixSize); + AddIPAddress(config, options, address, address.AddressFamily == AddressFamily.InterNetwork ? NetworkConstants.MinimumIPv4PrefixSize : NetworkConstants.MinimumIPv6PrefixSize); } } } @@ -306,7 +306,7 @@ namespace Jellyfin.Server.Extensions return; } - if (prefixLength == Network.MinimumIPv4PrefixSize) + if (prefixLength == NetworkConstants.MinimumIPv4PrefixSize) { options.KnownProxies.Add(addr); } diff --git a/Jellyfin.Networking/Constants/Network.cs b/MediaBrowser.Common/Net/NetworkConstants.cs similarity index 96% rename from Jellyfin.Networking/Constants/Network.cs rename to MediaBrowser.Common/Net/NetworkConstants.cs index 7fadc74bbc..396bc8fb50 100644 --- a/Jellyfin.Networking/Constants/Network.cs +++ b/MediaBrowser.Common/Net/NetworkConstants.cs @@ -1,12 +1,12 @@ using System.Net; using Microsoft.AspNetCore.HttpOverrides; -namespace Jellyfin.Networking.Constants; +namespace MediaBrowser.Common.Net; /// <summary> /// Networking constants. /// </summary> -public static class Network +public static class NetworkConstants { /// <summary> /// IPv4 mask bytes. From 9595636d6105bbe77292d87c7016c21f9df8d4c7 Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Fri, 10 Nov 2023 10:59:45 -0500 Subject: [PATCH 814/858] Move network utilities to MediaBrowser.Common --- Emby.Dlna/Main/DlnaHost.cs | 3 +- .../EntryPoints/UdpServerEntryPoint.cs | 3 +- Jellyfin.Networking/Manager/NetworkManager.cs | 33 +++++++++---------- .../ApiServiceCollectionExtensions.cs | 5 ++- .../Net/NetworkUtils.cs | 9 +++-- .../NetworkExtensionsTests.cs | 10 +++--- .../NetworkParseTests.cs | 16 ++++----- 7 files changed, 37 insertions(+), 42 deletions(-) rename Jellyfin.Networking/Extensions/NetworkExtensions.cs => MediaBrowser.Common/Net/NetworkUtils.cs (97%) diff --git a/Emby.Dlna/Main/DlnaHost.cs b/Emby.Dlna/Main/DlnaHost.cs index 3896b74a1b..26bf6d5e2c 100644 --- a/Emby.Dlna/Main/DlnaHost.cs +++ b/Emby.Dlna/Main/DlnaHost.cs @@ -10,7 +10,6 @@ using System.Threading.Tasks; using Emby.Dlna.PlayTo; using Emby.Dlna.Ssdp; using Jellyfin.Networking.Configuration; -using Jellyfin.Networking.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; @@ -280,7 +279,7 @@ public sealed class DlnaHost : IHostedService, IDisposable CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info. Location = uri.Uri, // Must point to the URL that serves your devices UPnP description document. Address = intf.Address, - PrefixLength = NetworkExtensions.MaskToCidr(intf.Subnet.Prefix), + PrefixLength = NetworkUtils.MaskToCidr(intf.Subnet.Prefix), FriendlyName = "Jellyfin", Manufacturer = "Jellyfin", ModelName = "Jellyfin Server", diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs index 7e4994f1af..662bd88a90 100644 --- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs +++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs @@ -7,7 +7,6 @@ using System.Threading; using System.Threading.Tasks; using Emby.Server.Implementations.Udp; using Jellyfin.Networking.Configuration; -using Jellyfin.Networking.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller; @@ -92,7 +91,7 @@ namespace Emby.Server.Implementations.EntryPoints var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork); foreach (var intf in validInterfaces) { - var broadcastAddress = NetworkExtensions.GetBroadcastAddress(intf.Subnet); + var broadcastAddress = NetworkUtils.GetBroadcastAddress(intf.Subnet); _logger.LogDebug("Binding UDP server to {Address} on port {PortNumber}", broadcastAddress, PortNumber); server = new UdpServer(_logger, _appHost, _config, broadcastAddress, PortNumber); diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 1656c3688d..d631fa51f8 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -8,7 +8,6 @@ using System.Net.NetworkInformation; using System.Net.Sockets; using System.Threading; using Jellyfin.Networking.Configuration; -using Jellyfin.Networking.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Model.Net; @@ -318,7 +317,7 @@ namespace Jellyfin.Networking.Manager var subnets = config.LocalNetworkSubnets; // If no LAN addresses are specified, all private subnets and Loopback are deemed to be the LAN - if (!NetworkExtensions.TryParseToSubnets(subnets, out var lanSubnets, false) || lanSubnets.Count == 0) + if (!NetworkUtils.TryParseToSubnets(subnets, out var lanSubnets, false) || lanSubnets.Count == 0) { _logger.LogDebug("Using LAN interface addresses as user provided no LAN details."); @@ -345,7 +344,7 @@ namespace Jellyfin.Networking.Manager _lanSubnets = lanSubnets; } - _excludedSubnets = NetworkExtensions.TryParseToSubnets(subnets, out var excludedSubnets, true) + _excludedSubnets = NetworkUtils.TryParseToSubnets(subnets, out var excludedSubnets, true) ? excludedSubnets : new List<IPNetwork>(); } @@ -363,7 +362,7 @@ namespace Jellyfin.Networking.Manager var localNetworkAddresses = config.LocalNetworkAddresses; if (localNetworkAddresses.Length > 0 && !string.IsNullOrWhiteSpace(localNetworkAddresses[0])) { - var bindAddresses = localNetworkAddresses.Select(p => NetworkExtensions.TryParseToSubnet(p, out var network) + var bindAddresses = localNetworkAddresses.Select(p => NetworkUtils.TryParseToSubnet(p, out var network) ? network.Prefix : (interfaces.Where(x => x.Name.Equals(p, StringComparison.OrdinalIgnoreCase)) .Select(x => x.Address) @@ -430,7 +429,7 @@ namespace Jellyfin.Networking.Manager // Parse all IPs with netmask to a subnet var remoteAddressFilter = new List<IPNetwork>(); var remoteFilteredSubnets = remoteIPFilter.Where(x => x.Contains('/', StringComparison.OrdinalIgnoreCase)).ToArray(); - if (NetworkExtensions.TryParseToSubnets(remoteFilteredSubnets, out var remoteAddressFilterResult, false)) + if (NetworkUtils.TryParseToSubnets(remoteFilteredSubnets, out var remoteAddressFilterResult, false)) { remoteAddressFilter = remoteAddressFilterResult.ToList(); } @@ -541,7 +540,7 @@ namespace Jellyfin.Networking.Manager false)); } } - else if (NetworkExtensions.TryParseToSubnet(identifier, out var result) && result is not null) + else if (NetworkUtils.TryParseToSubnet(identifier, out var result) && result is not null) { var data = new IPData(result.Prefix, result); publishedServerUrls.Add( @@ -607,7 +606,7 @@ namespace Jellyfin.Networking.Manager foreach (var details in interfaceList) { var parts = details.Split(','); - if (NetworkExtensions.TryParseToSubnet(parts[0], out var subnet)) + if (NetworkUtils.TryParseToSubnet(parts[0], out var subnet)) { var address = subnet.Prefix; var index = int.Parse(parts[1], CultureInfo.InvariantCulture); @@ -771,7 +770,7 @@ namespace Jellyfin.Networking.Manager /// <inheritdoc/> public string GetBindAddress(string source, out int? port) { - if (!NetworkExtensions.TryParseHost(source, out var addresses, IsIPv4Enabled, IsIPv6Enabled)) + if (!NetworkUtils.TryParseHost(source, out var addresses, IsIPv4Enabled, IsIPv6Enabled)) { addresses = Array.Empty<IPAddress>(); } @@ -846,7 +845,7 @@ namespace Jellyfin.Networking.Manager // If no source address is given, use the preferred (first) interface if (source is null) { - result = NetworkExtensions.FormatIPString(availableInterfaces.First().Address); + result = NetworkUtils.FormatIPString(availableInterfaces.First().Address); _logger.LogDebug("{Source}: Using first internal interface as bind address: {Result}", source, result); return result; } @@ -857,14 +856,14 @@ namespace Jellyfin.Networking.Manager { if (intf.Subnet.Contains(source)) { - result = NetworkExtensions.FormatIPString(intf.Address); + result = NetworkUtils.FormatIPString(intf.Address); _logger.LogDebug("{Source}: Found interface with matching subnet, using it as bind address: {Result}", source, result); return result; } } // Fallback to first available interface - result = NetworkExtensions.FormatIPString(availableInterfaces[0].Address); + result = NetworkUtils.FormatIPString(availableInterfaces[0].Address); _logger.LogDebug("{Source}: No matching interfaces found, using preferred interface as bind address: {Result}", source, result); return result; } @@ -881,12 +880,12 @@ namespace Jellyfin.Networking.Manager /// <inheritdoc/> public bool IsInLocalNetwork(string address) { - if (NetworkExtensions.TryParseToSubnet(address, out var subnet)) + if (NetworkUtils.TryParseToSubnet(address, out var subnet)) { return IPAddress.IsLoopback(subnet.Prefix) || (_lanSubnets.Any(x => x.Contains(subnet.Prefix)) && !_excludedSubnets.Any(x => x.Contains(subnet.Prefix))); } - if (NetworkExtensions.TryParseHost(address, out var addresses, IsIPv4Enabled, IsIPv6Enabled)) + if (NetworkUtils.TryParseHost(address, out var addresses, IsIPv4Enabled, IsIPv6Enabled)) { foreach (var ept in addresses) { @@ -1044,7 +1043,7 @@ namespace Jellyfin.Networking.Manager .Select(x => x.Address) .First(); - result = NetworkExtensions.FormatIPString(bindAddress); + result = NetworkUtils.FormatIPString(bindAddress); _logger.LogDebug("{Source}: External request received, matching external bind address found: {Result}", source, result); return true; } @@ -1063,7 +1062,7 @@ namespace Jellyfin.Networking.Manager if (bindAddress is not null) { - result = NetworkExtensions.FormatIPString(bindAddress); + result = NetworkUtils.FormatIPString(bindAddress); _logger.LogDebug("{Source}: Internal request received, matching internal bind address found: {Result}", source, result); return true; } @@ -1097,14 +1096,14 @@ namespace Jellyfin.Networking.Manager { if (intf.Subnet.Contains(source)) { - result = NetworkExtensions.FormatIPString(intf.Address); + result = NetworkUtils.FormatIPString(intf.Address); _logger.LogDebug("{Source}: Found external interface with matching subnet, using it as bind address: {Result}", source, result); return true; } } // Fallback to first external interface. - result = NetworkExtensions.FormatIPString(extResult[0].Address); + result = NetworkUtils.FormatIPString(extResult[0].Address); _logger.LogDebug("{Source}: Using first external interface as bind address: {Result}", source, result); return true; } diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 28916e916c..a842274959 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -21,7 +21,6 @@ using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; using Jellyfin.Extensions.Json; using Jellyfin.Networking.Configuration; -using Jellyfin.Networking.Extensions; using Jellyfin.Server.Configuration; using Jellyfin.Server.Filters; using MediaBrowser.Common.Net; @@ -277,14 +276,14 @@ namespace Jellyfin.Server.Extensions { AddIPAddress(config, options, addr, addr.AddressFamily == AddressFamily.InterNetwork ? NetworkConstants.MinimumIPv4PrefixSize : NetworkConstants.MinimumIPv6PrefixSize); } - else if (NetworkExtensions.TryParseToSubnet(allowedProxies[i], out var subnet)) + else if (NetworkUtils.TryParseToSubnet(allowedProxies[i], out var subnet)) { if (subnet is not null) { AddIPAddress(config, options, subnet.Prefix, subnet.PrefixLength); } } - else if (NetworkExtensions.TryParseHost(allowedProxies[i], out var addresses, config.EnableIPv4, config.EnableIPv6)) + else if (NetworkUtils.TryParseHost(allowedProxies[i], out var addresses, config.EnableIPv4, config.EnableIPv6)) { foreach (var address in addresses) { diff --git a/Jellyfin.Networking/Extensions/NetworkExtensions.cs b/MediaBrowser.Common/Net/NetworkUtils.cs similarity index 97% rename from Jellyfin.Networking/Extensions/NetworkExtensions.cs rename to MediaBrowser.Common/Net/NetworkUtils.cs index eb0cc81f09..f3bff7fa95 100644 --- a/Jellyfin.Networking/Extensions/NetworkExtensions.cs +++ b/MediaBrowser.Common/Net/NetworkUtils.cs @@ -5,15 +5,14 @@ using System.Net; using System.Net.Sockets; using System.Text.RegularExpressions; using Jellyfin.Extensions; -using MediaBrowser.Common.Net; using Microsoft.AspNetCore.HttpOverrides; -namespace Jellyfin.Networking.Extensions; +namespace MediaBrowser.Common.Net; /// <summary> -/// Defines the <see cref="NetworkExtensions" />. +/// Defines the <see cref="NetworkUtils" />. /// </summary> -public static partial class NetworkExtensions +public static partial class NetworkUtils { // Use regular expression as CheckHostName isn't RFC5892 compliant. // Modified from gSkinner's expression at https://stackoverflow.com/questions/11809631/fully-qualified-domain-name-validation @@ -224,7 +223,7 @@ public static partial class NetworkExtensions } else if (IPAddress.TryParse(subnetBlock, out var netmaskAddress)) { - result = new IPNetwork(address, NetworkExtensions.MaskToCidr(netmaskAddress)); + result = new IPNetwork(address, NetworkUtils.MaskToCidr(netmaskAddress)); return true; } } diff --git a/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs b/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs index 072e0a8c53..01546aa2b7 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs @@ -1,6 +1,6 @@ using FsCheck; using FsCheck.Xunit; -using Jellyfin.Networking.Extensions; +using MediaBrowser.Common.Net; using Xunit; namespace Jellyfin.Networking.Tests @@ -26,15 +26,15 @@ namespace Jellyfin.Networking.Tests [InlineData("192.168.1.2/255.255.255.0")] [InlineData("192.168.1.2/24")] public static void TryParse_ValidHostStrings_True(string address) - => Assert.True(NetworkExtensions.TryParseHost(address, out _, true, true)); + => Assert.True(NetworkUtils.TryParseHost(address, out _, true, true)); [Property] public static Property TryParse_IPv4Address_True(IPv4Address address) - => NetworkExtensions.TryParseHost(address.Item.ToString(), out _, true, true).ToProperty(); + => NetworkUtils.TryParseHost(address.Item.ToString(), out _, true, true).ToProperty(); [Property] public static Property TryParse_IPv6Address_True(IPv6Address address) - => NetworkExtensions.TryParseHost(address.Item.ToString(), out _, true, true).ToProperty(); + => NetworkUtils.TryParseHost(address.Item.ToString(), out _, true, true).ToProperty(); /// <summary> /// All should be invalid address strings. @@ -47,6 +47,6 @@ namespace Jellyfin.Networking.Tests [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517:1231")] [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517:1231]")] public static void TryParse_InvalidAddressString_False(string address) - => Assert.False(NetworkExtensions.TryParseHost(address, out _, true, true)); + => Assert.False(NetworkUtils.TryParseHost(address, out _, true, true)); } } diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs index 022b8a3d04..97beb6940c 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs @@ -3,9 +3,9 @@ using System.Collections.Generic; using System.Linq; using System.Net; using Jellyfin.Networking.Configuration; -using Jellyfin.Networking.Extensions; using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; using MediaBrowser.Model.Net; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; @@ -80,7 +80,7 @@ namespace Jellyfin.Networking.Tests [InlineData("[fe80::7add:12ff:febb:c67b%16]")] [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517/56")] public static void TryParseValidIPStringsTrue(string address) - => Assert.True(NetworkExtensions.TryParseToSubnet(address, out _)); + => Assert.True(NetworkUtils.TryParseToSubnet(address, out _)); /// <summary> /// Checks invalid IP address formats. @@ -93,7 +93,7 @@ namespace Jellyfin.Networking.Tests [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517:1231")] [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517:1231]")] public static void TryParseInvalidIPStringsFalse(string address) - => Assert.False(NetworkExtensions.TryParseToSubnet(address, out _)); + => Assert.False(NetworkUtils.TryParseToSubnet(address, out _)); /// <summary> /// Checks if IPv4 address is within a defined subnet. @@ -113,7 +113,7 @@ namespace Jellyfin.Networking.Tests public void IPv4SubnetMaskMatchesValidIPAddress(string netMask, string ipAddress) { var ipa = IPAddress.Parse(ipAddress); - Assert.True(NetworkExtensions.TryParseToSubnet(netMask, out var subnet) && subnet.Contains(IPAddress.Parse(ipAddress))); + Assert.True(NetworkUtils.TryParseToSubnet(netMask, out var subnet) && subnet.Contains(IPAddress.Parse(ipAddress))); } /// <summary> @@ -133,7 +133,7 @@ namespace Jellyfin.Networking.Tests public void IPv4SubnetMaskDoesNotMatchInvalidIPAddress(string netMask, string ipAddress) { var ipa = IPAddress.Parse(ipAddress); - Assert.False(NetworkExtensions.TryParseToSubnet(netMask, out var subnet) && subnet.Contains(IPAddress.Parse(ipAddress))); + Assert.False(NetworkUtils.TryParseToSubnet(netMask, out var subnet) && subnet.Contains(IPAddress.Parse(ipAddress))); } /// <summary> @@ -149,7 +149,7 @@ namespace Jellyfin.Networking.Tests [InlineData("2001:db8:abcd:0012::0/128", "2001:0DB8:ABCD:0012:0000:0000:0000:0000")] public void IPv6SubnetMaskMatchesValidIPAddress(string netMask, string ipAddress) { - Assert.True(NetworkExtensions.TryParseToSubnet(netMask, out var subnet) && subnet.Contains(IPAddress.Parse(ipAddress))); + Assert.True(NetworkUtils.TryParseToSubnet(netMask, out var subnet) && subnet.Contains(IPAddress.Parse(ipAddress))); } [Theory] @@ -160,7 +160,7 @@ namespace Jellyfin.Networking.Tests [InlineData("2001:db8:abcd:0012::0/128", "2001:0DB8:ABCD:0012:0000:0000:0000:0001")] public void IPv6SubnetMaskDoesNotMatchInvalidIPAddress(string netMask, string ipAddress) { - Assert.False(NetworkExtensions.TryParseToSubnet(netMask, out var subnet) && subnet.Contains(IPAddress.Parse(ipAddress))); + Assert.False(NetworkUtils.TryParseToSubnet(netMask, out var subnet) && subnet.Contains(IPAddress.Parse(ipAddress))); } [Theory] @@ -207,7 +207,7 @@ namespace Jellyfin.Networking.Tests NetworkManager.MockNetworkSettings = string.Empty; // Check to see if DNS resolution is working. If not, skip test. - if (!NetworkExtensions.TryParseHost(source, out var host)) + if (!NetworkUtils.TryParseHost(source, out var host)) { return; } From e463dbda47cc51d9e774e867140921f001a3a52a Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Fri, 10 Nov 2023 11:10:51 -0500 Subject: [PATCH 815/858] Move network configuration to MediaBrowser.Common --- Emby.Dlna/Main/DlnaHost.cs | 1 - .../ApplicationHost.cs | 1 - .../EntryPoints/ExternalPortForwarding.cs | 2 +- .../EntryPoints/UdpServerEntryPoint.cs | 1 - Jellyfin.Api/Controllers/StartupController.cs | 2 +- .../BaseUrlRedirectionMiddleware.cs | 2 +- .../Middleware/LanFilteringMiddleware.cs | 1 - .../Configuration/NetworkConfiguration.cs | 176 ------------------ .../NetworkConfigurationExtensions.cs | 20 -- .../NetworkConfigurationFactory.cs | 23 --- .../NetworkConfigurationStore.cs | 24 --- Jellyfin.Networking/Manager/NetworkManager.cs | 1 - .../ApiApplicationBuilderExtensions.cs | 2 +- .../ApiServiceCollectionExtensions.cs | 1 - .../MigrateNetworkConfiguration.cs | 2 +- Jellyfin.Server/Startup.cs | 1 - .../Net/NetworkConfiguration.cs | 175 +++++++++++++++++ .../Net/NetworkConfigurationExtensions.cs | 19 ++ .../Net/NetworkConfigurationFactory.cs | 22 +++ .../Net/NetworkConfigurationStore.cs | 23 +++ .../NetworkConfigurationTests.cs | 2 +- .../NetworkManagerTests.cs | 2 +- .../NetworkParseTests.cs | 1 - .../ParseNetworkTests.cs | 2 +- 24 files changed, 247 insertions(+), 259 deletions(-) delete mode 100644 Jellyfin.Networking/Configuration/NetworkConfiguration.cs delete mode 100644 Jellyfin.Networking/Configuration/NetworkConfigurationExtensions.cs delete mode 100644 Jellyfin.Networking/Configuration/NetworkConfigurationFactory.cs delete mode 100644 Jellyfin.Networking/Configuration/NetworkConfigurationStore.cs create mode 100644 MediaBrowser.Common/Net/NetworkConfiguration.cs create mode 100644 MediaBrowser.Common/Net/NetworkConfigurationExtensions.cs create mode 100644 MediaBrowser.Common/Net/NetworkConfigurationFactory.cs create mode 100644 MediaBrowser.Common/Net/NetworkConfigurationStore.cs diff --git a/Emby.Dlna/Main/DlnaHost.cs b/Emby.Dlna/Main/DlnaHost.cs index 26bf6d5e2c..58db7c26fc 100644 --- a/Emby.Dlna/Main/DlnaHost.cs +++ b/Emby.Dlna/Main/DlnaHost.cs @@ -9,7 +9,6 @@ using System.Threading; using System.Threading.Tasks; using Emby.Dlna.PlayTo; using Emby.Dlna.Ssdp; -using Jellyfin.Networking.Configuration; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index a1f1cd6490..40aee063eb 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -41,7 +41,6 @@ using Emby.Server.Implementations.Updates; using Jellyfin.Api.Helpers; using Jellyfin.Drawing; using Jellyfin.MediaEncoding.Hls.Playlist; -using Jellyfin.Networking.Configuration; using Jellyfin.Networking.Manager; using Jellyfin.Server.Implementations; using MediaBrowser.Common; diff --git a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs index d6da597b8b..c4cd935c37 100644 --- a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs +++ b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs @@ -9,7 +9,7 @@ using System.Net; using System.Text; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Networking.Configuration; +using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Plugins; diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs index 662bd88a90..18e60b2101 100644 --- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs +++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs @@ -6,7 +6,6 @@ using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using Emby.Server.Implementations.Udp; -using Jellyfin.Networking.Configuration; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller; diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index 1098733b2c..fe99cee776 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -3,7 +3,7 @@ using System.Linq; using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Models.StartupDtos; -using Jellyfin.Networking.Configuration; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Middleware/BaseUrlRedirectionMiddleware.cs b/Jellyfin.Api/Middleware/BaseUrlRedirectionMiddleware.cs index 2241c68e7a..cbd948db0a 100644 --- a/Jellyfin.Api/Middleware/BaseUrlRedirectionMiddleware.cs +++ b/Jellyfin.Api/Middleware/BaseUrlRedirectionMiddleware.cs @@ -1,6 +1,6 @@ using System; using System.Threading.Tasks; -using Jellyfin.Networking.Configuration; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; diff --git a/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs b/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs index 94de30d1b1..d8c95ddffe 100644 --- a/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs +++ b/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs @@ -1,5 +1,4 @@ using System.Threading.Tasks; -using Jellyfin.Networking.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; diff --git a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs deleted file mode 100644 index 90ebcd390e..0000000000 --- a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs +++ /dev/null @@ -1,176 +0,0 @@ -#pragma warning disable CA1819 // Properties should not return arrays - -using System; - -namespace Jellyfin.Networking.Configuration -{ - /// <summary> - /// Defines the <see cref="NetworkConfiguration" />. - /// </summary> - public class NetworkConfiguration - { - /// <summary> - /// The default value for <see cref="InternalHttpPort"/>. - /// </summary> - public const int DefaultHttpPort = 8096; - - /// <summary> - /// The default value for <see cref="PublicHttpsPort"/> and <see cref="InternalHttpsPort"/>. - /// </summary> - public const int DefaultHttpsPort = 8920; - - private string _baseUrl = string.Empty; - - /// <summary> - /// Gets or sets a value used to specify the URL prefix that your Jellyfin instance can be accessed at. - /// </summary> - public string BaseUrl - { - get => _baseUrl; - set - { - // Normalize the start of the string - if (string.IsNullOrWhiteSpace(value)) - { - // If baseUrl is empty, set an empty prefix string - _baseUrl = string.Empty; - return; - } - - if (value[0] != '/') - { - // If baseUrl was not configured with a leading slash, append one for consistency - value = "/" + value; - } - - // Normalize the end of the string - if (value[^1] == '/') - { - // If baseUrl was configured with a trailing slash, remove it for consistency - value = value.Remove(value.Length - 1); - } - - _baseUrl = value; - } - } - - /// <summary> - /// Gets or sets a value indicating whether to use HTTPS. - /// </summary> - /// <remarks> - /// In order for HTTPS to be used, in addition to setting this to true, valid values must also be - /// provided for <see cref="CertificatePath"/> and <see cref="CertificatePassword"/>. - /// </remarks> - public bool EnableHttps { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether the server should force connections over HTTPS. - /// </summary> - public bool RequireHttps { get; set; } - - /// <summary> - /// Gets or sets the filesystem path of an X.509 certificate to use for SSL. - /// </summary> - public string CertificatePath { get; set; } = string.Empty; - - /// <summary> - /// Gets or sets the password required to access the X.509 certificate data in the file specified by <see cref="CertificatePath"/>. - /// </summary> - public string CertificatePassword { get; set; } = string.Empty; - - /// <summary> - /// Gets or sets the internal HTTP server port. - /// </summary> - /// <value>The HTTP server port.</value> - public int InternalHttpPort { get; set; } = DefaultHttpPort; - - /// <summary> - /// Gets or sets the internal HTTPS server port. - /// </summary> - /// <value>The HTTPS server port.</value> - public int InternalHttpsPort { get; set; } = DefaultHttpsPort; - - /// <summary> - /// Gets or sets the public HTTP port. - /// </summary> - /// <value>The public HTTP port.</value> - public int PublicHttpPort { get; set; } = DefaultHttpPort; - - /// <summary> - /// Gets or sets the public HTTPS port. - /// </summary> - /// <value>The public HTTPS port.</value> - public int PublicHttpsPort { get; set; } = DefaultHttpsPort; - - /// <summary> - /// Gets or sets a value indicating whether Autodiscovery is enabled. - /// </summary> - public bool AutoDiscovery { get; set; } = true; - - /// <summary> - /// Gets or sets a value indicating whether to enable automatic port forwarding. - /// </summary> - public bool EnableUPnP { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether IPv6 is enabled. - /// </summary> - public bool EnableIPv4 { get; set; } = true; - - /// <summary> - /// Gets or sets a value indicating whether IPv6 is enabled. - /// </summary> - public bool EnableIPv6 { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether access from outside of the LAN is permitted. - /// </summary> - public bool EnableRemoteAccess { get; set; } = true; - - /// <summary> - /// Gets or sets the subnets that are deemed to make up the LAN. - /// </summary> - public string[] LocalNetworkSubnets { get; set; } = Array.Empty<string>(); - - /// <summary> - /// Gets or sets the interface addresses which Jellyfin will bind to. If empty, all interfaces will be used. - /// </summary> - public string[] LocalNetworkAddresses { get; set; } = Array.Empty<string>(); - - /// <summary> - /// Gets or sets the known proxies. - /// </summary> - public string[] KnownProxies { get; set; } = Array.Empty<string>(); - - /// <summary> - /// Gets or sets a value indicating whether address names that match <see cref="VirtualInterfaceNames"/> should be ignored for the purposes of binding. - /// </summary> - public bool IgnoreVirtualInterfaces { get; set; } = true; - - /// <summary> - /// Gets or sets a value indicating the interface name prefixes that should be ignored. The list can be comma separated and values are case-insensitive. <seealso cref="IgnoreVirtualInterfaces"/>. - /// </summary> - public string[] VirtualInterfaceNames { get; set; } = new string[] { "veth" }; - - /// <summary> - /// Gets or sets a value indicating whether the published server uri is based on information in HTTP requests. - /// </summary> - public bool EnablePublishedServerUriByRequest { get; set; } = false; - - /// <summary> - /// Gets or sets the PublishedServerUriBySubnet - /// Gets or sets PublishedServerUri to advertise for specific subnets. - /// </summary> - public string[] PublishedServerUriBySubnet { get; set; } = Array.Empty<string>(); - - /// <summary> - /// Gets or sets the filter for remote IP connectivity. Used in conjunction with <seealso cref="IsRemoteIPFilterBlacklist"/>. - /// </summary> - public string[] RemoteIPFilter { get; set; } = Array.Empty<string>(); - - /// <summary> - /// Gets or sets a value indicating whether <seealso cref="RemoteIPFilter"/> contains a blacklist or a whitelist. Default is a whitelist. - /// </summary> - public bool IsRemoteIPFilterBlacklist { get; set; } - } -} diff --git a/Jellyfin.Networking/Configuration/NetworkConfigurationExtensions.cs b/Jellyfin.Networking/Configuration/NetworkConfigurationExtensions.cs deleted file mode 100644 index 3ba6bb8fcb..0000000000 --- a/Jellyfin.Networking/Configuration/NetworkConfigurationExtensions.cs +++ /dev/null @@ -1,20 +0,0 @@ -using MediaBrowser.Common.Configuration; - -namespace Jellyfin.Networking.Configuration -{ - /// <summary> - /// Defines the <see cref="NetworkConfigurationExtensions" />. - /// </summary> - public static class NetworkConfigurationExtensions - { - /// <summary> - /// Retrieves the network configuration. - /// </summary> - /// <param name="config">The <see cref="IConfigurationManager"/>.</param> - /// <returns>The <see cref="NetworkConfiguration"/>.</returns> - public static NetworkConfiguration GetNetworkConfiguration(this IConfigurationManager config) - { - return config.GetConfiguration<NetworkConfiguration>(NetworkConfigurationStore.StoreKey); - } - } -} diff --git a/Jellyfin.Networking/Configuration/NetworkConfigurationFactory.cs b/Jellyfin.Networking/Configuration/NetworkConfigurationFactory.cs deleted file mode 100644 index 14726565aa..0000000000 --- a/Jellyfin.Networking/Configuration/NetworkConfigurationFactory.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Collections.Generic; -using MediaBrowser.Common.Configuration; - -namespace Jellyfin.Networking.Configuration -{ - /// <summary> - /// Defines the <see cref="NetworkConfigurationFactory" />. - /// </summary> - public class NetworkConfigurationFactory : IConfigurationFactory - { - /// <summary> - /// The GetConfigurations. - /// </summary> - /// <returns>The <see cref="IEnumerable{ConfigurationStore}"/>.</returns> - public IEnumerable<ConfigurationStore> GetConfigurations() - { - return new[] - { - new NetworkConfigurationStore() - }; - } - } -} diff --git a/Jellyfin.Networking/Configuration/NetworkConfigurationStore.cs b/Jellyfin.Networking/Configuration/NetworkConfigurationStore.cs deleted file mode 100644 index a268ebb68f..0000000000 --- a/Jellyfin.Networking/Configuration/NetworkConfigurationStore.cs +++ /dev/null @@ -1,24 +0,0 @@ -using MediaBrowser.Common.Configuration; - -namespace Jellyfin.Networking.Configuration -{ - /// <summary> - /// A configuration that stores network related settings. - /// </summary> - public class NetworkConfigurationStore : ConfigurationStore - { - /// <summary> - /// The name of the configuration in the storage. - /// </summary> - public const string StoreKey = "network"; - - /// <summary> - /// Initializes a new instance of the <see cref="NetworkConfigurationStore"/> class. - /// </summary> - public NetworkConfigurationStore() - { - ConfigurationType = typeof(NetworkConfiguration); - Key = StoreKey; - } - } -} diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index d631fa51f8..b0fe4aba65 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -7,7 +7,6 @@ using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; using System.Threading; -using Jellyfin.Networking.Configuration; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Model.Net; diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs index b6af9baec3..6066893de3 100644 --- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using Jellyfin.Api.Middleware; -using Jellyfin.Networking.Configuration; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Builder; using Microsoft.OpenApi.Models; diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index a842274959..93df7d3156 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -20,7 +20,6 @@ using Jellyfin.Api.Formatters; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; using Jellyfin.Extensions.Json; -using Jellyfin.Networking.Configuration; using Jellyfin.Server.Configuration; using Jellyfin.Server.Filters; using MediaBrowser.Common.Net; diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs index c6d86b8cdb..d92c00991b 100644 --- a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs +++ b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs @@ -3,7 +3,7 @@ using System.IO; using System.Xml; using System.Xml.Serialization; using Emby.Server.Implementations; -using Jellyfin.Networking.Configuration; +using MediaBrowser.Common.Net; using Microsoft.Extensions.Logging; namespace Jellyfin.Server.Migrations.PreStartupRoutines; diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 2acddb243d..18d13c0563 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -7,7 +7,6 @@ using System.Text; using Emby.Dlna.Extensions; using Jellyfin.Api.Middleware; using Jellyfin.MediaEncoding.Hls.Extensions; -using Jellyfin.Networking.Configuration; using Jellyfin.Networking.HappyEyeballs; using Jellyfin.Server.Extensions; using Jellyfin.Server.HealthChecks; diff --git a/MediaBrowser.Common/Net/NetworkConfiguration.cs b/MediaBrowser.Common/Net/NetworkConfiguration.cs new file mode 100644 index 0000000000..61a51c99e2 --- /dev/null +++ b/MediaBrowser.Common/Net/NetworkConfiguration.cs @@ -0,0 +1,175 @@ +#pragma warning disable CA1819 // Properties should not return arrays + +using System; + +namespace MediaBrowser.Common.Net; + +/// <summary> +/// Defines the <see cref="NetworkConfiguration" />. +/// </summary> +public class NetworkConfiguration +{ + /// <summary> + /// The default value for <see cref="InternalHttpPort"/>. + /// </summary> + public const int DefaultHttpPort = 8096; + + /// <summary> + /// The default value for <see cref="PublicHttpsPort"/> and <see cref="InternalHttpsPort"/>. + /// </summary> + public const int DefaultHttpsPort = 8920; + + private string _baseUrl = string.Empty; + + /// <summary> + /// Gets or sets a value used to specify the URL prefix that your Jellyfin instance can be accessed at. + /// </summary> + public string BaseUrl + { + get => _baseUrl; + set + { + // Normalize the start of the string + if (string.IsNullOrWhiteSpace(value)) + { + // If baseUrl is empty, set an empty prefix string + _baseUrl = string.Empty; + return; + } + + if (value[0] != '/') + { + // If baseUrl was not configured with a leading slash, append one for consistency + value = "/" + value; + } + + // Normalize the end of the string + if (value[^1] == '/') + { + // If baseUrl was configured with a trailing slash, remove it for consistency + value = value.Remove(value.Length - 1); + } + + _baseUrl = value; + } + } + + /// <summary> + /// Gets or sets a value indicating whether to use HTTPS. + /// </summary> + /// <remarks> + /// In order for HTTPS to be used, in addition to setting this to true, valid values must also be + /// provided for <see cref="CertificatePath"/> and <see cref="CertificatePassword"/>. + /// </remarks> + public bool EnableHttps { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether the server should force connections over HTTPS. + /// </summary> + public bool RequireHttps { get; set; } + + /// <summary> + /// Gets or sets the filesystem path of an X.509 certificate to use for SSL. + /// </summary> + public string CertificatePath { get; set; } = string.Empty; + + /// <summary> + /// Gets or sets the password required to access the X.509 certificate data in the file specified by <see cref="CertificatePath"/>. + /// </summary> + public string CertificatePassword { get; set; } = string.Empty; + + /// <summary> + /// Gets or sets the internal HTTP server port. + /// </summary> + /// <value>The HTTP server port.</value> + public int InternalHttpPort { get; set; } = DefaultHttpPort; + + /// <summary> + /// Gets or sets the internal HTTPS server port. + /// </summary> + /// <value>The HTTPS server port.</value> + public int InternalHttpsPort { get; set; } = DefaultHttpsPort; + + /// <summary> + /// Gets or sets the public HTTP port. + /// </summary> + /// <value>The public HTTP port.</value> + public int PublicHttpPort { get; set; } = DefaultHttpPort; + + /// <summary> + /// Gets or sets the public HTTPS port. + /// </summary> + /// <value>The public HTTPS port.</value> + public int PublicHttpsPort { get; set; } = DefaultHttpsPort; + + /// <summary> + /// Gets or sets a value indicating whether Autodiscovery is enabled. + /// </summary> + public bool AutoDiscovery { get; set; } = true; + + /// <summary> + /// Gets or sets a value indicating whether to enable automatic port forwarding. + /// </summary> + public bool EnableUPnP { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether IPv6 is enabled. + /// </summary> + public bool EnableIPv4 { get; set; } = true; + + /// <summary> + /// Gets or sets a value indicating whether IPv6 is enabled. + /// </summary> + public bool EnableIPv6 { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether access from outside of the LAN is permitted. + /// </summary> + public bool EnableRemoteAccess { get; set; } = true; + + /// <summary> + /// Gets or sets the subnets that are deemed to make up the LAN. + /// </summary> + public string[] LocalNetworkSubnets { get; set; } = Array.Empty<string>(); + + /// <summary> + /// Gets or sets the interface addresses which Jellyfin will bind to. If empty, all interfaces will be used. + /// </summary> + public string[] LocalNetworkAddresses { get; set; } = Array.Empty<string>(); + + /// <summary> + /// Gets or sets the known proxies. + /// </summary> + public string[] KnownProxies { get; set; } = Array.Empty<string>(); + + /// <summary> + /// Gets or sets a value indicating whether address names that match <see cref="VirtualInterfaceNames"/> should be ignored for the purposes of binding. + /// </summary> + public bool IgnoreVirtualInterfaces { get; set; } = true; + + /// <summary> + /// Gets or sets a value indicating the interface name prefixes that should be ignored. The list can be comma separated and values are case-insensitive. <seealso cref="IgnoreVirtualInterfaces"/>. + /// </summary> + public string[] VirtualInterfaceNames { get; set; } = new string[] { "veth" }; + + /// <summary> + /// Gets or sets a value indicating whether the published server uri is based on information in HTTP requests. + /// </summary> + public bool EnablePublishedServerUriByRequest { get; set; } = false; + + /// <summary> + /// Gets or sets the PublishedServerUriBySubnet + /// Gets or sets PublishedServerUri to advertise for specific subnets. + /// </summary> + public string[] PublishedServerUriBySubnet { get; set; } = Array.Empty<string>(); + + /// <summary> + /// Gets or sets the filter for remote IP connectivity. Used in conjunction with <seealso cref="IsRemoteIPFilterBlacklist"/>. + /// </summary> + public string[] RemoteIPFilter { get; set; } = Array.Empty<string>(); + + /// <summary> + /// Gets or sets a value indicating whether <seealso cref="RemoteIPFilter"/> contains a blacklist or a whitelist. Default is a whitelist. + /// </summary> + public bool IsRemoteIPFilterBlacklist { get; set; } +} diff --git a/MediaBrowser.Common/Net/NetworkConfigurationExtensions.cs b/MediaBrowser.Common/Net/NetworkConfigurationExtensions.cs new file mode 100644 index 0000000000..9288964d22 --- /dev/null +++ b/MediaBrowser.Common/Net/NetworkConfigurationExtensions.cs @@ -0,0 +1,19 @@ +using MediaBrowser.Common.Configuration; + +namespace MediaBrowser.Common.Net; + +/// <summary> +/// Defines the <see cref="NetworkConfigurationExtensions" />. +/// </summary> +public static class NetworkConfigurationExtensions +{ + /// <summary> + /// Retrieves the network configuration. + /// </summary> + /// <param name="config">The <see cref="IConfigurationManager"/>.</param> + /// <returns>The <see cref="NetworkConfiguration"/>.</returns> + public static NetworkConfiguration GetNetworkConfiguration(this IConfigurationManager config) + { + return config.GetConfiguration<NetworkConfiguration>(NetworkConfigurationStore.StoreKey); + } +} diff --git a/MediaBrowser.Common/Net/NetworkConfigurationFactory.cs b/MediaBrowser.Common/Net/NetworkConfigurationFactory.cs new file mode 100644 index 0000000000..9309834f4b --- /dev/null +++ b/MediaBrowser.Common/Net/NetworkConfigurationFactory.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using MediaBrowser.Common.Configuration; + +namespace MediaBrowser.Common.Net; + +/// <summary> +/// Defines the <see cref="NetworkConfigurationFactory" />. +/// </summary> +public class NetworkConfigurationFactory : IConfigurationFactory +{ + /// <summary> + /// The GetConfigurations. + /// </summary> + /// <returns>The <see cref="IEnumerable{ConfigurationStore}"/>.</returns> + public IEnumerable<ConfigurationStore> GetConfigurations() + { + return new[] + { + new NetworkConfigurationStore() + }; + } +} diff --git a/MediaBrowser.Common/Net/NetworkConfigurationStore.cs b/MediaBrowser.Common/Net/NetworkConfigurationStore.cs new file mode 100644 index 0000000000..d2f5187072 --- /dev/null +++ b/MediaBrowser.Common/Net/NetworkConfigurationStore.cs @@ -0,0 +1,23 @@ +using MediaBrowser.Common.Configuration; + +namespace MediaBrowser.Common.Net; + +/// <summary> +/// A configuration that stores network related settings. +/// </summary> +public class NetworkConfigurationStore : ConfigurationStore +{ + /// <summary> + /// The name of the configuration in the storage. + /// </summary> + public const string StoreKey = "network"; + + /// <summary> + /// Initializes a new instance of the <see cref="NetworkConfigurationStore"/> class. + /// </summary> + public NetworkConfigurationStore() + { + ConfigurationType = typeof(NetworkConfiguration); + Key = StoreKey; + } +} diff --git a/tests/Jellyfin.Networking.Tests/Configuration/NetworkConfigurationTests.cs b/tests/Jellyfin.Networking.Tests/Configuration/NetworkConfigurationTests.cs index a78b872dff..30726f1d3c 100644 --- a/tests/Jellyfin.Networking.Tests/Configuration/NetworkConfigurationTests.cs +++ b/tests/Jellyfin.Networking.Tests/Configuration/NetworkConfigurationTests.cs @@ -1,4 +1,4 @@ -using Jellyfin.Networking.Configuration; +using MediaBrowser.Common.Net; using Xunit; namespace Jellyfin.Networking.Tests.Configuration; diff --git a/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs b/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs index 2302f90b8d..0333d98e67 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs @@ -1,6 +1,6 @@ using System.Net; -using Jellyfin.Networking.Configuration; using Jellyfin.Networking.Manager; +using MediaBrowser.Common.Net; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; using Moq; diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs index 97beb6940c..93514a5017 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Net; -using Jellyfin.Networking.Configuration; using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; diff --git a/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs index 2881020375..e0b65cbc44 100644 --- a/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs +++ b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs @@ -1,10 +1,10 @@ using System; using System.Linq; using System.Net; -using Jellyfin.Networking.Configuration; using Jellyfin.Networking.Manager; using Jellyfin.Server.Extensions; using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.Extensions.Configuration; From de0241e975c6b765f2af465734635a1a024a142a Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Fri, 10 Nov 2023 11:17:22 -0500 Subject: [PATCH 816/858] Move API policies to MediaBrowser.Common --- Jellyfin.Api/Controllers/ActivityLogController.cs | 1 + Jellyfin.Api/Controllers/ApiKeyController.cs | 1 + Jellyfin.Api/Controllers/CollectionController.cs | 1 + Jellyfin.Api/Controllers/ConfigurationController.cs | 1 + Jellyfin.Api/Controllers/DevicesController.cs | 1 + Jellyfin.Api/Controllers/DlnaController.cs | 1 + Jellyfin.Api/Controllers/DlnaServerController.cs | 1 + Jellyfin.Api/Controllers/EnvironmentController.cs | 1 + Jellyfin.Api/Controllers/ImageController.cs | 1 + Jellyfin.Api/Controllers/ItemLookupController.cs | 1 + Jellyfin.Api/Controllers/ItemRefreshController.cs | 1 + Jellyfin.Api/Controllers/ItemUpdateController.cs | 1 + Jellyfin.Api/Controllers/LibraryController.cs | 1 + Jellyfin.Api/Controllers/LibraryStructureController.cs | 1 + Jellyfin.Api/Controllers/LiveTvController.cs | 1 + Jellyfin.Api/Controllers/LocalizationController.cs | 1 + Jellyfin.Api/Controllers/PackageController.cs | 1 + Jellyfin.Api/Controllers/PluginsController.cs | 1 + Jellyfin.Api/Controllers/RemoteImageController.cs | 1 + Jellyfin.Api/Controllers/ScheduledTasksController.cs | 1 + Jellyfin.Api/Controllers/SessionController.cs | 1 + Jellyfin.Api/Controllers/StartupController.cs | 1 + Jellyfin.Api/Controllers/SubtitleController.cs | 1 + Jellyfin.Api/Controllers/SyncPlayController.cs | 1 + Jellyfin.Api/Controllers/SystemController.cs | 1 + Jellyfin.Api/Controllers/UserController.cs | 1 + Jellyfin.Api/Controllers/VideosController.cs | 1 + Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs | 1 + {Jellyfin.Api/Constants => MediaBrowser.Common/Api}/Policies.cs | 2 +- 29 files changed, 29 insertions(+), 1 deletion(-) rename {Jellyfin.Api/Constants => MediaBrowser.Common/Api}/Policies.cs (98%) diff --git a/Jellyfin.Api/Controllers/ActivityLogController.cs b/Jellyfin.Api/Controllers/ActivityLogController.cs index c3d02976eb..a19a203b51 100644 --- a/Jellyfin.Api/Controllers/ActivityLogController.cs +++ b/Jellyfin.Api/Controllers/ActivityLogController.cs @@ -2,6 +2,7 @@ using System; using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Data.Queries; +using MediaBrowser.Common.Api; using MediaBrowser.Model.Activity; using MediaBrowser.Model.Querying; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Controllers/ApiKeyController.cs b/Jellyfin.Api/Controllers/ApiKeyController.cs index 991f8cbf20..3363d7bad2 100644 --- a/Jellyfin.Api/Controllers/ApiKeyController.cs +++ b/Jellyfin.Api/Controllers/ApiKeyController.cs @@ -1,6 +1,7 @@ using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; using Jellyfin.Api.Constants; +using MediaBrowser.Common.Api; using MediaBrowser.Controller.Security; using MediaBrowser.Model.Querying; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs index 2db04afb80..2d9f1ed69a 100644 --- a/Jellyfin.Api/Controllers/CollectionController.cs +++ b/Jellyfin.Api/Controllers/CollectionController.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.ModelBinders; +using MediaBrowser.Common.Api; using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Dto; using MediaBrowser.Model.Collections; diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs index 9007dfc410..8db22f7ebe 100644 --- a/Jellyfin.Api/Controllers/ConfigurationController.cs +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -6,6 +6,7 @@ using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Models.ConfigurationDtos; using Jellyfin.Extensions.Json; +using MediaBrowser.Common.Api; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Configuration; diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index aa0dff2123..aa200a7221 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -6,6 +6,7 @@ using Jellyfin.Api.Helpers; using Jellyfin.Data.Dtos; using Jellyfin.Data.Entities.Security; using Jellyfin.Data.Queries; +using MediaBrowser.Common.Api; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Devices; diff --git a/Jellyfin.Api/Controllers/DlnaController.cs b/Jellyfin.Api/Controllers/DlnaController.cs index 415385463d..79a41ce3b4 100644 --- a/Jellyfin.Api/Controllers/DlnaController.cs +++ b/Jellyfin.Api/Controllers/DlnaController.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using Jellyfin.Api.Constants; +using MediaBrowser.Common.Api; using MediaBrowser.Controller.Dlna; using MediaBrowser.Model.Dlna; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs index 42576934b3..ce8d910ffd 100644 --- a/Jellyfin.Api/Controllers/DlnaServerController.cs +++ b/Jellyfin.Api/Controllers/DlnaServerController.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Emby.Dlna; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; +using MediaBrowser.Common.Api; using MediaBrowser.Controller.Dlna; using MediaBrowser.Model.Net; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Controllers/EnvironmentController.cs b/Jellyfin.Api/Controllers/EnvironmentController.cs index 8c9ee1a19e..e0713cf054 100644 --- a/Jellyfin.Api/Controllers/EnvironmentController.cs +++ b/Jellyfin.Api/Controllers/EnvironmentController.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using Jellyfin.Api.Constants; using Jellyfin.Api.Models.EnvironmentDtos; +using MediaBrowser.Common.Api; using MediaBrowser.Common.Extensions; using MediaBrowser.Model.IO; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index 7b10ea170f..1e6580ed15 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -13,6 +13,7 @@ using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; +using MediaBrowser.Common.Api; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Drawing; diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs index b030e74dda..e3aee1bf7a 100644 --- a/Jellyfin.Api/Controllers/ItemLookupController.cs +++ b/Jellyfin.Api/Controllers/ItemLookupController.cs @@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Constants; +using MediaBrowser.Common.Api; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; diff --git a/Jellyfin.Api/Controllers/ItemRefreshController.cs b/Jellyfin.Api/Controllers/ItemRefreshController.cs index b8f6e91ad2..0a8522e1cf 100644 --- a/Jellyfin.Api/Controllers/ItemRefreshController.cs +++ b/Jellyfin.Api/Controllers/ItemRefreshController.cs @@ -2,6 +2,7 @@ using System; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using Jellyfin.Api.Constants; +using MediaBrowser.Common.Api; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.IO; diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs index 3be891b930..4e5ed60d50 100644 --- a/Jellyfin.Api/Controllers/ItemUpdateController.cs +++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Data.Enums; +using MediaBrowser.Common.Api; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index 3cd78b0863..af9a93719c 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -15,6 +15,7 @@ using Jellyfin.Api.Models.LibraryDtos; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Extensions; +using MediaBrowser.Common.Api; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Progress; using MediaBrowser.Controller.Configuration; diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs index b012ff42eb..d483ca4d2b 100644 --- a/Jellyfin.Api/Controllers/LibraryStructureController.cs +++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.LibraryStructureDto; +using MediaBrowser.Common.Api; using MediaBrowser.Common.Progress; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 58159406a2..425086895b 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -16,6 +16,7 @@ using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.LiveTvDtos; using Jellyfin.Data.Enums; +using MediaBrowser.Common.Api; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Dto; diff --git a/Jellyfin.Api/Controllers/LocalizationController.cs b/Jellyfin.Api/Controllers/LocalizationController.cs index b9772a0693..f65d95c411 100644 --- a/Jellyfin.Api/Controllers/LocalizationController.cs +++ b/Jellyfin.Api/Controllers/LocalizationController.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using Jellyfin.Api.Constants; +using MediaBrowser.Common.Api; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs index 0ba5e995fb..c5e940108c 100644 --- a/Jellyfin.Api/Controllers/PackageController.cs +++ b/Jellyfin.Api/Controllers/PackageController.cs @@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; using Jellyfin.Api.Constants; +using MediaBrowser.Common.Api; using MediaBrowser.Common.Updates; using MediaBrowser.Controller.Configuration; using MediaBrowser.Model.Updates; diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs index 72ad14a281..f63e639276 100644 --- a/Jellyfin.Api/Controllers/PluginsController.cs +++ b/Jellyfin.Api/Controllers/PluginsController.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Extensions.Json; +using MediaBrowser.Common.Api; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Updates; using MediaBrowser.Model.Net; diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs index 5c77db2407..595cab2df1 100644 --- a/Jellyfin.Api/Controllers/RemoteImageController.cs +++ b/Jellyfin.Api/Controllers/RemoteImageController.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Constants; +using MediaBrowser.Common.Api; using MediaBrowser.Controller; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; diff --git a/Jellyfin.Api/Controllers/ScheduledTasksController.cs b/Jellyfin.Api/Controllers/ScheduledTasksController.cs index c8fa11ac62..065466cbca 100644 --- a/Jellyfin.Api/Controllers/ScheduledTasksController.cs +++ b/Jellyfin.Api/Controllers/ScheduledTasksController.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using Jellyfin.Api.Constants; +using MediaBrowser.Common.Api; using MediaBrowser.Model.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index e20cf034dc..f0e578e7a0 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -10,6 +10,7 @@ using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.SessionDtos; using Jellyfin.Data.Enums; +using MediaBrowser.Common.Api; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Session; diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index fe99cee776..41b0858d19 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 MediaBrowser.Common.Api; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index c9e256af38..49ca058bd4 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -14,6 +14,7 @@ using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Models.SubtitleDtos; +using MediaBrowser.Common.Api; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; diff --git a/Jellyfin.Api/Controllers/SyncPlayController.cs b/Jellyfin.Api/Controllers/SyncPlayController.cs index 23abba7dc7..3839781971 100644 --- a/Jellyfin.Api/Controllers/SyncPlayController.cs +++ b/Jellyfin.Api/Controllers/SyncPlayController.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.SyncPlayDtos; +using MediaBrowser.Common.Api; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Session; using MediaBrowser.Controller.SyncPlay; diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index 11095a97f0..3d4df03869 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Net.Mime; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; +using MediaBrowser.Common.Api; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 1be40111dd..f9f27f1480 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -8,6 +8,7 @@ using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.UserDtos; using Jellyfin.Data.Enums; +using MediaBrowser.Common.Api; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Authentication; diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index c0ec646eda..7aa5d01e23 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -12,6 +12,7 @@ using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.StreamingDtos; +using MediaBrowser.Common.Api; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 93df7d3156..89f9c08e7e 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -22,6 +22,7 @@ using Jellyfin.Data.Enums; using Jellyfin.Extensions.Json; using Jellyfin.Server.Configuration; using Jellyfin.Server.Filters; +using MediaBrowser.Common.Api; using MediaBrowser.Common.Net; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Session; diff --git a/Jellyfin.Api/Constants/Policies.cs b/MediaBrowser.Common/Api/Policies.cs similarity index 98% rename from Jellyfin.Api/Constants/Policies.cs rename to MediaBrowser.Common/Api/Policies.cs index 02fdef1507..e5427b8ef3 100644 --- a/Jellyfin.Api/Constants/Policies.cs +++ b/MediaBrowser.Common/Api/Policies.cs @@ -1,4 +1,4 @@ -namespace Jellyfin.Api.Constants; +namespace MediaBrowser.Common.Api; /// <summary> /// Policies for the API authorization. From a76a9056a2804fe80dfb7dcfb30c4b38d07b5bc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simas=20=C5=A0imas?= <taoleifhelp.10258@gmail.com> Date: Sun, 12 Nov 2023 15:50:05 +0000 Subject: [PATCH 817/858] Translated using Weblate (Lithuanian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/lt/ --- Emby.Server.Implementations/Localization/Core/lt-LT.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/lt-LT.json b/Emby.Server.Implementations/Localization/Core/lt-LT.json index ce8d8fc322..e7279994bb 100644 --- a/Emby.Server.Implementations/Localization/Core/lt-LT.json +++ b/Emby.Server.Implementations/Localization/Core/lt-LT.json @@ -124,5 +124,7 @@ "TaskKeyframeExtractor": "Pagrindinių kadrų ištraukėjas", "TaskOptimizeDatabaseDescription": "Suspaudžia duomenų bazę ir atlaisvina vietą. Paleidžiant šią užduotį, po bibliotekos skenavimo arba kitų veiksmų kurie galimai modifikuoja duomenų bazė, gali pagerinti greitaveiką.", "External": "Išorinis", - "HearingImpaired": "Su klausos sutrikimais" + "HearingImpaired": "Su klausos sutrikimais", + "TaskRefreshTrickplayImages": "Generuoti Trickplay atvaizdus", + "TaskRefreshTrickplayImagesDescription": "Sukuria trickplay peržiūras vaizdo įrašams įgalintose bibliotekose." } From 532f1e844b96ce819138e6091d2a9ad6c5aad3ab Mon Sep 17 00:00:00 2001 From: Christo <christogreeff@gmail.com> Date: Sun, 12 Nov 2023 14:05:48 +0000 Subject: [PATCH 818/858] Translated using Weblate (Afrikaans) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/af/ --- Emby.Server.Implementations/Localization/Core/af.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/af.json b/Emby.Server.Implementations/Localization/Core/af.json index 9fbf364efe..ecea8df6a8 100644 --- a/Emby.Server.Implementations/Localization/Core/af.json +++ b/Emby.Server.Implementations/Localization/Core/af.json @@ -123,5 +123,7 @@ "TaskKeyframeExtractorDescription": "Haal keyframes vanuit video lêers om meer presiese HLS afspeellyste te maak. Dit kan lank duur.", "TaskKeyframeExtractor": "Keyframe Ekstraktor", "External": "Ekstern", - "HearingImpaired": "gehoorgestremd" + "HearingImpaired": "gehoorgestremd", + "TaskRefreshTrickplayImages": "Genereer Fopspeel Beelde", + "TaskRefreshTrickplayImagesDescription": "Skep fopspeel voorskou vir videos in aangeskakelde media versameling." } From ea546230586a00a75db5c379db904e47cbbf270b Mon Sep 17 00:00:00 2001 From: Radu Terec <raduterec@gmail.com> Date: Sun, 12 Nov 2023 13:42:37 +0000 Subject: [PATCH 819/858] Translated using Weblate (Romanian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ro/ --- Emby.Server.Implementations/Localization/Core/ro.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/ro.json b/Emby.Server.Implementations/Localization/Core/ro.json index 2c10bb4779..537a6d3f2f 100644 --- a/Emby.Server.Implementations/Localization/Core/ro.json +++ b/Emby.Server.Implementations/Localization/Core/ro.json @@ -123,5 +123,7 @@ "TaskKeyframeExtractorDescription": "Extrage cadrele cheie din fișierele video pentru a crea liste de redare HLS mai precise. Această sarcină poate rula o perioadă lungă de timp.", "External": "Extern", "TaskKeyframeExtractor": "Extractor de cadre cheie", - "HearingImpaired": "Ascultare Impară" + "HearingImpaired": "Ascultare Impară", + "TaskRefreshTrickplayImages": "Generează imagini Trickplay", + "TaskRefreshTrickplayImagesDescription": "Generează previzualizările trickplay pentru videourile din librăriile selectate." } From 203fe5c1034006f1a04af50cc283f86e3db4ff6f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 14 Nov 2023 17:11:01 +0000 Subject: [PATCH 820/858] chore(deps): update github/codeql-action action to v2.22.6 --- .github/workflows/ci-codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index f43d743f04..38bf82f29c 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5 + uses: github/codeql-action/init@689fdc5193eeb735ecb2e52e819e3382876f93f4 # v2.22.6 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5 + uses: github/codeql-action/autobuild@689fdc5193eeb735ecb2e52e819e3382876f93f4 # v2.22.6 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5 + uses: github/codeql-action/analyze@689fdc5193eeb735ecb2e52e819e3382876f93f4 # v2.22.6 From 89b1eba249b675f8c3b2f1a29bd2b859ad78cea5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 14 Nov 2023 20:11:11 +0000 Subject: [PATCH 821/858] chore(deps): update danielpalme/reportgenerator-github-action digest to 4d510cb --- .github/workflows/ci-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 36686e64ba..9de0796916 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -34,7 +34,7 @@ jobs: --verbosity minimal - name: Merge code coverage results - uses: danielpalme/ReportGenerator-GitHub-Action@873ee34c88a6234bdab7fd264d3666fd1ab417f7 # 5 + uses: danielpalme/ReportGenerator-GitHub-Action@4d510cbed8a05af5aefea46c7fd6e05b95844c89 # 5 with: reports: "**/coverage.cobertura.xml" targetdir: "merged/" From eb022c49ccb310ee46d8d7b7f46678312b13abc6 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Mon, 23 Oct 2023 23:36:56 +0200 Subject: [PATCH 822/858] Update to .NET 8 --- .ci/azure-pipelines-test.yml | 2 +- .config/dotnet-tools.json | 2 +- .github/workflows/ci-openapi.yml | 4 +- .vscode/launch.json | 4 +- Directory.Packages.props | 52 +++++++++---------- Emby.Dlna/Emby.Dlna.csproj | 2 +- Emby.Naming/Emby.Naming.csproj | 2 +- Emby.Photos/Emby.Photos.csproj | 2 +- .../Emby.Server.Implementations.csproj | 2 +- Jellyfin.Api/Jellyfin.Api.csproj | 2 +- Jellyfin.Data/Jellyfin.Data.csproj | 2 +- .../Jellyfin.Networking.csproj | 2 +- .../Jellyfin.Server.Implementations.csproj | 2 +- Jellyfin.Server/Jellyfin.Server.csproj | 2 +- .../MediaBrowser.Common.csproj | 2 +- .../MediaBrowser.Controller.csproj | 2 +- .../MediaBrowser.LocalMetadata.csproj | 2 +- .../MediaBrowser.MediaEncoding.csproj | 2 +- MediaBrowser.Model/MediaBrowser.Model.csproj | 2 +- .../MediaBrowser.Providers.csproj | 2 +- .../MediaBrowser.XbmcMetadata.csproj | 2 +- README.md | 2 +- RSSDP/RSSDP.csproj | 2 +- fedora/jellyfin.spec | 2 +- .../Emby.Server.Implementations.Fuzz.csproj | 2 +- fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh | 2 +- .../Jellyfin.Api.Fuzz.csproj | 2 +- fuzz/Jellyfin.Api.Fuzz/fuzz.sh | 2 +- global.json | 2 +- .../Jellyfin.Drawing.Skia.csproj | 2 +- src/Jellyfin.Drawing/Jellyfin.Drawing.csproj | 2 +- .../Jellyfin.Extensions.csproj | 2 +- .../Jellyfin.MediaEncoding.Hls.csproj | 2 +- .../Jellyfin.MediaEncoding.Keyframes.csproj | 2 +- tests/Directory.Build.props | 2 +- 35 files changed, 62 insertions(+), 62 deletions(-) diff --git a/.ci/azure-pipelines-test.yml b/.ci/azure-pipelines-test.yml index 81362aab23..f15d6a6cda 100644 --- a/.ci/azure-pipelines-test.yml +++ b/.ci/azure-pipelines-test.yml @@ -94,5 +94,5 @@ jobs: displayName: 'Publish OpenAPI Artifact' condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) inputs: - targetPath: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net7.0/openapi.json" + targetPath: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net8.0/openapi.json" artifactName: 'OpenAPI Spec' diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index dbe78984a2..37aa7721e3 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -9,4 +9,4 @@ ] } } -} \ No newline at end of file +} diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml index 8c463a8fcf..96b790c0c7 100644 --- a/.github/workflows/ci-openapi.yml +++ b/.github/workflows/ci-openapi.yml @@ -30,7 +30,7 @@ jobs: name: openapi-head retention-days: 14 if-no-files-found: error - path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net7.0/openapi.json + path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net8.0/openapi.json openapi-base: name: OpenAPI - BASE @@ -64,7 +64,7 @@ jobs: name: openapi-base retention-days: 14 if-no-files-found: error - path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net7.0/openapi.json + path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net8.0/openapi.json openapi-diff: permissions: diff --git a/.vscode/launch.json b/.vscode/launch.json index 55e6508a9a..be55764fd4 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,7 +6,7 @@ "type": "coreclr", "request": "launch", "preLaunchTask": "build", - "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net7.0/jellyfin.dll", + "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net8.0/jellyfin.dll", "args": [], "cwd": "${workspaceFolder}/Jellyfin.Server", "console": "internalConsole", @@ -22,7 +22,7 @@ "type": "coreclr", "request": "launch", "preLaunchTask": "build", - "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net7.0/jellyfin.dll", + "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net8.0/jellyfin.dll", "args": ["--nowebclient"], "cwd": "${workspaceFolder}/Jellyfin.Server", "console": "internalConsole", diff --git a/Directory.Packages.props b/Directory.Packages.props index 9109a5a18a..0d7dbf12ce 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -23,30 +23,30 @@ <PackageVersion Include="libse" Version="3.6.13" /> <PackageVersion Include="LrcParser" Version="2023.524.0" /> <PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.1" /> - <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.13" /> + <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.0" /> <PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" /> - <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.13" /> + <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" /> - <PackageVersion Include="Microsoft.Data.Sqlite" Version="7.0.13" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.13" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.13" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.13" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.13" /> - <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" /> - <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" /> - <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" /> - <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.4" /> - <PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="7.0.0" /> - <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" /> - <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" /> - <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" /> - <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.13" /> - <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.13" /> - <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" /> - <PackageVersion Include="Microsoft.Extensions.Http" Version="7.0.0" /> - <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" /> - <PackageVersion Include="Microsoft.Extensions.Logging" Version="7.0.0" /> - <PackageVersion Include="Microsoft.Extensions.Options" Version="7.0.1" /> + <PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.0" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.0" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.0" /> + <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" /> + <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" /> + <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" /> + <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.0" /> + <PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" /> + <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" /> + <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" /> + <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" /> + <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.0" /> + <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.0" /> + <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" /> + <PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0" /> + <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" /> + <PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" /> + <PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.0" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.8.0" /> <PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" /> <PackageVersion Include="MimeTypes" Version="2.4.0" /> @@ -77,9 +77,9 @@ <PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" /> <PackageVersion Include="System.Globalization" Version="4.3.0" /> <PackageVersion Include="System.Linq.Async" Version="6.0.1" /> - <PackageVersion Include="System.Text.Encoding.CodePages" Version="7.0.0" /> - <PackageVersion Include="System.Text.Json" Version="7.0.3" /> - <PackageVersion Include="System.Threading.Tasks.Dataflow" Version="7.0.0" /> + <PackageVersion Include="System.Text.Encoding.CodePages" Version="8.0.0" /> + <PackageVersion Include="System.Text.Json" Version="8.0.0" /> + <PackageVersion Include="System.Threading.Tasks.Dataflow" Version="8.0.0" /> <PackageVersion Include="TagLibSharp" Version="2.3.0" /> <PackageVersion Include="TMDbLib" Version="2.0.0" /> <PackageVersion Include="UTF.Unknown" Version="2.5.1" /> @@ -88,4 +88,4 @@ <PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" /> <PackageVersion Include="xunit" Version="2.6.1" /> </ItemGroup> -</Project> \ No newline at end of file +</Project> diff --git a/Emby.Dlna/Emby.Dlna.csproj b/Emby.Dlna/Emby.Dlna.csproj index efbef05640..7336482e56 100644 --- a/Emby.Dlna/Emby.Dlna.csproj +++ b/Emby.Dlna/Emby.Dlna.csproj @@ -17,7 +17,7 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>net7.0</TargetFramework> + <TargetFramework>net8.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj index bc7548189b..47f2605501 100644 --- a/Emby.Naming/Emby.Naming.csproj +++ b/Emby.Naming/Emby.Naming.csproj @@ -6,7 +6,7 @@ </PropertyGroup> <PropertyGroup> - <TargetFramework>net7.0</TargetFramework> + <TargetFramework>net8.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <PublishRepositoryUrl>true</PublishRepositoryUrl> diff --git a/Emby.Photos/Emby.Photos.csproj b/Emby.Photos/Emby.Photos.csproj index 5a04bbe49b..55dbe393c7 100644 --- a/Emby.Photos/Emby.Photos.csproj +++ b/Emby.Photos/Emby.Photos.csproj @@ -19,7 +19,7 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>net7.0</TargetFramework> + <TargetFramework>net8.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index b48e389ace..905f36e43e 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -40,7 +40,7 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>net7.0</TargetFramework> + <TargetFramework>net8.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index 03dd97367f..2473fb288a 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -6,7 +6,7 @@ </PropertyGroup> <PropertyGroup> - <TargetFramework>net7.0</TargetFramework> + <TargetFramework>net8.0</TargetFramework> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj index 847853ca00..c26e6cf239 100644 --- a/Jellyfin.Data/Jellyfin.Data.csproj +++ b/Jellyfin.Data/Jellyfin.Data.csproj @@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>net7.0</TargetFramework> + <TargetFramework>net8.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <PublishRepositoryUrl>true</PublishRepositoryUrl> diff --git a/Jellyfin.Networking/Jellyfin.Networking.csproj b/Jellyfin.Networking/Jellyfin.Networking.csproj index 43d08c37a1..30f41aeb22 100644 --- a/Jellyfin.Networking/Jellyfin.Networking.csproj +++ b/Jellyfin.Networking/Jellyfin.Networking.csproj @@ -1,6 +1,6 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>net7.0</TargetFramework> + <TargetFramework>net8.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index df1d5a3e18..0ed1578c70 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>net7.0</TargetFramework> + <TargetFramework>net8.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 5479d22965..1d4d97551e 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -8,7 +8,7 @@ <PropertyGroup> <AssemblyName>jellyfin</AssemblyName> <OutputType>Exe</OutputType> - <TargetFramework>net7.0</TargetFramework> + <TargetFramework>net8.0</TargetFramework> <ServerGarbageCollection>false</ServerGarbageCollection> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index 7d0d7a173b..9b3ea43683 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -29,7 +29,7 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>net7.0</TargetFramework> + <TargetFramework>net8.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <PublishRepositoryUrl>true</PublishRepositoryUrl> diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index f9468f6cdb..83faac3372 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -35,7 +35,7 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>net7.0</TargetFramework> + <TargetFramework>net8.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <PublishRepositoryUrl>true</PublishRepositoryUrl> diff --git a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj index a39bc238a7..05177ac398 100644 --- a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj +++ b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj @@ -11,7 +11,7 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>net7.0</TargetFramework> + <TargetFramework>net8.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj index 1f39e88cde..a4e8194c15 100644 --- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj +++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj @@ -6,7 +6,7 @@ </PropertyGroup> <PropertyGroup> - <TargetFramework>net7.0</TargetFramework> + <TargetFramework>net8.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 75c5bc6f00..89ec156a9f 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -14,7 +14,7 @@ </PropertyGroup> <PropertyGroup> - <TargetFramework>net7.0</TargetFramework> + <TargetFramework>net8.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <PublishRepositoryUrl>true</PublishRepositoryUrl> diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 8471f6fa10..7a50c6cf4b 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -27,7 +27,7 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>net7.0</TargetFramework> + <TargetFramework>net8.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> diff --git a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj index d7e34fd226..c20073eea1 100644 --- a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj +++ b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj @@ -15,7 +15,7 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>net7.0</TargetFramework> + <TargetFramework>net8.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> diff --git a/README.md b/README.md index 2362741b47..911d9a094b 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ A second option is to build the project and then run the resulting executable fi ```bash dotnet build # Build the project -cd Jellyfin.Server/bin/Debug/net7.0 # Change into the build output directory +cd Jellyfin.Server/bin/Debug/net8.0 # Change into the build output directory ``` 2. Execute the build output. On Linux, Mac, etc. use `./jellyfin` and on Windows use `jellyfin.exe`. diff --git a/RSSDP/RSSDP.csproj b/RSSDP/RSSDP.csproj index df5d982f62..3f24de4e65 100644 --- a/RSSDP/RSSDP.csproj +++ b/RSSDP/RSSDP.csproj @@ -11,7 +11,7 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>net7.0</TargetFramework> + <TargetFramework>net8.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <AnalysisMode>AllDisabledByDefault</AnalysisMode> <Nullable>disable</Nullable> diff --git a/fedora/jellyfin.spec b/fedora/jellyfin.spec index e783689069..c56a189ce9 100644 --- a/fedora/jellyfin.spec +++ b/fedora/jellyfin.spec @@ -73,7 +73,7 @@ dotnet publish --configuration Release --self-contained --runtime %{dotnet_runti %install # Jellyfin files %{__mkdir} -p %{buildroot}%{_libdir}/jellyfin %{buildroot}%{_bindir} -%{__cp} -r Jellyfin.Server/bin/Release/net7.0/%{dotnet_runtime}/publish/* %{buildroot}%{_libdir}/jellyfin +%{__cp} -r Jellyfin.Server/bin/Release/net8.0/%{dotnet_runtime}/publish/* %{buildroot}%{_libdir}/jellyfin %{__install} -D %{SOURCE10} %{buildroot}%{_bindir}/jellyfin sed -i -e 's|/usr/lib64|%{_libdir}|g' %{buildroot}%{_bindir}/jellyfin diff --git a/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj b/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj index 1e3f8a0482..73aae3f3df 100644 --- a/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj +++ b/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj @@ -2,7 +2,7 @@ <PropertyGroup> <OutputType>Exe</OutputType> - <TargetFramework>net7.0</TargetFramework> + <TargetFramework>net8.0</TargetFramework> </PropertyGroup> <ItemGroup> diff --git a/fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh b/fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh index aa2a34cdde..80a5cd7c1f 100755 --- a/fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh +++ b/fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh @@ -8,4 +8,4 @@ cp bin/Emby.Server.Implementations.dll . dotnet build mkdir -p Findings -AFL_SKIP_BIN_CHECK=1 afl-fuzz -i "Testcases/$1" -o "Findings/$1" -t 5000 ./bin/Debug/net7.0/Emby.Server.Implementations.Fuzz "$1" +AFL_SKIP_BIN_CHECK=1 afl-fuzz -i "Testcases/$1" -o "Findings/$1" -t 5000 ./bin/Debug/net8.0/Emby.Server.Implementations.Fuzz "$1" diff --git a/fuzz/Jellyfin.Api.Fuzz/Jellyfin.Api.Fuzz.csproj b/fuzz/Jellyfin.Api.Fuzz/Jellyfin.Api.Fuzz.csproj index da46e63a5e..faac7d976f 100644 --- a/fuzz/Jellyfin.Api.Fuzz/Jellyfin.Api.Fuzz.csproj +++ b/fuzz/Jellyfin.Api.Fuzz/Jellyfin.Api.Fuzz.csproj @@ -2,7 +2,7 @@ <PropertyGroup> <OutputType>Exe</OutputType> - <TargetFramework>net7.0</TargetFramework> + <TargetFramework>net8.0</TargetFramework> </PropertyGroup> <ItemGroup> diff --git a/fuzz/Jellyfin.Api.Fuzz/fuzz.sh b/fuzz/Jellyfin.Api.Fuzz/fuzz.sh index edf9655626..96b0192cf7 100755 --- a/fuzz/Jellyfin.Api.Fuzz/fuzz.sh +++ b/fuzz/Jellyfin.Api.Fuzz/fuzz.sh @@ -8,4 +8,4 @@ cp bin/Jellyfin.Api.dll . dotnet build mkdir -p Findings -AFL_SKIP_BIN_CHECK=1 afl-fuzz -i "Testcases/$1" -o "Findings/$1" -t 5000 ./bin/Debug/net7.0/Jellyfin.Api.Fuzz "$1" +AFL_SKIP_BIN_CHECK=1 afl-fuzz -i "Testcases/$1" -o "Findings/$1" -t 5000 ./bin/Debug/net8.0/Jellyfin.Api.Fuzz "$1" diff --git a/global.json b/global.json index 24335d7a0f..9db4b532ce 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "7.0.0", + "version": "8.0.0", "rollForward": "latestMinor" } } diff --git a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj index 3c417f8ff0..f0f8e7afcd 100644 --- a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj +++ b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj @@ -6,7 +6,7 @@ </PropertyGroup> <PropertyGroup> - <TargetFramework>net7.0</TargetFramework> + <TargetFramework>net8.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> diff --git a/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj index d7ef6f8e77..23c4c0a9a4 100644 --- a/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj +++ b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj @@ -6,7 +6,7 @@ </PropertyGroup> <PropertyGroup> - <TargetFramework>net7.0</TargetFramework> + <TargetFramework>net8.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> diff --git a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj index 997df6dbed..c91f5d008e 100644 --- a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj +++ b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj @@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>net7.0</TargetFramework> + <TargetFramework>net8.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <PublishRepositoryUrl>true</PublishRepositoryUrl> diff --git a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj index 76dde1cf6a..ee79802a1e 100644 --- a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj +++ b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj @@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>net7.0</TargetFramework> + <TargetFramework>net8.0</TargetFramework> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> diff --git a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj index 0d91a447bc..c79dcee3c4 100644 --- a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj +++ b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj @@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>net7.0</TargetFramework> + <TargetFramework>net8.0</TargetFramework> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index de8fc1bb8b..bec3481cb7 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -4,7 +4,7 @@ <Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" /> <PropertyGroup> - <TargetFramework>net7.0</TargetFramework> + <TargetFramework>net8.0</TargetFramework> <IsPackable>false</IsPackable> <CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)/jellyfin-tests.ruleset</CodeAnalysisRuleSet> </PropertyGroup> From 99e0d46ad93c1f2e62aed67c26b92f256610f1a6 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Mon, 23 Oct 2023 23:45:01 +0200 Subject: [PATCH 823/858] Use System.Net.IPNetwork --- Emby.Dlna/Main/DlnaEntryPoint.cs | 363 ++++++++++++++++++ Jellyfin.Networking/Manager/NetworkManager.cs | 17 +- .../ApiServiceCollectionExtensions.cs | 3 +- MediaBrowser.Common/Net/NetworkConstants.cs | 1 - MediaBrowser.Common/Net/NetworkUtils.cs | 3 +- MediaBrowser.Model/MediaBrowser.Model.csproj | 1 - MediaBrowser.Model/Net/IPData.cs | 5 +- .../Jellyfin.Drawing.Skia.csproj | 2 + .../ParseNetworkTests.cs | 3 +- 9 files changed, 379 insertions(+), 19 deletions(-) create mode 100644 Emby.Dlna/Main/DlnaEntryPoint.cs diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs new file mode 100644 index 0000000000..dbc47d9816 --- /dev/null +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -0,0 +1,363 @@ +#nullable disable + +#pragma warning disable CS1591 + +using System; +using System.Globalization; +using System.Linq; +using System.Net.Http; +using System.Net.Sockets; +using System.Threading.Tasks; +using Emby.Dlna.PlayTo; +using Emby.Dlna.Ssdp; +using Jellyfin.Networking.Configuration; +using Jellyfin.Networking.Extensions; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Dlna; +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Globalization; +using Microsoft.Extensions.Logging; +using Rssdp; +using Rssdp.Infrastructure; + +namespace Emby.Dlna.Main +{ + public sealed class DlnaEntryPoint : IServerEntryPoint, IRunBeforeStartup + { + private readonly IServerConfigurationManager _config; + private readonly ILogger<DlnaEntryPoint> _logger; + private readonly IServerApplicationHost _appHost; + private readonly ISessionManager _sessionManager; + private readonly IHttpClientFactory _httpClientFactory; + private readonly ILibraryManager _libraryManager; + private readonly IUserManager _userManager; + private readonly IDlnaManager _dlnaManager; + private readonly IImageProcessor _imageProcessor; + private readonly IUserDataManager _userDataManager; + private readonly ILocalizationManager _localization; + private readonly IMediaSourceManager _mediaSourceManager; + private readonly IMediaEncoder _mediaEncoder; + private readonly IDeviceDiscovery _deviceDiscovery; + private readonly ISsdpCommunicationsServer _communicationsServer; + private readonly INetworkManager _networkManager; + private readonly object _syncLock = new(); + private readonly bool _disabled; + + private PlayToManager _manager; + private SsdpDevicePublisher _publisher; + + private bool _disposed; + + public DlnaEntryPoint( + IServerConfigurationManager config, + ILoggerFactory loggerFactory, + IServerApplicationHost appHost, + ISessionManager sessionManager, + IHttpClientFactory httpClientFactory, + ILibraryManager libraryManager, + IUserManager userManager, + IDlnaManager dlnaManager, + IImageProcessor imageProcessor, + IUserDataManager userDataManager, + ILocalizationManager localizationManager, + IMediaSourceManager mediaSourceManager, + IDeviceDiscovery deviceDiscovery, + IMediaEncoder mediaEncoder, + ISsdpCommunicationsServer communicationsServer, + INetworkManager networkManager) + { + _config = config; + _appHost = appHost; + _sessionManager = sessionManager; + _httpClientFactory = httpClientFactory; + _libraryManager = libraryManager; + _userManager = userManager; + _dlnaManager = dlnaManager; + _imageProcessor = imageProcessor; + _userDataManager = userDataManager; + _localization = localizationManager; + _mediaSourceManager = mediaSourceManager; + _deviceDiscovery = deviceDiscovery; + _mediaEncoder = mediaEncoder; + _communicationsServer = communicationsServer; + _networkManager = networkManager; + _logger = loggerFactory.CreateLogger<DlnaEntryPoint>(); + + var netConfig = config.GetConfiguration<NetworkConfiguration>(NetworkConfigurationStore.StoreKey); + _disabled = appHost.ListenWithHttps && netConfig.RequireHttps; + + if (_disabled && _config.GetDlnaConfiguration().EnableServer) + { + _logger.LogError("The DLNA specification does not support HTTPS."); + } + } + + public async Task RunAsync() + { + await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false); + + if (_disabled) + { + // No use starting as dlna won't work, as we're running purely on HTTPS. + return; + } + + ReloadComponents(); + + _config.NamedConfigurationUpdated += OnNamedConfigurationUpdated; + } + + private void OnNamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e) + { + if (string.Equals(e.Key, "dlna", StringComparison.OrdinalIgnoreCase)) + { + ReloadComponents(); + } + } + + private void ReloadComponents() + { + var options = _config.GetDlnaConfiguration(); + StartDeviceDiscovery(); + + if (options.EnableServer) + { + StartDevicePublisher(options); + } + else + { + DisposeDevicePublisher(); + } + + if (options.EnablePlayTo) + { + StartPlayToManager(); + } + else + { + DisposePlayToManager(); + } + } + + private void StartDeviceDiscovery() + { + try + { + ((DeviceDiscovery)_deviceDiscovery).Start(_communicationsServer); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error starting device discovery"); + } + } + + public void StartDevicePublisher(Configuration.DlnaOptions options) + { + if (_publisher is not null) + { + return; + } + + try + { + _publisher = new SsdpDevicePublisher( + _communicationsServer, + Environment.OSVersion.Platform.ToString(), + // Can not use VersionString here since that includes OS and version + Environment.OSVersion.Version.ToString(), + _config.GetDlnaConfiguration().SendOnlyMatchedHost) + { + LogFunction = (msg) => _logger.LogDebug("{Msg}", msg), + SupportPnpRootDevice = false + }; + + RegisterServerEndpoints(); + + if (options.BlastAliveMessages) + { + _publisher.StartSendingAliveNotifications(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds)); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error registering endpoint"); + } + } + + private void RegisterServerEndpoints() + { + var udn = CreateUuid(_appHost.SystemId); + var descriptorUri = "/dlna/" + udn + "/description.xml"; + + // Only get bind addresses in LAN + // IPv6 is currently unsupported + var validInterfaces = _networkManager.GetInternalBindAddresses() + .Where(x => x.Address is not null) + .Where(x => x.AddressFamily != AddressFamily.InterNetworkV6) + .ToList(); + + if (validInterfaces.Count == 0) + { + // No interfaces returned, fall back to loopback + validInterfaces = _networkManager.GetLoopbacks().ToList(); + } + + foreach (var intf in validInterfaces) + { + var fullService = "urn:schemas-upnp-org:device:MediaServer:1"; + + _logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, intf.Address); + + var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(intf.Address, false) + descriptorUri); + + var device = new SsdpRootDevice + { + CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info. + Location = uri.Uri, // Must point to the URL that serves your devices UPnP description document. + Address = intf.Address, + PrefixLength = NetworkExtensions.MaskToCidr(intf.Subnet.BaseAddress), + FriendlyName = "Jellyfin", + Manufacturer = "Jellyfin", + ModelName = "Jellyfin Server", + Uuid = udn + // This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc. + }; + + SetProperties(device, fullService); + _publisher.AddDevice(device); + + var embeddedDevices = new[] + { + "urn:schemas-upnp-org:service:ContentDirectory:1", + "urn:schemas-upnp-org:service:ConnectionManager:1", + // "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1" + }; + + foreach (var subDevice in embeddedDevices) + { + var embeddedDevice = new SsdpEmbeddedDevice + { + FriendlyName = device.FriendlyName, + Manufacturer = device.Manufacturer, + ModelName = device.ModelName, + Uuid = udn + // This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc. + }; + + SetProperties(embeddedDevice, subDevice); + device.AddDevice(embeddedDevice); + } + } + } + + private static string CreateUuid(string text) + { + if (!Guid.TryParse(text, out var guid)) + { + guid = text.GetMD5(); + } + + return guid.ToString("D", CultureInfo.InvariantCulture); + } + + private static void SetProperties(SsdpDevice device, string fullDeviceType) + { + var serviceParts = fullDeviceType + .Replace("urn:", string.Empty, StringComparison.OrdinalIgnoreCase) + .Replace(":1", string.Empty, StringComparison.OrdinalIgnoreCase) + .Split(':'); + + device.DeviceTypeNamespace = serviceParts[0].Replace('.', '-'); + device.DeviceClass = serviceParts[1]; + device.DeviceType = serviceParts[2]; + } + + private void StartPlayToManager() + { + lock (_syncLock) + { + if (_manager is not null) + { + return; + } + + try + { + _manager = new PlayToManager( + _logger, + _sessionManager, + _libraryManager, + _userManager, + _dlnaManager, + _appHost, + _imageProcessor, + _deviceDiscovery, + _httpClientFactory, + _userDataManager, + _localization, + _mediaSourceManager, + _mediaEncoder); + + _manager.Start(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error starting PlayTo manager"); + } + } + } + + private void DisposePlayToManager() + { + lock (_syncLock) + { + if (_manager is not null) + { + try + { + _logger.LogInformation("Disposing PlayToManager"); + _manager.Dispose(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error disposing PlayTo manager"); + } + + _manager = null; + } + } + } + + public void DisposeDevicePublisher() + { + if (_publisher is not null) + { + _logger.LogInformation("Disposing SsdpDevicePublisher"); + _publisher.Dispose(); + _publisher = null; + } + } + + /// <inheritdoc /> + public void Dispose() + { + if (_disposed) + { + return; + } + + DisposeDevicePublisher(); + DisposePlayToManager(); + _disposed = true; + } + } +} diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index b0fe4aba65..3c03d137be 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -11,7 +11,6 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Model.Net; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.HttpOverrides; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; @@ -530,7 +529,7 @@ namespace Jellyfin.Networking.Manager { foreach (var lan in _lanSubnets) { - var lanPrefix = lan.Prefix; + var lanPrefix = lan.BaseAddress; publishedServerUrls.Add( new PublishedServerUriOverride( new IPData(lanPrefix, new IPNetwork(lanPrefix, lan.PrefixLength)), @@ -541,7 +540,7 @@ namespace Jellyfin.Networking.Manager } else if (NetworkUtils.TryParseToSubnet(identifier, out var result) && result is not null) { - var data = new IPData(result.Prefix, result); + var data = new IPData(result.BaseAddress, result); publishedServerUrls.Add( new PublishedServerUriOverride( data, @@ -607,7 +606,7 @@ namespace Jellyfin.Networking.Manager var parts = details.Split(','); if (NetworkUtils.TryParseToSubnet(parts[0], out var subnet)) { - var address = subnet.Prefix; + var address = subnet.BaseAddress; var index = int.Parse(parts[1], CultureInfo.InvariantCulture); if (address.AddressFamily == AddressFamily.InterNetwork || address.AddressFamily == AddressFamily.InterNetworkV6) { @@ -881,7 +880,7 @@ namespace Jellyfin.Networking.Manager { if (NetworkUtils.TryParseToSubnet(address, out var subnet)) { - return IPAddress.IsLoopback(subnet.Prefix) || (_lanSubnets.Any(x => x.Contains(subnet.Prefix)) && !_excludedSubnets.Any(x => x.Contains(subnet.Prefix))); + return IPAddress.IsLoopback(subnet.BaseAddress) || (_lanSubnets.Any(x => x.Contains(subnet.BaseAddress)) && !_excludedSubnets.Any(x => x.Contains(subnet.BaseAddress))); } if (NetworkUtils.TryParseHost(address, out var addresses, IsIPv4Enabled, IsIPv6Enabled)) @@ -1112,12 +1111,12 @@ namespace Jellyfin.Networking.Manager var logLevel = debug ? LogLevel.Debug : LogLevel.Information; if (_logger.IsEnabled(logLevel)) { - _logger.Log(logLevel, "Defined LAN addresses: {0}", _lanSubnets.Select(s => s.Prefix + "/" + s.PrefixLength)); - _logger.Log(logLevel, "Defined LAN exclusions: {0}", _excludedSubnets.Select(s => s.Prefix + "/" + s.PrefixLength)); - _logger.Log(logLevel, "Using LAN addresses: {0}", _lanSubnets.Where(s => !_excludedSubnets.Contains(s)).Select(s => s.Prefix + "/" + s.PrefixLength)); + _logger.Log(logLevel, "Defined LAN addresses: {0}", _lanSubnets.Select(s => s.BaseAddress + "/" + s.PrefixLength)); + _logger.Log(logLevel, "Defined LAN exclusions: {0}", _excludedSubnets.Select(s => s.BaseAddress + "/" + s.PrefixLength)); + _logger.Log(logLevel, "Using LAN addresses: {0}", _lanSubnets.Where(s => !_excludedSubnets.Contains(s)).Select(s => s.BaseAddress + "/" + s.PrefixLength)); _logger.Log(logLevel, "Using bind addresses: {0}", _interfaces.OrderByDescending(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => x.Address)); _logger.Log(logLevel, "Remote IP filter is {0}", config.IsRemoteIPFilterBlacklist ? "Blocklist" : "Allowlist"); - _logger.Log(logLevel, "Filter list: {0}", _remoteAddressFilter.Select(s => s.Prefix + "/" + s.PrefixLength)); + _logger.Log(logLevel, "Filter list: {0}", _remoteAddressFilter.Select(s => s.BaseAddress + "/" + s.PrefixLength)); } } } diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 89f9c08e7e..753029f2cd 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -30,7 +30,6 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Cors.Infrastructure; -using Microsoft.AspNetCore.HttpOverrides; using Microsoft.Extensions.DependencyInjection; using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Interfaces; @@ -280,7 +279,7 @@ namespace Jellyfin.Server.Extensions { if (subnet is not null) { - AddIPAddress(config, options, subnet.Prefix, subnet.PrefixLength); + AddIPAddress(config, options, subnet.BaseAddress, subnet.PrefixLength); } } else if (NetworkUtils.TryParseHost(allowedProxies[i], out var addresses, config.EnableIPv4, config.EnableIPv6)) diff --git a/MediaBrowser.Common/Net/NetworkConstants.cs b/MediaBrowser.Common/Net/NetworkConstants.cs index 396bc8fb50..fe7ab89632 100644 --- a/MediaBrowser.Common/Net/NetworkConstants.cs +++ b/MediaBrowser.Common/Net/NetworkConstants.cs @@ -1,5 +1,4 @@ using System.Net; -using Microsoft.AspNetCore.HttpOverrides; namespace MediaBrowser.Common.Net; diff --git a/MediaBrowser.Common/Net/NetworkUtils.cs b/MediaBrowser.Common/Net/NetworkUtils.cs index f3bff7fa95..452cb694ed 100644 --- a/MediaBrowser.Common/Net/NetworkUtils.cs +++ b/MediaBrowser.Common/Net/NetworkUtils.cs @@ -5,6 +5,7 @@ using System.Net; using System.Net.Sockets; using System.Text.RegularExpressions; using Jellyfin.Extensions; +using Jellyfin.Networking.Constants; using Microsoft.AspNetCore.HttpOverrides; namespace MediaBrowser.Common.Net; @@ -335,7 +336,7 @@ public static partial class NetworkUtils /// <returns>The broadcast address.</returns> public static IPAddress GetBroadcastAddress(IPNetwork network) { - var addressBytes = network.Prefix.GetAddressBytes(); + var addressBytes = network.BaseAddress.GetAddressBytes(); uint ipAddress = BitConverter.ToUInt32(addressBytes, 0); uint ipMaskV4 = BitConverter.ToUInt32(CidrToMask(network.PrefixLength, AddressFamily.InterNetwork).GetAddressBytes(), 0); uint broadCastIPAddress = ipAddress | ~ipMaskV4; diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 89ec156a9f..1ca5e4327c 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -33,7 +33,6 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.AspNetCore.HttpOverrides" /> <PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" /> <PackageReference Include="MimeTypes"> diff --git a/MediaBrowser.Model/Net/IPData.cs b/MediaBrowser.Model/Net/IPData.cs index e9fcd67975..e016ffea10 100644 --- a/MediaBrowser.Model/Net/IPData.cs +++ b/MediaBrowser.Model/Net/IPData.cs @@ -1,6 +1,5 @@ using System.Net; using System.Net.Sockets; -using Microsoft.AspNetCore.HttpOverrides; namespace MediaBrowser.Model.Net; @@ -66,9 +65,9 @@ public class IPData { if (Address.Equals(IPAddress.None)) { - return Subnet.Prefix.AddressFamily.Equals(IPAddress.None) + return Subnet.BaseAddress.AddressFamily.Equals(IPAddress.None) ? AddressFamily.Unspecified - : Subnet.Prefix.AddressFamily; + : Subnet.BaseAddress.AddressFamily; } else { diff --git a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj index f0f8e7afcd..0590ded32a 100644 --- a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj +++ b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj @@ -9,6 +9,8 @@ <TargetFramework>net8.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> + <!-- TODO: Remove once we update SkiaSharp > 2.88.5 --> + <NoWarn>NU1903</NoWarn> </PropertyGroup> <ItemGroup> diff --git a/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs index e0b65cbc44..8d464d4e75 100644 --- a/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs +++ b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs @@ -6,7 +6,6 @@ using Jellyfin.Server.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.HttpOverrides; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; using Moq; @@ -99,7 +98,7 @@ namespace Jellyfin.Server.Tests Assert.Equal(knownNetworks.Length, options.KnownNetworks.Count); foreach (var item in knownNetworks) { - Assert.NotNull(options.KnownNetworks.FirstOrDefault(x => x.Prefix.Equals(item.Prefix) && x.PrefixLength == item.PrefixLength)); + Assert.NotNull(options.KnownNetworks.FirstOrDefault(x => x.BaseAddress.Equals(item.BaseAddress) && x.PrefixLength == item.PrefixLength)); } } From b62b0ec2b581369de42c69305773f0edb9d701b4 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Tue, 24 Oct 2023 00:10:31 +0200 Subject: [PATCH 824/858] Fix warnings --- Emby.Server.Implementations/ApplicationHost.cs | 1 + .../EntryPoints/UdpServerEntryPoint.cs | 1 + .../LiveTv/TunerHosts/LiveStream.cs | 6 ++---- Jellyfin.Api/Auth/CustomAuthenticationHandler.cs | 5 ++--- Jellyfin.Api/Controllers/ImageController.cs | 16 ++++++++-------- Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 4 ++-- Jellyfin.Api/Helpers/StreamingHelpers.cs | 14 +++++++------- Jellyfin.Api/Helpers/TranscodingJobHelper.cs | 2 +- .../HappyEyeballs/HttpClientExtension.cs | 6 +++--- Jellyfin.Networking/Manager/NetworkManager.cs | 11 ++++++----- .../Security/AuthorizationContext.cs | 2 +- .../Users/UserManager.cs | 2 +- .../Extensions/ApiServiceCollectionExtensions.cs | 8 +++++--- .../Migrations/Routines/MigrateRatingLevels.cs | 6 +----- MediaBrowser.Common/Net/NetworkUtils.cs | 2 +- .../MediaEncoding/EncodingHelper.cs | 1 + .../Probing/ProbeResultNormalizer.cs | 4 ++-- MediaBrowser.Model/Dlna/StreamBuilder.cs | 11 +++-------- MediaBrowser.Providers/Lyric/LrcLyricParser.cs | 2 +- .../Manager/ItemImageProvider.cs | 4 ++-- .../MediaInfo/AudioImageProvider.cs | 4 ++-- .../MediaInfo/MediaInfoResolver.cs | 2 +- .../Music/AlbumMetadataService.cs | 2 +- .../Plugins/AudioDb/AudioDbAlbumImageProvider.cs | 2 +- .../AudioDb/AudioDbArtistImageProvider.cs | 2 +- .../TV/SeriesMetadataService.cs | 2 +- MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs | 4 ++-- jellyfin.ruleset | 3 +++ .../FfProbe/FfProbeKeyframeExtractor.cs | 7 ++++--- 29 files changed, 67 insertions(+), 69 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 40aee063eb..9affe235db 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -99,6 +99,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Prometheus.DotNetRuntime; using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; +using IConfigurationManager = MediaBrowser.Common.Configuration.IConfigurationManager; using WebSocketManager = Emby.Server.Implementations.HttpServer.WebSocketManager; namespace Emby.Server.Implementations diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs index 18e60b2101..56ccb21ee7 100644 --- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs +++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs @@ -12,6 +12,7 @@ using MediaBrowser.Controller; using MediaBrowser.Controller.Plugins; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; +using IConfigurationManager = MediaBrowser.Common.Configuration.IConfigurationManager; namespace Emby.Server.Implementations.EntryPoints { diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs index 3ae9e256b2..767b941366 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs @@ -84,15 +84,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts return Task.CompletedTask; } - public Task Close() + public async Task Close() { EnableStreamSharing = false; Logger.LogInformation("Closing {Type}", GetType().Name); - LiveStreamCancellationTokenSource.Cancel(); - - return Task.CompletedTask; + await LiveStreamCancellationTokenSource.CancelAsync().ConfigureAwait(false); } public Stream GetStream() diff --git a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs index bd3e7d9e3e..2853e69b01 100644 --- a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs +++ b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs @@ -27,13 +27,12 @@ namespace Jellyfin.Api.Auth /// <param name="options">Options monitor.</param> /// <param name="logger">The logger.</param> /// <param name="encoder">The url encoder.</param> - /// <param name="clock">The system clock.</param> public CustomAuthenticationHandler( IAuthService authService, IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, - UrlEncoder encoder, - ISystemClock clock) : base(options, logger, encoder, clock) + UrlEncoder encoder) + : base(options, logger, encoder) { _authService = authService; _logger = logger.CreateLogger<CustomAuthenticationHandler>(); diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index 1e6580ed15..7d1a54eced 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -2080,30 +2080,30 @@ public class ImageController : BaseJellyfinApiController foreach (var (key, value) in headers) { - Response.Headers.Add(key, value); + Response.Headers.Append(key, value); } Response.ContentType = imageContentType ?? MediaTypeNames.Text.Plain; - Response.Headers.Add(HeaderNames.Age, Convert.ToInt64((DateTime.UtcNow - dateImageModified).TotalSeconds).ToString(CultureInfo.InvariantCulture)); - Response.Headers.Add(HeaderNames.Vary, HeaderNames.Accept); + Response.Headers.Append(HeaderNames.Age, Convert.ToInt64((DateTime.UtcNow - dateImageModified).TotalSeconds).ToString(CultureInfo.InvariantCulture)); + Response.Headers.Append(HeaderNames.Vary, HeaderNames.Accept); if (disableCaching) { - Response.Headers.Add(HeaderNames.CacheControl, "no-cache, no-store, must-revalidate"); - Response.Headers.Add(HeaderNames.Pragma, "no-cache, no-store, must-revalidate"); + Response.Headers.Append(HeaderNames.CacheControl, "no-cache, no-store, must-revalidate"); + Response.Headers.Append(HeaderNames.Pragma, "no-cache, no-store, must-revalidate"); } else { if (cacheDuration.HasValue) { - Response.Headers.Add(HeaderNames.CacheControl, "public, max-age=" + cacheDuration.Value.TotalSeconds); + Response.Headers.Append(HeaderNames.CacheControl, "public, max-age=" + cacheDuration.Value.TotalSeconds); } else { - Response.Headers.Add(HeaderNames.CacheControl, "public"); + Response.Headers.Append(HeaderNames.CacheControl, "public"); } - Response.Headers.Add(HeaderNames.LastModified, dateImageModified.ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss \"GMT\"", CultureInfo.InvariantCulture)); + Response.Headers.Append(HeaderNames.LastModified, dateImageModified.ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss \"GMT\"", CultureInfo.InvariantCulture)); // if the image was not modified since "ifModifiedSinceHeader"-header, return a HTTP status code 304 not modified if (!(dateImageModified > ifModifiedSinceHeader) && cacheDuration.HasValue) diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index 24082fcff1..a8df628f0d 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -147,7 +147,7 @@ public class DynamicHlsHelper cancellationTokenSource.Token) .ConfigureAwait(false); - _httpContextAccessor.HttpContext.Response.Headers.Add(HeaderNames.Expires, "0"); + _httpContextAccessor.HttpContext.Response.Headers.Append(HeaderNames.Expires, "0"); if (isHeadRequest) { return new FileContentResult(Array.Empty<byte>(), MimeTypes.GetMimeType("playlist.m3u8")); @@ -568,7 +568,7 @@ public class DynamicHlsHelper && state.VideoStream is not null && state.VideoStream.Level.HasValue) { - levelString = state.VideoStream.Level.ToString() ?? string.Empty; + levelString = state.VideoStream.Level.Value.ToString(CultureInfo.InvariantCulture) ?? string.Empty; } else { diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index 6fbbceeabb..7d9a389312 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -279,15 +279,15 @@ public static class StreamingHelpers var profile = state.DeviceProfile; StringValues transferMode = request.Headers["transferMode.dlna.org"]; - responseHeaders.Add("transferMode.dlna.org", string.IsNullOrEmpty(transferMode) ? "Streaming" : transferMode.ToString()); - responseHeaders.Add("realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*"); + responseHeaders.Append("transferMode.dlna.org", string.IsNullOrEmpty(transferMode) ? "Streaming" : transferMode.ToString()); + responseHeaders.Append("realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*"); if (state.RunTimeTicks.HasValue) { if (string.Equals(request.Headers["getMediaInfo.sec"], "1", StringComparison.OrdinalIgnoreCase)) { var ms = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalMilliseconds; - responseHeaders.Add("MediaInfo.sec", string.Format( + responseHeaders.Append("MediaInfo.sec", string.Format( CultureInfo.InvariantCulture, "SEC_Duration={0};", Convert.ToInt32(ms))); @@ -305,7 +305,7 @@ public static class StreamingHelpers if (!state.IsVideoRequest) { - responseHeaders.Add("contentFeatures.dlna.org", ContentFeatureBuilder.BuildAudioHeader( + responseHeaders.Append("contentFeatures.dlna.org", ContentFeatureBuilder.BuildAudioHeader( profile, state.OutputContainer, audioCodec, @@ -321,7 +321,7 @@ public static class StreamingHelpers { var videoCodec = state.ActualOutputVideoCodec; - responseHeaders.Add( + responseHeaders.Append( "contentFeatures.dlna.org", ContentFeatureBuilder.BuildVideoHeader(profile, state.OutputContainer, videoCodec, audioCodec, state.OutputWidth, state.OutputHeight, state.TargetVideoBitDepth, state.OutputVideoBitrate, state.TargetTimestamp, isStaticallyStreamed, state.RunTimeTicks, state.TargetVideoProfile, state.TargetVideoRangeType, state.TargetVideoLevel, state.TargetFramerate, state.TargetPacketLength, state.TranscodeSeekInfo, state.IsTargetAnamorphic, state.IsTargetInterlaced, state.TargetRefFrames, state.TargetVideoStreamCount, state.TargetAudioStreamCount, state.TargetVideoCodecTag, state.IsTargetAVC).FirstOrDefault() ?? string.Empty); } @@ -404,12 +404,12 @@ public static class StreamingHelpers var runtimeSeconds = TimeSpan.FromTicks(state.RunTimeTicks!.Value).TotalSeconds.ToString(CultureInfo.InvariantCulture); var startSeconds = TimeSpan.FromTicks(startTimeTicks ?? 0).TotalSeconds.ToString(CultureInfo.InvariantCulture); - responseHeaders.Add("TimeSeekRange.dlna.org", string.Format( + responseHeaders.Append("TimeSeekRange.dlna.org", string.Format( CultureInfo.InvariantCulture, "npt={0}-{1}/{1}", startSeconds, runtimeSeconds)); - responseHeaders.Add("X-AvailableSeekRange", string.Format( + responseHeaders.Append("X-AvailableSeekRange", string.Format( CultureInfo.InvariantCulture, "1 npt={0}-{1}", startSeconds, diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index c16a586d60..5b407e4a95 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -280,6 +280,7 @@ public class TranscodingJobHelper : IDisposable if (job.CancellationTokenSource?.IsCancellationRequested == false) { +#pragma warning disable CA1849 // Can't await in lock block job.CancellationTokenSource.Cancel(); } } @@ -291,7 +292,6 @@ public class TranscodingJobHelper : IDisposable lock (job.ProcessLock!) { -#pragma warning disable CA1849 // Can't await in lock block job.TranscodingThrottler?.Stop().GetAwaiter().GetResult(); var process = job.Process; diff --git a/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs b/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs index 59e6956c71..d59e4e5e38 100644 --- a/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs +++ b/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs @@ -65,7 +65,7 @@ namespace Jellyfin.Networking.HappyEyeballs // See https://github.com/dotnet/corefx/pull/29792/files#r189415885 for more details. if (await Task.WhenAny(tryConnectAsyncIPv6, Task.Delay(200, cancelIPv6.Token)).ConfigureAwait(false) == tryConnectAsyncIPv6 && tryConnectAsyncIPv6.IsCompletedSuccessfully) { - cancelIPv6.Cancel(); + await cancelIPv6.CancelAsync().ConfigureAwait(false); return tryConnectAsyncIPv6.GetAwaiter().GetResult(); } @@ -76,7 +76,7 @@ namespace Jellyfin.Networking.HappyEyeballs { if (tryConnectAsyncIPv6.IsCompletedSuccessfully) { - cancelIPv4.Cancel(); + await cancelIPv4.CancelAsync().ConfigureAwait(false); return tryConnectAsyncIPv6.GetAwaiter().GetResult(); } @@ -86,7 +86,7 @@ namespace Jellyfin.Networking.HappyEyeballs { if (tryConnectAsyncIPv4.IsCompletedSuccessfully) { - cancelIPv6.Cancel(); + await cancelIPv6.CancelAsync().ConfigureAwait(false); return tryConnectAsyncIPv4.GetAwaiter().GetResult(); } diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 3c03d137be..dbf7127a68 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; +using IConfigurationManager = MediaBrowser.Common.Configuration.IConfigurationManager; namespace Jellyfin.Networking.Manager { @@ -422,7 +423,7 @@ namespace Jellyfin.Networking.Manager { // Parse config values into filter collection var remoteIPFilter = config.RemoteIPFilter; - if (remoteIPFilter.Any() && !string.IsNullOrWhiteSpace(remoteIPFilter.First())) + if (remoteIPFilter.Length != 0 && !string.IsNullOrWhiteSpace(remoteIPFilter[0])) { // Parse all IPs with netmask to a subnet var remoteAddressFilter = new List<IPNetwork>(); @@ -540,7 +541,7 @@ namespace Jellyfin.Networking.Manager } else if (NetworkUtils.TryParseToSubnet(identifier, out var result) && result is not null) { - var data = new IPData(result.BaseAddress, result); + var data = new IPData(result.Value.BaseAddress, result); publishedServerUrls.Add( new PublishedServerUriOverride( data, @@ -606,11 +607,11 @@ namespace Jellyfin.Networking.Manager var parts = details.Split(','); if (NetworkUtils.TryParseToSubnet(parts[0], out var subnet)) { - var address = subnet.BaseAddress; + var address = subnet.Value.BaseAddress; var index = int.Parse(parts[1], CultureInfo.InvariantCulture); if (address.AddressFamily == AddressFamily.InterNetwork || address.AddressFamily == AddressFamily.InterNetworkV6) { - var data = new IPData(address, subnet, parts[2]) + var data = new IPData(address, subnet.Value, parts[2]) { Index = index }; @@ -880,7 +881,7 @@ namespace Jellyfin.Networking.Manager { if (NetworkUtils.TryParseToSubnet(address, out var subnet)) { - return IPAddress.IsLoopback(subnet.BaseAddress) || (_lanSubnets.Any(x => x.Contains(subnet.BaseAddress)) && !_excludedSubnets.Any(x => x.Contains(subnet.BaseAddress))); + return IPAddress.IsLoopback(subnet.Value.BaseAddress) || (_lanSubnets.Any(x => x.Contains(subnet.Value.BaseAddress)) && !_excludedSubnets.Any(x => x.Contains(subnet.Value.BaseAddress))); } if (NetworkUtils.TryParseHost(address, out var addresses, IsIPv4Enabled, IsIPv6Enabled)) diff --git a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs index 77f8f7071b..6bda12c5b4 100644 --- a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs +++ b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs @@ -60,7 +60,7 @@ namespace Jellyfin.Server.Implementations.Security } private async Task<AuthorizationInfo> GetAuthorizationInfoFromDictionary( - IReadOnlyDictionary<string, string>? auth, + Dictionary<string, string>? auth, IHeaderDictionary headers, IQueryCollection queryString) { diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index edae4cfc5e..075f4cb3a1 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -775,7 +775,7 @@ namespace Jellyfin.Server.Implementations.Users return providers; } - private IList<IPasswordResetProvider> GetPasswordResetProviders(User user) + private IPasswordResetProvider[] GetPasswordResetProviders(User user) { var passwordResetProviderId = user.PasswordResetProviderId; var providers = _passwordResetProviders.Where(i => i.IsEnabled).ToArray(); diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 753029f2cd..92c483c0fe 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -30,12 +30,14 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Cors.Infrastructure; +using Microsoft.AspNetCore.HttpOverrides; using Microsoft.Extensions.DependencyInjection; using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; using AuthenticationSchemes = Jellyfin.Api.Constants.AuthenticationSchemes; +using IPNetwork = System.Net.IPNetwork; namespace Jellyfin.Server.Extensions { @@ -277,9 +279,9 @@ namespace Jellyfin.Server.Extensions } else if (NetworkUtils.TryParseToSubnet(allowedProxies[i], out var subnet)) { - if (subnet is not null) + if (subnet.HasValue) { - AddIPAddress(config, options, subnet.BaseAddress, subnet.PrefixLength); + AddIPAddress(config, options, subnet.Value.BaseAddress, subnet.Value.PrefixLength); } } else if (NetworkUtils.TryParseHost(allowedProxies[i], out var addresses, config.EnableIPv4, config.EnableIPv6)) @@ -310,7 +312,7 @@ namespace Jellyfin.Server.Extensions } else { - options.KnownNetworks.Add(new IPNetwork(addr, prefixLength)); + options.KnownNetworks.Add(new Microsoft.AspNetCore.HttpOverrides.IPNetwork(addr, prefixLength)); } } diff --git a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs index ac50474010..247e1d8450 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs @@ -78,11 +78,7 @@ namespace Jellyfin.Server.Migrations.Routines } else { - var ratingValue = _localizationManager.GetRatingLevel(ratingString).ToString(); - if (string.IsNullOrEmpty(ratingValue)) - { - ratingValue = "NULL"; - } + var ratingValue = _localizationManager.GetRatingLevel(ratingString)?.ToString(CultureInfo.InvariantCulture) ?? "NULL"; using var statement = connection.PrepareStatement("UPDATE TypedBaseItems SET InheritedParentalRatingValue = @Value WHERE OfficialRating = @Rating;"); statement.TryBind("@Value", ratingValue); diff --git a/MediaBrowser.Common/Net/NetworkUtils.cs b/MediaBrowser.Common/Net/NetworkUtils.cs index 452cb694ed..7ae99336f1 100644 --- a/MediaBrowser.Common/Net/NetworkUtils.cs +++ b/MediaBrowser.Common/Net/NetworkUtils.cs @@ -180,7 +180,7 @@ public static partial class NetworkUtils { if (TryParseToSubnet(values[a], out var innerResult, negated)) { - tmpResult.Add(innerResult); + tmpResult.Add(innerResult.Value); } } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 6621ae2842..46fd1ae478 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -20,6 +20,7 @@ using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.MediaInfo; using Microsoft.Extensions.Configuration; +using IConfigurationManager = MediaBrowser.Common.Configuration.IConfigurationManager; namespace MediaBrowser.Controller.MediaEncoding { diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index be1e8a1726..020e69eced 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -516,7 +516,7 @@ namespace MediaBrowser.MediaEncoding.Probing private void ProcessPairs(string key, List<NameValuePair> pairs, MediaInfo info) { - IList<BaseItemPerson> peoples = new List<BaseItemPerson>(); + List<BaseItemPerson> peoples = new List<BaseItemPerson>(); if (string.Equals(key, "studio", StringComparison.OrdinalIgnoreCase)) { info.Studios = pairs.Select(p => p.Value) @@ -1182,7 +1182,7 @@ namespace MediaBrowser.MediaEncoding.Probing info.Size = string.IsNullOrEmpty(data.Format.Size) ? null : long.Parse(data.Format.Size, CultureInfo.InvariantCulture); } - private void SetAudioInfoFromTags(MediaInfo audio, IReadOnlyDictionary<string, string> tags) + private void SetAudioInfoFromTags(MediaInfo audio, Dictionary<string, string> tags) { var people = new List<BaseItemPerson>(); if (tags.TryGetValue("composer", out var composer) && !string.IsNullOrWhiteSpace(composer)) diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 666e787951..252ac1303f 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -835,11 +835,6 @@ namespace MediaBrowser.Model.Dlna playlistItem.SetOption(qualifier, "profile", videoStream.Profile.ToLowerInvariant()); } - if (videoStream is not null && videoStream.Level != 0) - { - playlistItem.SetOption(qualifier, "level", videoStream.Level.ToString() ?? string.Empty); - } - // Prefer matching audio codecs, could do better here var audioCodecs = ContainerProfile.SplitValue(audioCodec); @@ -866,16 +861,16 @@ namespace MediaBrowser.Model.Dlna // Copy matching audio codec options playlistItem.AudioSampleRate = audioStream.SampleRate; - playlistItem.SetOption(qualifier, "audiochannels", audioStream.Channels.ToString() ?? string.Empty); + playlistItem.SetOption(qualifier, "audiochannels", audioStream.Channels?.ToString(CultureInfo.InvariantCulture) ?? string.Empty); if (!string.IsNullOrEmpty(audioStream.Profile)) { playlistItem.SetOption(audioStream.Codec, "profile", audioStream.Profile.ToLowerInvariant()); } - if (audioStream.Level != 0) + if (audioStream.Level.HasValue) { - playlistItem.SetOption(audioStream.Codec, "level", audioStream.Level.ToString() ?? string.Empty); + playlistItem.SetOption(audioStream.Codec, "level", audioStream.Level.Value.ToString(CultureInfo.InvariantCulture)); } } diff --git a/MediaBrowser.Providers/Lyric/LrcLyricParser.cs b/MediaBrowser.Providers/Lyric/LrcLyricParser.cs index 7f1ecd7435..a10ff198b4 100644 --- a/MediaBrowser.Providers/Lyric/LrcLyricParser.cs +++ b/MediaBrowser.Providers/Lyric/LrcLyricParser.cs @@ -125,7 +125,7 @@ public class LrcLyricParser : ILyricParser /// </summary> /// <param name="metaData">The metadata from the LRC file.</param> /// <returns>A lyricMetadata object with mapped property data.</returns> - private static LyricMetadata MapMetadataValues(IDictionary<string, string> metaData) + private static LyricMetadata MapMetadataValues(Dictionary<string, string> metaData) { LyricMetadata lyricMetadata = new(); diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs index dab36625e5..1a5dbd7a55 100644 --- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs +++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs @@ -175,7 +175,7 @@ namespace MediaBrowser.Providers.Manager IDynamicImageProvider provider, ImageRefreshOptions refreshOptions, TypeOptions savedOptions, - ICollection<ImageType> downloadedImages, + List<ImageType> downloadedImages, RefreshResult result, CancellationToken cancellationToken) { @@ -263,7 +263,7 @@ namespace MediaBrowser.Providers.Manager ImageRefreshOptions refreshOptions, TypeOptions savedOptions, int backdropLimit, - ICollection<ImageType> downloadedImages, + List<ImageType> downloadedImages, RefreshResult result, CancellationToken cancellationToken) { diff --git a/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs b/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs index b4b1895f51..d1c0ddb375 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs @@ -82,8 +82,8 @@ namespace MediaBrowser.Providers.MediaInfo { Directory.CreateDirectory(Path.GetDirectoryName(path)); - var imageStream = imageStreams.FirstOrDefault(i => (i.Comment ?? string.Empty).IndexOf("front", StringComparison.OrdinalIgnoreCase) != -1) ?? - imageStreams.FirstOrDefault(i => (i.Comment ?? string.Empty).IndexOf("cover", StringComparison.OrdinalIgnoreCase) != -1) ?? + var imageStream = imageStreams.FirstOrDefault(i => (i.Comment ?? string.Empty).Contains("front", StringComparison.OrdinalIgnoreCase)) ?? + imageStreams.FirstOrDefault(i => (i.Comment ?? string.Empty).Contains("cover", StringComparison.OrdinalIgnoreCase)) ?? imageStreams.FirstOrDefault(); var imageStreamIndex = imageStream?.Index; diff --git a/MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs b/MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs index 909cbb9b9c..f846aa5dec 100644 --- a/MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs +++ b/MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs @@ -183,7 +183,7 @@ namespace MediaBrowser.Providers.MediaInfo files.AddRange(directoryService.GetFilePaths(internalMetadataPath, clearCache, true)); } - if (!files.Any()) + if (files.Count == 0) { return Array.Empty<ExternalPathParserResult>(); } diff --git a/MediaBrowser.Providers/Music/AlbumMetadataService.cs b/MediaBrowser.Providers/Music/AlbumMetadataService.cs index 0ddb2ad67b..e4f34776b9 100644 --- a/MediaBrowser.Providers/Music/AlbumMetadataService.cs +++ b/MediaBrowser.Providers/Music/AlbumMetadataService.cs @@ -148,7 +148,7 @@ namespace MediaBrowser.Providers.Music .ToArray(); var id = item.GetProviderId(provider); - if (ids.Any()) + if (ids.Length != 0) { var firstId = ids[0]; if (!string.IsNullOrEmpty(firstId) diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs index 7f73afc53a..8a516e1ce7 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs @@ -73,7 +73,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb return Enumerable.Empty<RemoteImageInfo>(); } - private IEnumerable<RemoteImageInfo> GetImages(AudioDbAlbumProvider.Album item) + private List<RemoteImageInfo> GetImages(AudioDbAlbumProvider.Album item) { var list = new List<RemoteImageInfo>(); diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs index 2232dfa0d7..4e7757cd26 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs @@ -78,7 +78,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb return Enumerable.Empty<RemoteImageInfo>(); } - private IEnumerable<RemoteImageInfo> GetImages(AudioDbArtistProvider.Artist item) + private List<RemoteImageInfo> GetImages(AudioDbArtistProvider.Artist item) { var list = new List<RemoteImageInfo>(); diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs index e01c0f4830..01c07d6332 100644 --- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs +++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs @@ -121,7 +121,7 @@ namespace MediaBrowser.Providers.TV var seasonNumber = virtualSeason.IndexNumber; // If there's a physical season with the same number or no episodes in the season, delete it if ((seasonNumber.HasValue && physicalSeasonNumbers.Contains(seasonNumber.Value)) - || !virtualSeason.GetEpisodes().Any()) + || virtualSeason.GetEpisodes().Count == 0) { Logger.LogInformation("Removing virtual season {SeasonNumber} in series {SeriesName}", virtualSeason.IndexNumber, series.Name); diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs index bf66a31458..1399ac307a 100644 --- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs @@ -314,11 +314,11 @@ namespace MediaBrowser.XbmcMetadata.Savers { var codec = stream.Codec; - if ((stream.CodecTag ?? string.Empty).IndexOf("xvid", StringComparison.OrdinalIgnoreCase) != -1) + if ((stream.CodecTag ?? string.Empty).Contains("xvid", StringComparison.OrdinalIgnoreCase)) { codec = "xvid"; } - else if ((stream.CodecTag ?? string.Empty).IndexOf("divx", StringComparison.OrdinalIgnoreCase) != -1) + else if ((stream.CodecTag ?? string.Empty).Contains("divx", StringComparison.OrdinalIgnoreCase)) { codec = "divx"; } diff --git a/jellyfin.ruleset b/jellyfin.ruleset index 870cf253f2..10225e3af8 100644 --- a/jellyfin.ruleset +++ b/jellyfin.ruleset @@ -140,6 +140,9 @@ <Rule Id="CA1812" Action="Info" /> <!-- disable warning CA1822: Member does not access instance data and can be marked as static --> <Rule Id="CA1822" Action="Info" /> + <!-- TODO: Enable --> + <!-- CA1861: Prefer 'static readonly' fields over constant array arguments if the called method is called repeatedly and is not mutating the passed array --> + <Rule Id="CA1861" Action="Info" /> <!-- disable warning CA2000: Dispose objects before losing scope --> <Rule Id="CA2000" Action="Info" /> <!-- disable warning CA2253: Named placeholders should not be numeric values --> diff --git a/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs b/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs index 479e6ffdc8..720d987f13 100644 --- a/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs +++ b/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs @@ -11,8 +11,6 @@ namespace Jellyfin.MediaEncoding.Keyframes.FfProbe; /// </summary> public static class FfProbeKeyframeExtractor { - private const string DefaultArguments = "-fflags +genpts -v error -skip_frame nokey -show_entries format=duration -show_entries stream=duration -show_entries packet=pts_time,flags -select_streams v -of csv \"{0}\""; - /// <summary> /// Extracts the keyframes using the ffprobe executable at the specified path. /// </summary> @@ -26,7 +24,10 @@ public static class FfProbeKeyframeExtractor StartInfo = new ProcessStartInfo { FileName = ffProbePath, - Arguments = string.Format(CultureInfo.InvariantCulture, DefaultArguments, filePath), + Arguments = string.Format( + CultureInfo.InvariantCulture, + "-fflags +genpts -v error -skip_frame nokey -show_entries format=duration -show_entries stream=duration -show_entries packet=pts_time,flags -select_streams v -of csv \"{0}\"", + filePath), CreateNoWindow = true, UseShellExecute = false, From 0fd36a5bf1fc87ebbfd5b74585f0c080995c1688 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Tue, 14 Nov 2023 19:12:17 +0100 Subject: [PATCH 825/858] Fix warnings in test projects --- .../Controllers/UserControllerTests.cs | 2 +- .../JsonCommaDelimitedArrayTests.cs | 67 +++++++++---------- .../JsonCommaDelimitedIReadOnlyListTests.cs | 43 ++++++------ .../NetworkParseTests.cs | 1 + .../Manager/ItemImageProviderTests.cs | 14 ++-- .../Sorting/AiredEpisodeOrderComparerTests.cs | 11 ++- .../Sorting/IndexNumberComparerTests.cs | 2 +- .../Sorting/ParentIndexNumberComparerTests.cs | 2 +- .../ParseNetworkTests.cs | 3 +- 9 files changed, 69 insertions(+), 76 deletions(-) diff --git a/tests/Jellyfin.Api.Tests/Controllers/UserControllerTests.cs b/tests/Jellyfin.Api.Tests/Controllers/UserControllerTests.cs index 3f965d0ff0..c7331c7181 100644 --- a/tests/Jellyfin.Api.Tests/Controllers/UserControllerTests.cs +++ b/tests/Jellyfin.Api.Tests/Controllers/UserControllerTests.cs @@ -109,7 +109,7 @@ public class UserControllerTests v.ErrorMessage.Contains("required", StringComparison.CurrentCultureIgnoreCase)); } - private IList<ValidationResult> Validate(object model) + private List<ValidationResult> Validate(object model) { var result = new List<ValidationResult>(); var context = new ValidationContext(model, null, null); diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs index f2ca2ff081..61105b42b2 100644 --- a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs +++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs @@ -7,135 +7,128 @@ using Xunit; namespace Jellyfin.Extensions.Tests.Json.Converters { - public static class JsonCommaDelimitedArrayTests + public class JsonCommaDelimitedArrayTests { - [Fact] - public static void Deserialize_String_Null_Success() + private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions() { - var options = new JsonSerializerOptions(); - var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": null }", options); + Converters = + { + new JsonStringEnumConverter() + } + }; + + [Fact] + public void Deserialize_String_Null_Success() + { + var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": null }", _jsonOptions); Assert.Null(value?.Value); } [Fact] - public static void Deserialize_Empty_Success() + public void Deserialize_Empty_Success() { var desiredValue = new GenericBodyArrayModel<string> { Value = Array.Empty<string>() }; - var options = new JsonSerializerOptions(); - var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": """" }", options); + var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": """" }", _jsonOptions); Assert.Equal(desiredValue.Value, value?.Value); } [Fact] - public static void Deserialize_String_Valid_Success() + public void Deserialize_String_Valid_Success() { var desiredValue = new GenericBodyArrayModel<string> { Value = new[] { "a", "b", "c" } }; - var options = new JsonSerializerOptions(); - var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": ""a,b,c"" }", options); + var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": ""a,b,c"" }", _jsonOptions); Assert.Equal(desiredValue.Value, value?.Value); } [Fact] - public static void Deserialize_String_Space_Valid_Success() + public void Deserialize_String_Space_Valid_Success() { var desiredValue = new GenericBodyArrayModel<string> { Value = new[] { "a", "b", "c" } }; - var options = new JsonSerializerOptions(); - var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": ""a, b, c"" }", options); + var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": ""a, b, c"" }", _jsonOptions); Assert.Equal(desiredValue.Value, value?.Value); } [Fact] - public static void Deserialize_GenericCommandType_Valid_Success() + public void Deserialize_GenericCommandType_Valid_Success() { var desiredValue = new GenericBodyArrayModel<GeneralCommandType> { Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } }; - var options = new JsonSerializerOptions(); - options.Converters.Add(new JsonStringEnumConverter()); - var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,MoveDown"" }", options); + var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,MoveDown"" }", _jsonOptions); Assert.Equal(desiredValue.Value, value?.Value); } [Fact] - public static void Deserialize_GenericCommandType_EmptyEntry_Success() + public void Deserialize_GenericCommandType_EmptyEntry_Success() { var desiredValue = new GenericBodyArrayModel<GeneralCommandType> { Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } }; - var options = new JsonSerializerOptions(); - options.Converters.Add(new JsonStringEnumConverter()); - var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,,MoveDown"" }", options); + var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,,MoveDown"" }", _jsonOptions); Assert.Equal(desiredValue.Value, value?.Value); } [Fact] - public static void Deserialize_GenericCommandType_Invalid_Success() + public void Deserialize_GenericCommandType_Invalid_Success() { var desiredValue = new GenericBodyArrayModel<GeneralCommandType> { Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } }; - var options = new JsonSerializerOptions(); - options.Converters.Add(new JsonStringEnumConverter()); - var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,TotallyNotAVallidCommand,MoveDown"" }", options); + var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,TotallyNotAVallidCommand,MoveDown"" }", _jsonOptions); Assert.Equal(desiredValue.Value, value?.Value); } [Fact] - public static void Deserialize_GenericCommandType_Space_Valid_Success() + public void Deserialize_GenericCommandType_Space_Valid_Success() { var desiredValue = new GenericBodyArrayModel<GeneralCommandType> { Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } }; - var options = new JsonSerializerOptions(); - options.Converters.Add(new JsonStringEnumConverter()); - var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp, MoveDown"" }", options); + var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp, MoveDown"" }", _jsonOptions); Assert.Equal(desiredValue.Value, value?.Value); } [Fact] - public static void Deserialize_String_Array_Valid_Success() + public void Deserialize_String_Array_Valid_Success() { var desiredValue = new GenericBodyArrayModel<string> { Value = new[] { "a", "b", "c" } }; - var options = new JsonSerializerOptions(); - var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": [""a"",""b"",""c""] }", options); + var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": [""a"",""b"",""c""] }", _jsonOptions); Assert.Equal(desiredValue.Value, value?.Value); } [Fact] - public static void Deserialize_GenericCommandType_Array_Valid_Success() + public void Deserialize_GenericCommandType_Array_Valid_Success() { var desiredValue = new GenericBodyArrayModel<GeneralCommandType> { Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } }; - var options = new JsonSerializerOptions(); - options.Converters.Add(new JsonStringEnumConverter()); - var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", options); + var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", _jsonOptions); Assert.Equal(desiredValue.Value, value?.Value); } } diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedIReadOnlyListTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedIReadOnlyListTests.cs index 92886dcd28..9b977b9a5d 100644 --- a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedIReadOnlyListTests.cs +++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedIReadOnlyListTests.cs @@ -6,86 +6,85 @@ using Xunit; namespace Jellyfin.Extensions.Tests.Json.Converters { - public static class JsonCommaDelimitedIReadOnlyListTests + public class JsonCommaDelimitedIReadOnlyListTests { + private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions() + { + Converters = + { + new JsonStringEnumConverter() + } + }; + [Fact] - public static void Deserialize_String_Valid_Success() + public void Deserialize_String_Valid_Success() { var desiredValue = new GenericBodyIReadOnlyListModel<string> { Value = new[] { "a", "b", "c" } }; - var options = new JsonSerializerOptions(); - var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<string>>(@"{ ""Value"": ""a,b,c"" }", options); + var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<string>>(@"{ ""Value"": ""a,b,c"" }", _jsonOptions); Assert.Equal(desiredValue.Value, value?.Value); } [Fact] - public static void Deserialize_String_Space_Valid_Success() + public void Deserialize_String_Space_Valid_Success() { var desiredValue = new GenericBodyIReadOnlyListModel<string> { Value = new[] { "a", "b", "c" } }; - var options = new JsonSerializerOptions(); - var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<string>>(@"{ ""Value"": ""a, b, c"" }", options); + var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<string>>(@"{ ""Value"": ""a, b, c"" }", _jsonOptions); Assert.Equal(desiredValue.Value, value?.Value); } [Fact] - public static void Deserialize_GenericCommandType_Valid_Success() + public void Deserialize_GenericCommandType_Valid_Success() { var desiredValue = new GenericBodyIReadOnlyListModel<GeneralCommandType> { Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } }; - var options = new JsonSerializerOptions(); - options.Converters.Add(new JsonStringEnumConverter()); - var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,MoveDown"" }", options); + var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,MoveDown"" }", _jsonOptions); Assert.Equal(desiredValue.Value, value?.Value); } [Fact] - public static void Deserialize_GenericCommandType_Space_Valid_Success() + public void Deserialize_GenericCommandType_Space_Valid_Success() { var desiredValue = new GenericBodyIReadOnlyListModel<GeneralCommandType> { Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } }; - var options = new JsonSerializerOptions(); - options.Converters.Add(new JsonStringEnumConverter()); - var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp, MoveDown"" }", options); + var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp, MoveDown"" }", _jsonOptions); Assert.Equal(desiredValue.Value, value?.Value); } [Fact] - public static void Deserialize_String_Array_Valid_Success() + public void Deserialize_String_Array_Valid_Success() { var desiredValue = new GenericBodyIReadOnlyListModel<string> { Value = new[] { "a", "b", "c" } }; - var options = new JsonSerializerOptions(); - var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<string>>(@"{ ""Value"": [""a"",""b"",""c""] }", options); + var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<string>>(@"{ ""Value"": [""a"",""b"",""c""] }", _jsonOptions); Assert.Equal(desiredValue.Value, value?.Value); } [Fact] - public static void Deserialize_GenericCommandType_Array_Valid_Success() + public void Deserialize_GenericCommandType_Array_Valid_Success() { var desiredValue = new GenericBodyIReadOnlyListModel<GeneralCommandType> { Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } }; - var options = new JsonSerializerOptions(); - options.Converters.Add(new JsonStringEnumConverter()); - var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<GeneralCommandType>>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", options); + var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<GeneralCommandType>>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", _jsonOptions); Assert.Equal(desiredValue.Value, value?.Value); } } diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs index 93514a5017..19aaa5ef8f 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs @@ -10,6 +10,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; +using IConfigurationManager = MediaBrowser.Common.Configuration.IConfigurationManager; namespace Jellyfin.Networking.Tests { diff --git a/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs b/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs index d5ab6ab4be..be5a401b1a 100644 --- a/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs +++ b/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs @@ -27,7 +27,7 @@ namespace Jellyfin.Providers.Tests.Manager { public partial class ItemImageProviderTests { - private const string TestDataImagePath = "Test Data/Images/blank{0}.jpg"; + private static readonly CompositeFormat _testDataImagePath = CompositeFormat.Parse("Test Data/Images/blank{0}.jpg"); [GeneratedRegex("[0-9]+")] private static partial Regex NumbersRegex(); @@ -275,7 +275,7 @@ namespace Jellyfin.Providers.Tests.Manager { HasImage = true, Format = ImageFormat.Jpg, - Path = responseHasPath ? string.Format(CultureInfo.InvariantCulture, TestDataImagePath, 0) : null, + Path = responseHasPath ? string.Format(CultureInfo.InvariantCulture, _testDataImagePath, 0) : null, Protocol = protocol }; @@ -563,21 +563,21 @@ namespace Jellyfin.Providers.Tests.Manager mockFileSystem.Setup(fs => fs.GetFilePaths(It.IsAny<string>(), It.IsAny<bool>())) .Returns(new[] { - string.Format(CultureInfo.InvariantCulture, TestDataImagePath, 0), - string.Format(CultureInfo.InvariantCulture, TestDataImagePath, 1) + string.Format(CultureInfo.InvariantCulture, _testDataImagePath, 0), + string.Format(CultureInfo.InvariantCulture, _testDataImagePath, 1) }); return new ItemImageProvider(new NullLogger<ItemImageProvider>(), providerManager, mockFileSystem.Object); } - private static BaseItem GetItemWithImages(ImageType type, int count, bool validPaths) + private static Video GetItemWithImages(ImageType type, int count, bool validPaths) { // Has to exist for querying DateModified time on file, results stored but not checked so not populating BaseItem.FileSystem ??= Mock.Of<IFileSystem>(); var item = new Video(); - var path = validPaths ? TestDataImagePath : "invalid path {0}"; + var path = validPaths ? _testDataImagePath.Format : "invalid path {0}"; for (int i = 0; i < count; i++) { item.SetImagePath(type, i, new FileSystemMetadata @@ -604,7 +604,7 @@ namespace Jellyfin.Providers.Tests.Manager /// </summary> private static LocalImageInfo[] GetImages(ImageType type, int count, bool validPaths) { - var path = validPaths ? TestDataImagePath : "invalid path {0}"; + var path = validPaths ? _testDataImagePath.Format : "invalid path {0}"; var images = new LocalImageInfo[count]; for (int i = 0; i < count; i++) { diff --git a/tests/Jellyfin.Server.Implementations.Tests/Sorting/AiredEpisodeOrderComparerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Sorting/AiredEpisodeOrderComparerTests.cs index 1dd49b2cfa..ad85bdb6e1 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Sorting/AiredEpisodeOrderComparerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Sorting/AiredEpisodeOrderComparerTests.cs @@ -9,22 +9,21 @@ namespace Jellyfin.Server.Implementations.Tests.Sorting { public class AiredEpisodeOrderComparerTests { + private readonly AiredEpisodeOrderComparer _cmp = new AiredEpisodeOrderComparer(); + [Theory] [ClassData(typeof(EpisodeBadData))] public void Compare_GivenNull_ThrowsArgumentNullException(BaseItem? x, BaseItem? y) { - var cmp = new AiredEpisodeOrderComparer(); - Assert.Throws<ArgumentNullException>(() => cmp.Compare(x, y)); + Assert.Throws<ArgumentNullException>(() => _cmp.Compare(x, y)); } [Theory] [ClassData(typeof(EpisodeTestData))] public void AiredEpisodeOrderCompareTest(BaseItem x, BaseItem y, int expected) { - var cmp = new AiredEpisodeOrderComparer(); - - Assert.Equal(expected, cmp.Compare(x, y)); - Assert.Equal(-expected, cmp.Compare(y, x)); + Assert.Equal(expected, _cmp.Compare(x, y)); + Assert.Equal(-expected, _cmp.Compare(y, x)); } private sealed class EpisodeBadData : TheoryData<BaseItem?, BaseItem?> diff --git a/tests/Jellyfin.Server.Implementations.Tests/Sorting/IndexNumberComparerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Sorting/IndexNumberComparerTests.cs index 18588bd67a..52f71ef4a3 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Sorting/IndexNumberComparerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Sorting/IndexNumberComparerTests.cs @@ -9,7 +9,7 @@ namespace Jellyfin.Server.Implementations.Tests.Sorting; public class IndexNumberComparerTests { - private readonly IBaseItemComparer _cmp = new IndexNumberComparer(); + private readonly IndexNumberComparer _cmp = new IndexNumberComparer(); public static TheoryData<BaseItem?, BaseItem?> Compare_GivenNull_ThrowsArgumentNullException_TestData() => new() diff --git a/tests/Jellyfin.Server.Implementations.Tests/Sorting/ParentIndexNumberComparerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Sorting/ParentIndexNumberComparerTests.cs index 261092e019..bedd187ebb 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Sorting/ParentIndexNumberComparerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Sorting/ParentIndexNumberComparerTests.cs @@ -9,7 +9,7 @@ namespace Jellyfin.Server.Implementations.Tests.Sorting; public class ParentIndexNumberComparerTests { - private readonly IBaseItemComparer _cmp = new ParentIndexNumberComparer(); + private readonly ParentIndexNumberComparer _cmp = new ParentIndexNumberComparer(); public static TheoryData<BaseItem?, BaseItem?> Compare_GivenNull_ThrowsArgumentNullException_TestData() => new() diff --git a/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs index 8d464d4e75..44d9ce13c0 100644 --- a/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs +++ b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs @@ -10,6 +10,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; +using IConfigurationManager = MediaBrowser.Common.Configuration.IConfigurationManager; namespace Jellyfin.Server.Tests { @@ -98,7 +99,7 @@ namespace Jellyfin.Server.Tests Assert.Equal(knownNetworks.Length, options.KnownNetworks.Count); foreach (var item in knownNetworks) { - Assert.NotNull(options.KnownNetworks.FirstOrDefault(x => x.BaseAddress.Equals(item.BaseAddress) && x.PrefixLength == item.PrefixLength)); + Assert.NotNull(options.KnownNetworks.FirstOrDefault(x => x.Prefix.Equals(item.BaseAddress) && x.PrefixLength == item.PrefixLength)); } } From 635d67d458e02df53a1b08998ccd3cff16e76ac3 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Tue, 14 Nov 2023 20:21:34 +0100 Subject: [PATCH 826/858] Revert "Use System.Net.IPNetwork" This reverts commit 117d05d288da1d412159a29c0cb8d5c8259e48ae. --- Emby.Dlna/Main/DlnaEntryPoint.cs | 363 ------------------ .../Controllers/EnvironmentController.cs | 2 +- .../Controllers/HlsSegmentController.cs | 2 +- Jellyfin.Api/Controllers/ImageController.cs | 2 +- .../Controllers/MusicGenresController.cs | 2 +- Jellyfin.Api/Extensions/DtoExtensions.cs | 22 +- Jellyfin.Api/Helpers/HlsHelpers.cs | 2 +- Jellyfin.Api/Helpers/TranscodingJobHelper.cs | 2 +- .../Models/StreamingDtos/StreamState.cs | 10 +- Jellyfin.Networking/Manager/NetworkManager.cs | 20 +- .../Users/UserManager.cs | 2 +- .../ApiServiceCollectionExtensions.cs | 4 +- Jellyfin.Server/Program.cs | 2 +- Jellyfin.Server/Startup.cs | 2 +- MediaBrowser.Common/Net/NetworkConstants.cs | 1 + MediaBrowser.Common/Net/NetworkUtils.cs | 7 +- .../Encoder/EncoderValidator.cs | 12 +- .../Encoder/EncodingUtils.cs | 2 +- .../Probing/ProbeResultNormalizer.cs | 6 +- .../Subtitles/SubtitleEditParser.cs | 2 +- .../Subtitles/SubtitleEncoder.cs | 2 +- MediaBrowser.Model/MediaBrowser.Model.csproj | 1 + MediaBrowser.Model/Net/IPData.cs | 5 +- .../ParseNetworkTests.cs | 3 +- 24 files changed, 60 insertions(+), 418 deletions(-) delete mode 100644 Emby.Dlna/Main/DlnaEntryPoint.cs diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs deleted file mode 100644 index dbc47d9816..0000000000 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ /dev/null @@ -1,363 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System; -using System.Globalization; -using System.Linq; -using System.Net.Http; -using System.Net.Sockets; -using System.Threading.Tasks; -using Emby.Dlna.PlayTo; -using Emby.Dlna.Ssdp; -using Jellyfin.Networking.Configuration; -using Jellyfin.Networking.Extensions; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Dlna; -using MediaBrowser.Controller.Drawing; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Plugins; -using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Globalization; -using Microsoft.Extensions.Logging; -using Rssdp; -using Rssdp.Infrastructure; - -namespace Emby.Dlna.Main -{ - public sealed class DlnaEntryPoint : IServerEntryPoint, IRunBeforeStartup - { - private readonly IServerConfigurationManager _config; - private readonly ILogger<DlnaEntryPoint> _logger; - private readonly IServerApplicationHost _appHost; - private readonly ISessionManager _sessionManager; - private readonly IHttpClientFactory _httpClientFactory; - private readonly ILibraryManager _libraryManager; - private readonly IUserManager _userManager; - private readonly IDlnaManager _dlnaManager; - private readonly IImageProcessor _imageProcessor; - private readonly IUserDataManager _userDataManager; - private readonly ILocalizationManager _localization; - private readonly IMediaSourceManager _mediaSourceManager; - private readonly IMediaEncoder _mediaEncoder; - private readonly IDeviceDiscovery _deviceDiscovery; - private readonly ISsdpCommunicationsServer _communicationsServer; - private readonly INetworkManager _networkManager; - private readonly object _syncLock = new(); - private readonly bool _disabled; - - private PlayToManager _manager; - private SsdpDevicePublisher _publisher; - - private bool _disposed; - - public DlnaEntryPoint( - IServerConfigurationManager config, - ILoggerFactory loggerFactory, - IServerApplicationHost appHost, - ISessionManager sessionManager, - IHttpClientFactory httpClientFactory, - ILibraryManager libraryManager, - IUserManager userManager, - IDlnaManager dlnaManager, - IImageProcessor imageProcessor, - IUserDataManager userDataManager, - ILocalizationManager localizationManager, - IMediaSourceManager mediaSourceManager, - IDeviceDiscovery deviceDiscovery, - IMediaEncoder mediaEncoder, - ISsdpCommunicationsServer communicationsServer, - INetworkManager networkManager) - { - _config = config; - _appHost = appHost; - _sessionManager = sessionManager; - _httpClientFactory = httpClientFactory; - _libraryManager = libraryManager; - _userManager = userManager; - _dlnaManager = dlnaManager; - _imageProcessor = imageProcessor; - _userDataManager = userDataManager; - _localization = localizationManager; - _mediaSourceManager = mediaSourceManager; - _deviceDiscovery = deviceDiscovery; - _mediaEncoder = mediaEncoder; - _communicationsServer = communicationsServer; - _networkManager = networkManager; - _logger = loggerFactory.CreateLogger<DlnaEntryPoint>(); - - var netConfig = config.GetConfiguration<NetworkConfiguration>(NetworkConfigurationStore.StoreKey); - _disabled = appHost.ListenWithHttps && netConfig.RequireHttps; - - if (_disabled && _config.GetDlnaConfiguration().EnableServer) - { - _logger.LogError("The DLNA specification does not support HTTPS."); - } - } - - public async Task RunAsync() - { - await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false); - - if (_disabled) - { - // No use starting as dlna won't work, as we're running purely on HTTPS. - return; - } - - ReloadComponents(); - - _config.NamedConfigurationUpdated += OnNamedConfigurationUpdated; - } - - private void OnNamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e) - { - if (string.Equals(e.Key, "dlna", StringComparison.OrdinalIgnoreCase)) - { - ReloadComponents(); - } - } - - private void ReloadComponents() - { - var options = _config.GetDlnaConfiguration(); - StartDeviceDiscovery(); - - if (options.EnableServer) - { - StartDevicePublisher(options); - } - else - { - DisposeDevicePublisher(); - } - - if (options.EnablePlayTo) - { - StartPlayToManager(); - } - else - { - DisposePlayToManager(); - } - } - - private void StartDeviceDiscovery() - { - try - { - ((DeviceDiscovery)_deviceDiscovery).Start(_communicationsServer); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error starting device discovery"); - } - } - - public void StartDevicePublisher(Configuration.DlnaOptions options) - { - if (_publisher is not null) - { - return; - } - - try - { - _publisher = new SsdpDevicePublisher( - _communicationsServer, - Environment.OSVersion.Platform.ToString(), - // Can not use VersionString here since that includes OS and version - Environment.OSVersion.Version.ToString(), - _config.GetDlnaConfiguration().SendOnlyMatchedHost) - { - LogFunction = (msg) => _logger.LogDebug("{Msg}", msg), - SupportPnpRootDevice = false - }; - - RegisterServerEndpoints(); - - if (options.BlastAliveMessages) - { - _publisher.StartSendingAliveNotifications(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds)); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error registering endpoint"); - } - } - - private void RegisterServerEndpoints() - { - var udn = CreateUuid(_appHost.SystemId); - var descriptorUri = "/dlna/" + udn + "/description.xml"; - - // Only get bind addresses in LAN - // IPv6 is currently unsupported - var validInterfaces = _networkManager.GetInternalBindAddresses() - .Where(x => x.Address is not null) - .Where(x => x.AddressFamily != AddressFamily.InterNetworkV6) - .ToList(); - - if (validInterfaces.Count == 0) - { - // No interfaces returned, fall back to loopback - validInterfaces = _networkManager.GetLoopbacks().ToList(); - } - - foreach (var intf in validInterfaces) - { - var fullService = "urn:schemas-upnp-org:device:MediaServer:1"; - - _logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, intf.Address); - - var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(intf.Address, false) + descriptorUri); - - var device = new SsdpRootDevice - { - CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info. - Location = uri.Uri, // Must point to the URL that serves your devices UPnP description document. - Address = intf.Address, - PrefixLength = NetworkExtensions.MaskToCidr(intf.Subnet.BaseAddress), - FriendlyName = "Jellyfin", - Manufacturer = "Jellyfin", - ModelName = "Jellyfin Server", - Uuid = udn - // This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc. - }; - - SetProperties(device, fullService); - _publisher.AddDevice(device); - - var embeddedDevices = new[] - { - "urn:schemas-upnp-org:service:ContentDirectory:1", - "urn:schemas-upnp-org:service:ConnectionManager:1", - // "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1" - }; - - foreach (var subDevice in embeddedDevices) - { - var embeddedDevice = new SsdpEmbeddedDevice - { - FriendlyName = device.FriendlyName, - Manufacturer = device.Manufacturer, - ModelName = device.ModelName, - Uuid = udn - // This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc. - }; - - SetProperties(embeddedDevice, subDevice); - device.AddDevice(embeddedDevice); - } - } - } - - private static string CreateUuid(string text) - { - if (!Guid.TryParse(text, out var guid)) - { - guid = text.GetMD5(); - } - - return guid.ToString("D", CultureInfo.InvariantCulture); - } - - private static void SetProperties(SsdpDevice device, string fullDeviceType) - { - var serviceParts = fullDeviceType - .Replace("urn:", string.Empty, StringComparison.OrdinalIgnoreCase) - .Replace(":1", string.Empty, StringComparison.OrdinalIgnoreCase) - .Split(':'); - - device.DeviceTypeNamespace = serviceParts[0].Replace('.', '-'); - device.DeviceClass = serviceParts[1]; - device.DeviceType = serviceParts[2]; - } - - private void StartPlayToManager() - { - lock (_syncLock) - { - if (_manager is not null) - { - return; - } - - try - { - _manager = new PlayToManager( - _logger, - _sessionManager, - _libraryManager, - _userManager, - _dlnaManager, - _appHost, - _imageProcessor, - _deviceDiscovery, - _httpClientFactory, - _userDataManager, - _localization, - _mediaSourceManager, - _mediaEncoder); - - _manager.Start(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error starting PlayTo manager"); - } - } - } - - private void DisposePlayToManager() - { - lock (_syncLock) - { - if (_manager is not null) - { - try - { - _logger.LogInformation("Disposing PlayToManager"); - _manager.Dispose(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error disposing PlayTo manager"); - } - - _manager = null; - } - } - } - - public void DisposeDevicePublisher() - { - if (_publisher is not null) - { - _logger.LogInformation("Disposing SsdpDevicePublisher"); - _publisher.Dispose(); - _publisher = null; - } - } - - /// <inheritdoc /> - public void Dispose() - { - if (_disposed) - { - return; - } - - DisposeDevicePublisher(); - DisposePlayToManager(); - _disposed = true; - } - } -} diff --git a/Jellyfin.Api/Controllers/EnvironmentController.cs b/Jellyfin.Api/Controllers/EnvironmentController.cs index e0713cf054..284a97621d 100644 --- a/Jellyfin.Api/Controllers/EnvironmentController.cs +++ b/Jellyfin.Api/Controllers/EnvironmentController.cs @@ -169,7 +169,7 @@ public class EnvironmentController : BaseJellyfinApiController // Check if unc share var index = path.LastIndexOf(UncSeparator); - if (index != -1 && path.IndexOf(UncSeparator, StringComparison.OrdinalIgnoreCase) == 0) + if (index != -1 && path[0] == UncSeparator) { parent = path.Substring(0, index); diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs index 6eedfd8c7f..392d9955fb 100644 --- a/Jellyfin.Api/Controllers/HlsSegmentController.cs +++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs @@ -160,7 +160,7 @@ public class HlsSegmentController : BaseJellyfinApiController var pathExtension = Path.GetExtension(path); if ((string.Equals(pathExtension, segmentContainer, StringComparison.OrdinalIgnoreCase) || string.Equals(pathExtension, ".m3u8", StringComparison.OrdinalIgnoreCase)) - && path.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1) + && path.Contains(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase)) { playlistPath = path; break; diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index 7d1a54eced..c031ce338d 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -80,7 +80,7 @@ public class ImageController : BaseJellyfinApiController _appPaths = appPaths; } - private static Stream GetFromBase64Stream(Stream inputStream) + private static CryptoStream GetFromBase64Stream(Stream inputStream) => new CryptoStream(inputStream, new FromBase64Transform(), CryptoStreamMode.Read); /// <summary> diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs index 94c8993575..69b9042646 100644 --- a/Jellyfin.Api/Controllers/MusicGenresController.cs +++ b/Jellyfin.Api/Controllers/MusicGenresController.cs @@ -150,7 +150,7 @@ public class MusicGenresController : BaseJellyfinApiController MusicGenre? item; - if (genreName.IndexOf(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase) != -1) + if (genreName.Contains(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase)) { item = GetItemFromSlugName<MusicGenre>(_libraryManager, genreName, dtoOptions, BaseItemKind.MusicGenre); } diff --git a/Jellyfin.Api/Extensions/DtoExtensions.cs b/Jellyfin.Api/Extensions/DtoExtensions.cs index 2d7a56d913..7d9823c25c 100644 --- a/Jellyfin.Api/Extensions/DtoExtensions.cs +++ b/Jellyfin.Api/Extensions/DtoExtensions.cs @@ -38,10 +38,10 @@ public static class DtoExtensions if (!dtoOptions.ContainsField(ItemFields.RecursiveItemCount)) { - if (client.IndexOf("kodi", StringComparison.OrdinalIgnoreCase) != -1 || - client.IndexOf("wmc", StringComparison.OrdinalIgnoreCase) != -1 || - client.IndexOf("media center", StringComparison.OrdinalIgnoreCase) != -1 || - client.IndexOf("classic", StringComparison.OrdinalIgnoreCase) != -1) + if (client.Contains("kodi", StringComparison.OrdinalIgnoreCase) || + client.Contains("wmc", StringComparison.OrdinalIgnoreCase) || + client.Contains("media center", StringComparison.OrdinalIgnoreCase) || + client.Contains("classic", StringComparison.OrdinalIgnoreCase)) { int oldLen = dtoOptions.Fields.Count; var arr = new ItemFields[oldLen + 1]; @@ -53,13 +53,13 @@ public static class DtoExtensions if (!dtoOptions.ContainsField(ItemFields.ChildCount)) { - if (client.IndexOf("kodi", StringComparison.OrdinalIgnoreCase) != -1 || - client.IndexOf("wmc", StringComparison.OrdinalIgnoreCase) != -1 || - client.IndexOf("media center", StringComparison.OrdinalIgnoreCase) != -1 || - client.IndexOf("classic", StringComparison.OrdinalIgnoreCase) != -1 || - client.IndexOf("roku", StringComparison.OrdinalIgnoreCase) != -1 || - client.IndexOf("samsung", StringComparison.OrdinalIgnoreCase) != -1 || - client.IndexOf("androidtv", StringComparison.OrdinalIgnoreCase) != -1) + if (client.Contains("kodi", StringComparison.OrdinalIgnoreCase) || + client.Contains("wmc", StringComparison.OrdinalIgnoreCase) || + client.Contains("media center", StringComparison.OrdinalIgnoreCase) || + client.Contains("classic", StringComparison.OrdinalIgnoreCase) || + client.Contains("roku", StringComparison.OrdinalIgnoreCase) || + client.Contains("samsung", StringComparison.OrdinalIgnoreCase) || + client.Contains("androidtv", StringComparison.OrdinalIgnoreCase)) { int oldLen = dtoOptions.Fields.Count; var arr = new ItemFields[oldLen + 1]; diff --git a/Jellyfin.Api/Helpers/HlsHelpers.cs b/Jellyfin.Api/Helpers/HlsHelpers.cs index 2155e305da..e2d3bfb193 100644 --- a/Jellyfin.Api/Helpers/HlsHelpers.cs +++ b/Jellyfin.Api/Helpers/HlsHelpers.cs @@ -53,7 +53,7 @@ public static class HlsHelpers break; } - if (line.IndexOf("#EXTINF:", StringComparison.OrdinalIgnoreCase) != -1) + if (line.Contains("#EXTINF:", StringComparison.OrdinalIgnoreCase)) { count++; if (count >= segmentCount) diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index 5b407e4a95..77d3edbd65 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -405,7 +405,7 @@ public class TranscodingJobHelper : IDisposable var name = Path.GetFileNameWithoutExtension(outputFilePath); var filesToDelete = _fileSystem.GetFilePaths(directory) - .Where(f => f.IndexOf(name, StringComparison.OrdinalIgnoreCase) != -1); + .Where(f => f.Contains(name, StringComparison.OrdinalIgnoreCase)); List<Exception>? exs = null; foreach (var file in filesToDelete) diff --git a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs index b75272d3f6..f249f3bc6c 100644 --- a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs +++ b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs @@ -86,11 +86,11 @@ public class StreamState : EncodingJobInfo, IDisposable { var userAgent = UserAgent ?? string.Empty; - if (userAgent.IndexOf("AppleTV", StringComparison.OrdinalIgnoreCase) != -1 - || userAgent.IndexOf("cfnetwork", StringComparison.OrdinalIgnoreCase) != -1 - || userAgent.IndexOf("ipad", StringComparison.OrdinalIgnoreCase) != -1 - || userAgent.IndexOf("iphone", StringComparison.OrdinalIgnoreCase) != -1 - || userAgent.IndexOf("ipod", StringComparison.OrdinalIgnoreCase) != -1) + if (userAgent.Contains("AppleTV", StringComparison.OrdinalIgnoreCase) + || userAgent.Contains("cfnetwork", StringComparison.OrdinalIgnoreCase) + || userAgent.Contains("ipad", StringComparison.OrdinalIgnoreCase) + || userAgent.Contains("iphone", StringComparison.OrdinalIgnoreCase) + || userAgent.Contains("ipod", StringComparison.OrdinalIgnoreCase)) { return 6; } diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index dbf7127a68..749e0abbbe 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -11,10 +11,12 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Model.Net; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.HttpOverrides; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; using IConfigurationManager = MediaBrowser.Common.Configuration.IConfigurationManager; +using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork; namespace Jellyfin.Networking.Manager { @@ -530,7 +532,7 @@ namespace Jellyfin.Networking.Manager { foreach (var lan in _lanSubnets) { - var lanPrefix = lan.BaseAddress; + var lanPrefix = lan.Prefix; publishedServerUrls.Add( new PublishedServerUriOverride( new IPData(lanPrefix, new IPNetwork(lanPrefix, lan.PrefixLength)), @@ -541,7 +543,7 @@ namespace Jellyfin.Networking.Manager } else if (NetworkUtils.TryParseToSubnet(identifier, out var result) && result is not null) { - var data = new IPData(result.Value.BaseAddress, result); + var data = new IPData(result.Prefix, result); publishedServerUrls.Add( new PublishedServerUriOverride( data, @@ -607,11 +609,11 @@ namespace Jellyfin.Networking.Manager var parts = details.Split(','); if (NetworkUtils.TryParseToSubnet(parts[0], out var subnet)) { - var address = subnet.Value.BaseAddress; + var address = subnet.Prefix; var index = int.Parse(parts[1], CultureInfo.InvariantCulture); if (address.AddressFamily == AddressFamily.InterNetwork || address.AddressFamily == AddressFamily.InterNetworkV6) { - var data = new IPData(address, subnet.Value, parts[2]) + var data = new IPData(address, subnet, parts[2]) { Index = index }; @@ -881,7 +883,7 @@ namespace Jellyfin.Networking.Manager { if (NetworkUtils.TryParseToSubnet(address, out var subnet)) { - return IPAddress.IsLoopback(subnet.Value.BaseAddress) || (_lanSubnets.Any(x => x.Contains(subnet.Value.BaseAddress)) && !_excludedSubnets.Any(x => x.Contains(subnet.Value.BaseAddress))); + return IPAddress.IsLoopback(subnet.Prefix) || (_lanSubnets.Any(x => x.Contains(subnet.Prefix)) && !_excludedSubnets.Any(x => x.Contains(subnet.Prefix))); } if (NetworkUtils.TryParseHost(address, out var addresses, IsIPv4Enabled, IsIPv6Enabled)) @@ -1112,12 +1114,12 @@ namespace Jellyfin.Networking.Manager var logLevel = debug ? LogLevel.Debug : LogLevel.Information; if (_logger.IsEnabled(logLevel)) { - _logger.Log(logLevel, "Defined LAN addresses: {0}", _lanSubnets.Select(s => s.BaseAddress + "/" + s.PrefixLength)); - _logger.Log(logLevel, "Defined LAN exclusions: {0}", _excludedSubnets.Select(s => s.BaseAddress + "/" + s.PrefixLength)); - _logger.Log(logLevel, "Using LAN addresses: {0}", _lanSubnets.Where(s => !_excludedSubnets.Contains(s)).Select(s => s.BaseAddress + "/" + s.PrefixLength)); + _logger.Log(logLevel, "Defined LAN addresses: {0}", _lanSubnets.Select(s => s.Prefix + "/" + s.PrefixLength)); + _logger.Log(logLevel, "Defined LAN exclusions: {0}", _excludedSubnets.Select(s => s.Prefix + "/" + s.PrefixLength)); + _logger.Log(logLevel, "Using LAN addresses: {0}", _lanSubnets.Where(s => !_excludedSubnets.Contains(s)).Select(s => s.Prefix + "/" + s.PrefixLength)); _logger.Log(logLevel, "Using bind addresses: {0}", _interfaces.OrderByDescending(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => x.Address)); _logger.Log(logLevel, "Remote IP filter is {0}", config.IsRemoteIPFilterBlacklist ? "Blocklist" : "Allowlist"); - _logger.Log(logLevel, "Filter list: {0}", _remoteAddressFilter.Select(s => s.BaseAddress + "/" + s.PrefixLength)); + _logger.Log(logLevel, "Filter list: {0}", _remoteAddressFilter.Select(s => s.Prefix + "/" + s.PrefixLength)); } } } diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 075f4cb3a1..990b9a5bd4 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -748,7 +748,7 @@ namespace Jellyfin.Server.Implementations.Users return GetPasswordResetProviders(user)[0]; } - private IList<IAuthenticationProvider> GetAuthenticationProviders(User? user) + private List<IAuthenticationProvider> GetAuthenticationProviders(User? user) { var authenticationProviderId = user?.AuthenticationProviderId; diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 92c483c0fe..46df173bfb 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -279,9 +279,9 @@ namespace Jellyfin.Server.Extensions } else if (NetworkUtils.TryParseToSubnet(allowedProxies[i], out var subnet)) { - if (subnet.HasValue) + if (subnet is not null) { - AddIPAddress(config, options, subnet.Value.BaseAddress, subnet.Value.PrefixLength); + AddIPAddress(config, options, subnet.Prefix, subnet.PrefixLength); } } else if (NetworkUtils.TryParseHost(allowedProxies[i], out var addresses, config.EnableIPv4, config.EnableIPv6)) diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index f9259d0d92..c70ef17197 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -40,7 +40,7 @@ namespace Jellyfin.Server /// </summary> public const string LoggingConfigFileSystem = "logging.json"; - private static readonly ILoggerFactory _loggerFactory = new SerilogLoggerFactory(); + private static readonly SerilogLoggerFactory _loggerFactory = new SerilogLoggerFactory(); private static long _startTimestamp; private static ILogger _logger = NullLogger.Instance; private static bool _restartOnShutdown; diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 18d13c0563..49f5bf232c 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -35,7 +35,7 @@ namespace Jellyfin.Server /// </summary> public class Startup { - private readonly IServerApplicationHost _serverApplicationHost; + private readonly CoreAppHost _serverApplicationHost; private readonly IServerConfigurationManager _serverConfigurationManager; /// <summary> diff --git a/MediaBrowser.Common/Net/NetworkConstants.cs b/MediaBrowser.Common/Net/NetworkConstants.cs index fe7ab89632..b18058fa96 100644 --- a/MediaBrowser.Common/Net/NetworkConstants.cs +++ b/MediaBrowser.Common/Net/NetworkConstants.cs @@ -1,4 +1,5 @@ using System.Net; +using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork; namespace MediaBrowser.Common.Net; diff --git a/MediaBrowser.Common/Net/NetworkUtils.cs b/MediaBrowser.Common/Net/NetworkUtils.cs index 7ae99336f1..4110b000e5 100644 --- a/MediaBrowser.Common/Net/NetworkUtils.cs +++ b/MediaBrowser.Common/Net/NetworkUtils.cs @@ -5,8 +5,7 @@ using System.Net; using System.Net.Sockets; using System.Text.RegularExpressions; using Jellyfin.Extensions; -using Jellyfin.Networking.Constants; -using Microsoft.AspNetCore.HttpOverrides; +using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork; namespace MediaBrowser.Common.Net; @@ -180,7 +179,7 @@ public static partial class NetworkUtils { if (TryParseToSubnet(values[a], out var innerResult, negated)) { - tmpResult.Add(innerResult.Value); + tmpResult.Add(innerResult); } } @@ -336,7 +335,7 @@ public static partial class NetworkUtils /// <returns>The broadcast address.</returns> public static IPAddress GetBroadcastAddress(IPNetwork network) { - var addressBytes = network.BaseAddress.GetAddressBytes(); + var addressBytes = network.Prefix.GetAddressBytes(); uint ipAddress = BitConverter.ToUInt32(addressBytes, 0); uint ipMaskV4 = BitConverter.ToUInt32(CidrToMask(network.PrefixLength, AddressFamily.InterNetwork).GetAddressBytes(), 0); uint broadCastIPAddress = ipAddress | ~ipMaskV4; diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index f12ef7e634..0d1d27ae8b 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -121,7 +121,7 @@ namespace MediaBrowser.MediaEncoding.Encoder "yadif_videotoolbox" }; - private static readonly IReadOnlyDictionary<int, string[]> _filterOptionsDict = new Dictionary<int, string[]> + private static readonly Dictionary<int, string[]> _filterOptionsDict = new Dictionary<int, string[]> { { 0, new string[] { "scale_cuda", "Output format (default \"same\")" } }, { 1, new string[] { "tonemap_cuda", "GPU accelerated HDR to SDR tonemapping" } }, @@ -132,7 +132,7 @@ namespace MediaBrowser.MediaEncoding.Encoder }; // These are the library versions that corresponds to our minimum ffmpeg version 4.x according to the version table below - private static readonly IReadOnlyDictionary<string, Version> _ffmpegMinimumLibraryVersions = new Dictionary<string, Version> + private static readonly Dictionary<string, Version> _ffmpegMinimumLibraryVersions = new Dictionary<string, Version> { { "libavutil", new Version(56, 14) }, { "libavcodec", new Version(58, 18) }, @@ -197,7 +197,7 @@ namespace MediaBrowser.MediaEncoding.Encoder internal bool ValidateVersionInternal(string versionOutput) { - if (versionOutput.IndexOf("Libav developers", StringComparison.OrdinalIgnoreCase) != -1) + if (versionOutput.Contains("Libav developers", StringComparison.OrdinalIgnoreCase)) { _logger.LogError("FFmpeg validation: avconv instead of ffmpeg is not supported"); return false; @@ -333,7 +333,7 @@ namespace MediaBrowser.MediaEncoding.Encoder /// </summary> /// <param name="output">The 'ffmpeg -version' output.</param> /// <returns>The library names and major.minor version numbers.</returns> - private static IReadOnlyDictionary<string, Version> GetFFmpegLibraryVersions(string output) + private static Dictionary<string, Version> GetFFmpegLibraryVersions(string output) { var map = new Dictionary<string, Version>(); @@ -537,9 +537,9 @@ namespace MediaBrowser.MediaEncoding.Encoder return found; } - private IDictionary<int, bool> GetFFmpegFiltersWithOption() + private Dictionary<int, bool> GetFFmpegFiltersWithOption() { - IDictionary<int, bool> dict = new Dictionary<int, bool>(); + Dictionary<int, bool> dict = new Dictionary<int, bool>(); for (int i = 0; i < _filterOptionsDict.Count; i++) { if (_filterOptionsDict.TryGetValue(i, out var val) && val.Length == 2) diff --git a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs index 04128c9119..c5f500e76f 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs @@ -59,7 +59,7 @@ namespace MediaBrowser.MediaEncoding.Encoder /// <returns>System.String.</returns> private static string GetFileInputArgument(string path, string inputPrefix) { - if (path.IndexOf("://", StringComparison.Ordinal) != -1) + if (path.Contains("://", StringComparison.Ordinal)) { return string.Format(CultureInfo.InvariantCulture, "\"{0}\"", path); } diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 020e69eced..629c300603 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -612,11 +612,11 @@ namespace MediaBrowser.MediaEncoding.Probing { codec = "dvbsub"; } - else if ((codec ?? string.Empty).IndexOf("PGS", StringComparison.OrdinalIgnoreCase) != -1) + else if ((codec ?? string.Empty).Contains("PGS", StringComparison.OrdinalIgnoreCase)) { codec = "PGSSUB"; } - else if ((codec ?? string.Empty).IndexOf("DVD", StringComparison.OrdinalIgnoreCase) != -1) + else if ((codec ?? string.Empty).Contains("DVD", StringComparison.OrdinalIgnoreCase)) { codec = "DVDSUB"; } @@ -1339,7 +1339,7 @@ namespace MediaBrowser.MediaEncoding.Probing { // Only use the comma as a delimiter if there are no slashes or pipes. // We want to be careful not to split names that have commas in them - var delimiter = !allowCommaDelimiter || _nameDelimiters.Any(i => val.IndexOf(i, StringComparison.Ordinal) != -1) ? + var delimiter = !allowCommaDelimiter || _nameDelimiters.Any(i => val.Contains(i, StringComparison.Ordinal)) ? _nameDelimiters : new[] { ',' }; diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs index 0d4489517e..fd55db4ba4 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs @@ -88,7 +88,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles public bool SupportsFileExtension(string fileExtension) => _subtitleFormats.ContainsKey(fileExtension); - private IEnumerable<SubtitleFormat> GetSubtitleFormats() + private List<SubtitleFormat> GetSubtitleFormats() { var subtitleFormats = new List<SubtitleFormat>(); var assembly = typeof(SubtitleFormat).Assembly; diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 8eea773d86..459d854bf1 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -63,7 +63,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles private string SubtitleCachePath => Path.Combine(_appPaths.DataPath, "subtitles"); - private Stream ConvertSubtitles( + private MemoryStream ConvertSubtitles( Stream stream, string inputFormat, string outputFormat, diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 1ca5e4327c..89ec156a9f 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -33,6 +33,7 @@ </PropertyGroup> <ItemGroup> + <PackageReference Include="Microsoft.AspNetCore.HttpOverrides" /> <PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" /> <PackageReference Include="MimeTypes"> diff --git a/MediaBrowser.Model/Net/IPData.cs b/MediaBrowser.Model/Net/IPData.cs index e016ffea10..c116d883ed 100644 --- a/MediaBrowser.Model/Net/IPData.cs +++ b/MediaBrowser.Model/Net/IPData.cs @@ -1,5 +1,6 @@ using System.Net; using System.Net.Sockets; +using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork; namespace MediaBrowser.Model.Net; @@ -65,9 +66,9 @@ public class IPData { if (Address.Equals(IPAddress.None)) { - return Subnet.BaseAddress.AddressFamily.Equals(IPAddress.None) + return Subnet.Prefix.AddressFamily.Equals(IPAddress.None) ? AddressFamily.Unspecified - : Subnet.BaseAddress.AddressFamily; + : Subnet.Prefix.AddressFamily; } else { diff --git a/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs index 44d9ce13c0..123266d298 100644 --- a/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs +++ b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; using IConfigurationManager = MediaBrowser.Common.Configuration.IConfigurationManager; +using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork; namespace Jellyfin.Server.Tests { @@ -99,7 +100,7 @@ namespace Jellyfin.Server.Tests Assert.Equal(knownNetworks.Length, options.KnownNetworks.Count); foreach (var item in knownNetworks) { - Assert.NotNull(options.KnownNetworks.FirstOrDefault(x => x.Prefix.Equals(item.BaseAddress) && x.PrefixLength == item.PrefixLength)); + Assert.NotNull(options.KnownNetworks.FirstOrDefault(x => x.Prefix.Equals(item.Prefix) && x.PrefixLength == item.PrefixLength)); } } From 8ee15258944deb83b880cb5263fae2843a7af248 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Tue, 14 Nov 2023 22:01:10 +0100 Subject: [PATCH 827/858] Fix runtime errors --- Emby.Server.Implementations/ApplicationHost.cs | 4 +++- src/Jellyfin.Extensions/Json/JsonDefaults.cs | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 9affe235db..4540ab205a 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -310,7 +310,9 @@ namespace Emby.Server.Implementations { _creatingInstances.Add(type); Logger.LogDebug("Creating instance of {Type}", type); - return ActivatorUtilities.CreateInstance(ServiceProvider, type); + return ServiceProvider is null + ? Activator.CreateInstance(type) + : ActivatorUtilities.CreateInstance(ServiceProvider, type); } catch (Exception ex) { diff --git a/src/Jellyfin.Extensions/Json/JsonDefaults.cs b/src/Jellyfin.Extensions/Json/JsonDefaults.cs index 4d56ca6151..9e6d4c3f87 100644 --- a/src/Jellyfin.Extensions/Json/JsonDefaults.cs +++ b/src/Jellyfin.Extensions/Json/JsonDefaults.cs @@ -1,5 +1,6 @@ using System.Text.Json; using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; using Jellyfin.Extensions.Json.Converters; namespace Jellyfin.Extensions.Json @@ -41,7 +42,8 @@ namespace Jellyfin.Extensions.Json new JsonNullableStructConverterFactory(), new JsonDateTimeConverter(), new JsonStringConverter() - } + }, + TypeInfoResolver = new DefaultJsonTypeInfoResolver() }; private static readonly JsonSerializerOptions _pascalCaseJsonSerializerOptions = new(_jsonSerializerOptions) From efec6e7a23fb0540559f7868640daf9624eecd7e Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Wed, 15 Nov 2023 15:22:51 +0100 Subject: [PATCH 828/858] Address review comment --- MediaBrowser.Model/Dlna/StreamBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 252ac1303f..bf18d46dcd 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -868,7 +868,7 @@ namespace MediaBrowser.Model.Dlna playlistItem.SetOption(audioStream.Codec, "profile", audioStream.Profile.ToLowerInvariant()); } - if (audioStream.Level.HasValue) + if (audioStream.Level.HasValue && audioStream.Level.Value != 0) { playlistItem.SetOption(audioStream.Codec, "level", audioStream.Level.Value.ToString(CultureInfo.InvariantCulture)); } From a1410ea899bcd6e84511e3304dd71cd33e53f156 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Wed, 15 Nov 2023 16:16:51 +0100 Subject: [PATCH 829/858] Update GitHub workflows --- .github/workflows/ci-codeql-analysis.yml | 2 +- .github/workflows/ci-openapi.yml | 4 ++-- .github/workflows/ci-tests.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index f43d743f04..c3823610e5 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -24,7 +24,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0 with: - dotnet-version: '7.0.x' + dotnet-version: '8.0.x' - name: Initialize CodeQL uses: github/codeql-action/init@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5 diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml index 96b790c0c7..62445a1ca3 100644 --- a/.github/workflows/ci-openapi.yml +++ b/.github/workflows/ci-openapi.yml @@ -21,7 +21,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0 with: - dotnet-version: '7.0.x' + dotnet-version: '8.0.x' - name: Generate openapi.json run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests" - name: Upload openapi.json @@ -55,7 +55,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0 with: - dotnet-version: '7.0.x' + dotnet-version: '8.0.x' - name: Generate openapi.json run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests" - name: Upload openapi.json diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 36686e64ba..a464609742 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -9,7 +9,7 @@ on: pull_request: env: - SDK_VERSION: "7.0.x" + SDK_VERSION: "8.0.x" jobs: run-tests: From 05b35dc41fa14290c2940ccd6de7ef63d303d7d2 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Wed, 15 Nov 2023 16:49:44 +0100 Subject: [PATCH 830/858] Update Azure pipelines --- .ci/azure-pipelines-abi.yml | 2 +- .ci/azure-pipelines-main.yml | 2 +- .ci/azure-pipelines-package.yml | 4 ++-- .ci/azure-pipelines-test.yml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.ci/azure-pipelines-abi.yml b/.ci/azure-pipelines-abi.yml index 4b82eedb45..547a514f86 100644 --- a/.ci/azure-pipelines-abi.yml +++ b/.ci/azure-pipelines-abi.yml @@ -7,7 +7,7 @@ parameters: default: "ubuntu-latest" - name: DotNetSdkVersion type: string - default: 7.0.x + default: 8.0.x jobs: - job: CompatibilityCheck diff --git a/.ci/azure-pipelines-main.yml b/.ci/azure-pipelines-main.yml index 020d7fff4a..0702aeb6b6 100644 --- a/.ci/azure-pipelines-main.yml +++ b/.ci/azure-pipelines-main.yml @@ -1,7 +1,7 @@ parameters: LinuxImage: 'ubuntu-latest' RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj' - DotNetSdkVersion: 7.0.x + DotNetSdkVersion: 8.0.x jobs: - job: Build diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml index c91a084e58..39f98e063e 100644 --- a/.ci/azure-pipelines-package.yml +++ b/.ci/azure-pipelines-package.yml @@ -208,10 +208,10 @@ jobs: steps: - task: UseDotNet@2 - displayName: 'Use .NET 7.0 sdk' + displayName: 'Use .NET 8.0 sdk' inputs: packageType: 'sdk' - version: '7.0.x' + version: '8.0.x' - task: DotNetCoreCLI@2 displayName: 'Build Stable Nuget packages' diff --git a/.ci/azure-pipelines-test.yml b/.ci/azure-pipelines-test.yml index f15d6a6cda..3549c691cb 100644 --- a/.ci/azure-pipelines-test.yml +++ b/.ci/azure-pipelines-test.yml @@ -10,7 +10,7 @@ parameters: default: "tests/**/*Tests.csproj" - name: DotNetSdkVersion type: string - default: 7.0.x + default: 8.0.x jobs: - job: Test From 5dad89cee331138dd2e4c24eecaea36aef2ee503 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Wed, 15 Nov 2023 22:06:09 +0100 Subject: [PATCH 831/858] Update Dockerfiles --- deployment/Dockerfile.centos.amd64 | 2 +- deployment/Dockerfile.fedora.amd64 | 2 +- deployment/Dockerfile.ubuntu.amd64 | 2 +- deployment/Dockerfile.ubuntu.arm64 | 2 +- deployment/Dockerfile.ubuntu.armhf | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64 index e5cf638c1d..7c9bbf39e9 100644 --- a/deployment/Dockerfile.centos.amd64 +++ b/deployment/Dockerfile.centos.amd64 @@ -13,7 +13,7 @@ RUN yum update -yq \ && yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget # Install DotNET SDK -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ff8c660f-ffa9-4814-ac2d-4089e6ec4eb5/dc806d344844f1d58d8015d105e85c65/dotnet-sdk-7.0.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/5226a5fa-8c0b-474f-b79a-8984ad7c5beb/3113ccbf789c9fd29972835f0f334b7a/dotnet-sdk-8.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.fedora.amd64 b/deployment/Dockerfile.fedora.amd64 index 777d92c116..66ead37d7d 100644 --- a/deployment/Dockerfile.fedora.amd64 +++ b/deployment/Dockerfile.fedora.amd64 @@ -12,7 +12,7 @@ RUN dnf update -yq \ && dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd wget make # Install DotNET SDK -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ff8c660f-ffa9-4814-ac2d-4089e6ec4eb5/dc806d344844f1d58d8015d105e85c65/dotnet-sdk-7.0.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/5226a5fa-8c0b-474f-b79a-8984ad7c5beb/3113ccbf789c9fd29972835f0f334b7a/dotnet-sdk-8.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64 index f34c0358d1..84fa2028e5 100644 --- a/deployment/Dockerfile.ubuntu.amd64 +++ b/deployment/Dockerfile.ubuntu.amd64 @@ -17,7 +17,7 @@ RUN apt-get update -yqq \ libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ff8c660f-ffa9-4814-ac2d-4089e6ec4eb5/dc806d344844f1d58d8015d105e85c65/dotnet-sdk-7.0.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/5226a5fa-8c0b-474f-b79a-8984ad7c5beb/3113ccbf789c9fd29972835f0f334b7a/dotnet-sdk-8.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.arm64 b/deployment/Dockerfile.ubuntu.arm64 index 87466d20ea..ca3aa35085 100644 --- a/deployment/Dockerfile.ubuntu.arm64 +++ b/deployment/Dockerfile.ubuntu.arm64 @@ -16,7 +16,7 @@ RUN apt-get update -yqq \ mmv build-essential lsb-release # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ff8c660f-ffa9-4814-ac2d-4089e6ec4eb5/dc806d344844f1d58d8015d105e85c65/dotnet-sdk-7.0.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/5226a5fa-8c0b-474f-b79a-8984ad7c5beb/3113ccbf789c9fd29972835f0f334b7a/dotnet-sdk-8.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.armhf b/deployment/Dockerfile.ubuntu.armhf index 261deb3e45..e52b7fba35 100644 --- a/deployment/Dockerfile.ubuntu.armhf +++ b/deployment/Dockerfile.ubuntu.armhf @@ -16,7 +16,7 @@ RUN apt-get update -yqq \ mmv build-essential lsb-release # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ff8c660f-ffa9-4814-ac2d-4089e6ec4eb5/dc806d344844f1d58d8015d105e85c65/dotnet-sdk-7.0.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/5226a5fa-8c0b-474f-b79a-8984ad7c5beb/3113ccbf789c9fd29972835f0f334b7a/dotnet-sdk-8.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet From 5fa1b8ac3a7e65f0972e4fc55e83172f66efd794 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Wed, 15 Nov 2023 22:21:20 +0100 Subject: [PATCH 832/858] Update more Dockerfiles --- Dockerfile | 2 +- Dockerfile.arm | 2 +- Dockerfile.arm64 | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9be319311e..d3f10cd12e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ ##################################### # Requires binfm_misc registration # https://github.com/multiarch/qemu-user-static#binfmt_misc-register -ARG DOTNET_VERSION=7.0 +ARG DOTNET_VERSION=8.0 FROM node:20-alpine as web-builder ARG JELLYFIN_WEB_VERSION=master diff --git a/Dockerfile.arm b/Dockerfile.arm index e8ec6398e6..db1acc838a 100644 --- a/Dockerfile.arm +++ b/Dockerfile.arm @@ -2,7 +2,7 @@ ##################################### # Requires binfm_misc registration # https://github.com/multiarch/qemu-user-static#binfmt_misc-register -ARG DOTNET_VERSION=7.0 +ARG DOTNET_VERSION=8.0 FROM node:20-alpine as web-builder diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 index 83137ee895..3eb5f45fc4 100644 --- a/Dockerfile.arm64 +++ b/Dockerfile.arm64 @@ -2,7 +2,7 @@ ##################################### # Requires binfm_misc registration # https://github.com/multiarch/qemu-user-static#binfmt_misc-register -ARG DOTNET_VERSION=7.0 +ARG DOTNET_VERSION=8.0 FROM node:20-alpine as web-builder From f3b882d0e2c5303db8c3b711cc0c5c18ac2d6b7f Mon Sep 17 00:00:00 2001 From: Uruk <uruknarb20@gmail.com> Date: Thu, 16 Nov 2023 00:43:37 +0100 Subject: [PATCH 833/858] Fix build and changed debian to bookworm --- deployment/Dockerfile.debian.amd64 | 2 +- deployment/Dockerfile.debian.arm64 | 2 +- deployment/Dockerfile.debian.armhf | 2 +- deployment/Dockerfile.docker.amd64 | 2 +- deployment/Dockerfile.docker.arm64 | 2 +- deployment/Dockerfile.docker.armhf | 2 +- deployment/Dockerfile.linux.amd64 | 2 +- deployment/Dockerfile.linux.amd64-musl | 2 +- deployment/Dockerfile.linux.arm64 | 2 +- deployment/Dockerfile.linux.armhf | 2 +- deployment/Dockerfile.linux.musl-linux-arm64 | 2 +- deployment/Dockerfile.macos.amd64 | 2 +- deployment/Dockerfile.macos.arm64 | 2 +- deployment/Dockerfile.portable | 2 +- deployment/Dockerfile.windows.amd64 | 2 +- deployment/build.debian.amd64 | 4 ++-- deployment/build.debian.arm64 | 4 ++-- deployment/build.debian.armhf | 4 ++-- deployment/build.ubuntu.amd64 | 4 ++-- deployment/build.ubuntu.arm64 | 4 ++-- deployment/build.ubuntu.armhf | 4 ++-- 21 files changed, 27 insertions(+), 27 deletions(-) diff --git a/deployment/Dockerfile.debian.amd64 b/deployment/Dockerfile.debian.amd64 index 1e1f6e54e3..d344c59646 100644 --- a/deployment/Dockerfile.debian.amd64 +++ b/deployment/Dockerfile.debian.amd64 @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim +FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist diff --git a/deployment/Dockerfile.debian.arm64 b/deployment/Dockerfile.debian.arm64 index bbed2c5340..8a5411f059 100644 --- a/deployment/Dockerfile.debian.arm64 +++ b/deployment/Dockerfile.debian.arm64 @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim +FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist diff --git a/deployment/Dockerfile.debian.armhf b/deployment/Dockerfile.debian.armhf index 79373519cd..e95ba16962 100644 --- a/deployment/Dockerfile.debian.armhf +++ b/deployment/Dockerfile.debian.armhf @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim +FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist diff --git a/deployment/Dockerfile.docker.amd64 b/deployment/Dockerfile.docker.amd64 index 3a6ad95e8e..1749ca563c 100644 --- a/deployment/Dockerfile.docker.amd64 +++ b/deployment/Dockerfile.docker.amd64 @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim +FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim ARG SOURCE_DIR=/src ARG ARTIFACT_DIR=/jellyfin diff --git a/deployment/Dockerfile.docker.arm64 b/deployment/Dockerfile.docker.arm64 index ca72393047..bbddb61e4d 100644 --- a/deployment/Dockerfile.docker.arm64 +++ b/deployment/Dockerfile.docker.arm64 @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim +FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim ARG SOURCE_DIR=/src ARG ARTIFACT_DIR=/jellyfin diff --git a/deployment/Dockerfile.docker.armhf b/deployment/Dockerfile.docker.armhf index 26cce19584..3de1d68878 100644 --- a/deployment/Dockerfile.docker.armhf +++ b/deployment/Dockerfile.docker.armhf @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim +FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim ARG SOURCE_DIR=/src ARG ARTIFACT_DIR=/jellyfin diff --git a/deployment/Dockerfile.linux.amd64 b/deployment/Dockerfile.linux.amd64 index 39169bd2ac..386f7cefe0 100644 --- a/deployment/Dockerfile.linux.amd64 +++ b/deployment/Dockerfile.linux.amd64 @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim +FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist diff --git a/deployment/Dockerfile.linux.amd64-musl b/deployment/Dockerfile.linux.amd64-musl index 636a34544b..56c8773332 100644 --- a/deployment/Dockerfile.linux.amd64-musl +++ b/deployment/Dockerfile.linux.amd64-musl @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim +FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist diff --git a/deployment/Dockerfile.linux.arm64 b/deployment/Dockerfile.linux.arm64 index ba8ce82f08..c9692c440a 100644 --- a/deployment/Dockerfile.linux.arm64 +++ b/deployment/Dockerfile.linux.arm64 @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim +FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist diff --git a/deployment/Dockerfile.linux.armhf b/deployment/Dockerfile.linux.armhf index d771e9991d..2304615560 100644 --- a/deployment/Dockerfile.linux.armhf +++ b/deployment/Dockerfile.linux.armhf @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim +FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist diff --git a/deployment/Dockerfile.linux.musl-linux-arm64 b/deployment/Dockerfile.linux.musl-linux-arm64 index 8465611817..240d091869 100644 --- a/deployment/Dockerfile.linux.musl-linux-arm64 +++ b/deployment/Dockerfile.linux.musl-linux-arm64 @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim +FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist diff --git a/deployment/Dockerfile.macos.amd64 b/deployment/Dockerfile.macos.amd64 index 7ebf354421..1b054dfc47 100644 --- a/deployment/Dockerfile.macos.amd64 +++ b/deployment/Dockerfile.macos.amd64 @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim +FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist diff --git a/deployment/Dockerfile.macos.arm64 b/deployment/Dockerfile.macos.arm64 index 5041ff967c..07e18da55a 100644 --- a/deployment/Dockerfile.macos.arm64 +++ b/deployment/Dockerfile.macos.arm64 @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim +FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist diff --git a/deployment/Dockerfile.portable b/deployment/Dockerfile.portable index 822b66ee69..36135f7a6e 100644 --- a/deployment/Dockerfile.portable +++ b/deployment/Dockerfile.portable @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim +FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist diff --git a/deployment/Dockerfile.windows.amd64 b/deployment/Dockerfile.windows.amd64 index 805c63f8ce..08587aa7eb 100644 --- a/deployment/Dockerfile.windows.amd64 +++ b/deployment/Dockerfile.windows.amd64 @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim +FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist diff --git a/deployment/build.debian.amd64 b/deployment/build.debian.amd64 index d92953ad19..7e968192bb 100755 --- a/deployment/build.debian.amd64 +++ b/deployment/build.debian.amd64 @@ -9,9 +9,9 @@ set -o xtrace pushd ${SOURCE_DIR} if [[ ${IS_DOCKER} == YES ]]; then - # Remove build-dep for dotnet-sdk-7.0, since it's installed manually + # Remove build-dep for dotnet-sdk-8.0, since it's installed manually cp -a debian/control /tmp/control.orig - sed -i '/dotnet-sdk-7.0,/d' debian/control + sed -i '/dotnet-sdk-8.0,/d' debian/control fi # Modify changelog to unstable configuration if IS_UNSTABLE diff --git a/deployment/build.debian.arm64 b/deployment/build.debian.arm64 index 618a121b65..7b7b603d63 100755 --- a/deployment/build.debian.arm64 +++ b/deployment/build.debian.arm64 @@ -9,9 +9,9 @@ set -o xtrace pushd ${SOURCE_DIR} if [[ ${IS_DOCKER} == YES ]]; then - # Remove build-dep for dotnet-sdk-7.0, since it's installed manually + # Remove build-dep for dotnet-sdk-8.0, since it's installed manually cp -a debian/control /tmp/control.orig - sed -i '/dotnet-sdk-7.0,/d' debian/control + sed -i '/dotnet-sdk-8.0,/d' debian/control fi # Modify changelog to unstable configuration if IS_UNSTABLE diff --git a/deployment/build.debian.armhf b/deployment/build.debian.armhf index d1631d022c..3d894ba20e 100755 --- a/deployment/build.debian.armhf +++ b/deployment/build.debian.armhf @@ -9,9 +9,9 @@ set -o xtrace pushd ${SOURCE_DIR} if [[ ${IS_DOCKER} == YES ]]; then - # Remove build-dep for dotnet-sdk-7.0, since it's installed manually + # Remove build-dep for dotnet-sdk-8.0, since it's installed manually cp -a debian/control /tmp/control.orig - sed -i '/dotnet-sdk-7.0,/d' debian/control + sed -i '/dotnet-sdk-8.0,/d' debian/control fi # Modify changelog to unstable configuration if IS_UNSTABLE diff --git a/deployment/build.ubuntu.amd64 b/deployment/build.ubuntu.amd64 index 4254103fa8..5f25cb610f 100755 --- a/deployment/build.ubuntu.amd64 +++ b/deployment/build.ubuntu.amd64 @@ -9,9 +9,9 @@ set -o xtrace pushd ${SOURCE_DIR} if [[ ${IS_DOCKER} == YES ]]; then - # Remove build-dep for dotnet-sdk-7.0, since it's installed manually + # Remove build-dep for dotnet-sdk-8.0, since it's installed manually cp -a debian/control /tmp/control.orig - sed -i '/dotnet-sdk-7.0,/d' debian/control + sed -i '/dotnet-sdk-8.0,/d' debian/control fi # Modify changelog to unstable configuration if IS_UNSTABLE diff --git a/deployment/build.ubuntu.arm64 b/deployment/build.ubuntu.arm64 index 42f111a010..334ced9970 100755 --- a/deployment/build.ubuntu.arm64 +++ b/deployment/build.ubuntu.arm64 @@ -9,9 +9,9 @@ set -o xtrace pushd ${SOURCE_DIR} if [[ ${IS_DOCKER} == YES ]]; then - # Remove build-dep for dotnet-sdk-7.0, since it's installed manually + # Remove build-dep for dotnet-sdk-8.0, since it's installed manually cp -a debian/control /tmp/control.orig - sed -i '/dotnet-sdk-7.0,/d' debian/control + sed -i '/dotnet-sdk-8.0,/d' debian/control fi # Modify changelog to unstable configuration if IS_UNSTABLE diff --git a/deployment/build.ubuntu.armhf b/deployment/build.ubuntu.armhf index 357d63626c..77e33c3071 100755 --- a/deployment/build.ubuntu.armhf +++ b/deployment/build.ubuntu.armhf @@ -9,9 +9,9 @@ set -o xtrace pushd ${SOURCE_DIR} if [[ ${IS_DOCKER} == YES ]]; then - # Remove build-dep for dotnet-sdk-7.0, since it's installed manually + # Remove build-dep for dotnet-sdk-8.0, since it's installed manually cp -a debian/control /tmp/control.orig - sed -i '/dotnet-sdk-7.0,/d' debian/control + sed -i '/dotnet-sdk-8.0,/d' debian/control fi # Modify changelog to unstable configuration if IS_UNSTABLE From 464de13acf3be8e466e12c8087bef8623e9edba9 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Thu, 16 Nov 2023 00:49:23 +0100 Subject: [PATCH 834/858] Use new static ZipFile functions --- Emby.Server.Implementations/Updates/InstallationManager.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index c717744b12..b31b4116db 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -551,8 +551,7 @@ namespace Emby.Server.Implementations.Updates } stream.Position = 0; - using var reader = new ZipArchive(stream); - reader.ExtractToDirectory(targetDir, true); + ZipFile.ExtractToDirectory(stream, targetDir, true); // Ensure we create one or populate existing ones with missing data. await _pluginManager.PopulateManifest(package.PackageInfo, package.Version, targetDir, status).ConfigureAwait(false); From 3c3f0a765e285c8e3a73f922307ce62f9fd5e6d7 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Thu, 16 Nov 2023 00:50:17 +0100 Subject: [PATCH 835/858] Use new IPNetwork.TryParse function --- MediaBrowser.Common/Net/NetworkUtils.cs | 51 +++++++------------ .../NetworkParseTests.cs | 5 -- 2 files changed, 17 insertions(+), 39 deletions(-) diff --git a/MediaBrowser.Common/Net/NetworkUtils.cs b/MediaBrowser.Common/Net/NetworkUtils.cs index 4110b000e5..e482089f0a 100644 --- a/MediaBrowser.Common/Net/NetworkUtils.cs +++ b/MediaBrowser.Common/Net/NetworkUtils.cs @@ -197,46 +197,29 @@ public static partial class NetworkUtils /// <returns><c>True</c> if parsing was successful.</returns> public static bool TryParseToSubnet(ReadOnlySpan<char> value, [NotNullWhen(true)] out IPNetwork? result, bool negated = false) { - var splitString = value.Trim().Split('/'); - if (splitString.MoveNext()) + value = value.Trim(); + if (value.Contains('/')) { - var ipBlock = splitString.Current; - var address = IPAddress.None; - if (negated && ipBlock.StartsWith("!") && IPAddress.TryParse(ipBlock[1..], out var tmpAddress)) + if (negated && value.StartsWith("!") && IPNetwork.TryParse(value[1..], out result)) { - address = tmpAddress; + return true; } - else if (!negated && IPAddress.TryParse(ipBlock, out tmpAddress)) + else if (!negated && IPNetwork.TryParse(value, out result)) { - address = tmpAddress; + return true; } - - if (address != IPAddress.None) + } + else if (IPAddress.TryParse(value, out var address)) + { + if (address.AddressFamily == AddressFamily.InterNetwork) { - if (splitString.MoveNext()) - { - var subnetBlock = splitString.Current; - if (int.TryParse(subnetBlock, out var netmask)) - { - result = new IPNetwork(address, netmask); - return true; - } - else if (IPAddress.TryParse(subnetBlock, out var netmaskAddress)) - { - result = new IPNetwork(address, NetworkUtils.MaskToCidr(netmaskAddress)); - return true; - } - } - else if (address.AddressFamily == AddressFamily.InterNetwork) - { - result = address.Equals(IPAddress.Any) ? NetworkConstants.IPv4Any : new IPNetwork(address, NetworkConstants.MinimumIPv4PrefixSize); - return true; - } - else if (address.AddressFamily == AddressFamily.InterNetworkV6) - { - result = address.Equals(IPAddress.IPv6Any) ? NetworkConstants.IPv6Any : new IPNetwork(address, NetworkConstants.MinimumIPv6PrefixSize); - return true; - } + result = address.Equals(IPAddress.Any) ? NetworkConstants.IPv4Any : new IPNetwork(address, NetworkConstants.MinimumIPv4PrefixSize); + return true; + } + else if (address.AddressFamily == AddressFamily.InterNetworkV6) + { + result = address.Equals(IPAddress.IPv6Any) ? NetworkConstants.IPv6Any : new IPNetwork(address, NetworkConstants.MinimumIPv6PrefixSize); + return true; } } diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs index 19aaa5ef8f..3b7c43100f 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs @@ -71,7 +71,6 @@ namespace Jellyfin.Networking.Tests [InlineData("127.0.0.1/8")] [InlineData("192.168.1.2")] [InlineData("192.168.1.2/24")] - [InlineData("192.168.1.2/255.255.255.0")] [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517")] [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517]")] [InlineData("fe80::7add:12ff:febb:c67b%16")] @@ -103,12 +102,10 @@ namespace Jellyfin.Networking.Tests [Theory] [InlineData("192.168.5.85/24", "192.168.5.1")] [InlineData("192.168.5.85/24", "192.168.5.254")] - [InlineData("192.168.5.85/255.255.255.0", "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("10.128.240.50/255.255.255.252", "10.128.240.51")] [InlineData("127.0.0.1/8", "127.0.0.1")] public void IPv4SubnetMaskMatchesValidIPAddress(string netMask, string ipAddress) { @@ -124,12 +121,10 @@ namespace Jellyfin.Networking.Tests [Theory] [InlineData("192.168.5.85/24", "192.168.4.254")] [InlineData("192.168.5.85/24", "191.168.5.254")] - [InlineData("192.168.5.85/255.255.255.252", "192.168.4.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")] - [InlineData("10.128.240.50/255.255.255.252", "10.127.240.51")] public void IPv4SubnetMaskDoesNotMatchInvalidIPAddress(string netMask, string ipAddress) { var ipa = IPAddress.Parse(ipAddress); From faa22cdb843fff5d50983d7cbca4ffec4932b188 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Thu, 16 Nov 2023 00:50:48 +0100 Subject: [PATCH 836/858] Update deps * Removes SourceLink as it should work automagically with .NET 8 --- Directory.Packages.props | 5 ++--- Emby.Naming/Emby.Naming.csproj | 4 ---- Jellyfin.Data/Jellyfin.Data.csproj | 4 ---- MediaBrowser.Common/MediaBrowser.Common.csproj | 1 - MediaBrowser.Controller/MediaBrowser.Controller.csproj | 1 - MediaBrowser.Model/MediaBrowser.Model.csproj | 1 - 6 files changed, 2 insertions(+), 14 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 0d7dbf12ce..b0765c0de3 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -48,7 +48,6 @@ <PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" /> <PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.0" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.8.0" /> - <PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" /> <PackageVersion Include="MimeTypes" Version="2.4.0" /> <PackageVersion Include="Mono.Nat" Version="3.0.4" /> <PackageVersion Include="Moq" Version="4.18.4" /> @@ -58,9 +57,9 @@ <PackageVersion Include="prometheus-net.AspNetCore" Version="8.1.0" /> <PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" /> <PackageVersion Include="prometheus-net" Version="8.1.0" /> - <PackageVersion Include="Serilog.AspNetCore" Version="7.0.0" /> + <PackageVersion Include="Serilog.AspNetCore" Version="8.0.0" /> <PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" /> - <PackageVersion Include="Serilog.Settings.Configuration" Version="7.0.1" /> + <PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.0" /> <PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" /> <PackageVersion Include="Serilog.Sinks.Console" Version="5.0.0" /> <PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" /> diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj index 47f2605501..97015efd0b 100644 --- a/Emby.Naming/Emby.Naming.csproj +++ b/Emby.Naming/Emby.Naming.csproj @@ -41,10 +41,6 @@ <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> </PropertyGroup> - <ItemGroup> - <PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" /> - </ItemGroup> - <!-- Code Analyzers --> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> <PackageReference Include="IDisposableAnalyzers"> diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj index c26e6cf239..75912abf0f 100644 --- a/Jellyfin.Data/Jellyfin.Data.csproj +++ b/Jellyfin.Data/Jellyfin.Data.csproj @@ -23,10 +23,6 @@ <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> </PropertyGroup> - <ItemGroup> - <PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" /> - </ItemGroup> - <!-- Code Analyzers --> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> <PackageReference Include="IDisposableAnalyzers"> diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index 9b3ea43683..8ad626b412 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -21,7 +21,6 @@ <ItemGroup> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" /> - <PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" /> </ItemGroup> <ItemGroup> diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 83faac3372..f237993fd3 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -20,7 +20,6 @@ <ItemGroup> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" /> <PackageReference Include="Microsoft.Extensions.Configuration.Binder" /> - <PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" /> <PackageReference Include="System.Threading.Tasks.Dataflow" /> </ItemGroup> diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 89ec156a9f..7af46f8a09 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -34,7 +34,6 @@ <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.HttpOverrides" /> - <PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" /> <PackageReference Include="MimeTypes"> <PrivateAssets>all</PrivateAssets> From 598beeb5d41a86ea47529d02da444ba340382865 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 15 Nov 2023 23:56:14 +0000 Subject: [PATCH 837/858] chore(deps): update dependency dotnet-ef to v8 --- .config/dotnet-tools.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 37aa7721e3..c03564f970 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "7.0.13", + "version": "8.0.0", "commands": [ "dotnet-ef" ] From 72ba002837729a78318df78180b1b8e63bc272e6 Mon Sep 17 00:00:00 2001 From: Uruk <uruknarb20@gmail.com> Date: Thu, 16 Nov 2023 01:12:25 +0100 Subject: [PATCH 838/858] Fix README.md, debian/control, jellyfin.spec ``` --- README.md | 2 +- debian/control | 2 +- fedora/jellyfin.spec | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 911d9a094b..15dd0ae679 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ These instructions will help you get set up with a local development environment ### Prerequisites -Before the project can be built, you must first install the [.NET 7.0 SDK](https://dotnet.microsoft.com/download/dotnet) on your system. +Before the project can be built, you must first install the [.NET 8.0 SDK](https://dotnet.microsoft.com/download/dotnet) on your system. Instructions to run this project from the command line are included here, but you will also need to install an IDE if you want to debug the server while it is running. Any IDE that supports .NET 6 development will work, but two options are recent versions of [Visual Studio](https://visualstudio.microsoft.com/downloads/) (at least 2022) and [Visual Studio Code](https://code.visualstudio.com/Download). diff --git a/debian/control b/debian/control index 0b9dd570e4..5e0460de9e 100644 --- a/debian/control +++ b/debian/control @@ -3,7 +3,7 @@ Section: misc Priority: optional Maintainer: Jellyfin Team <team@jellyfin.org> Build-Depends: debhelper (>= 9), - dotnet-sdk-7.0, + dotnet-sdk-8.0, libc6-dev, libcurl4-openssl-dev, libfontconfig1-dev, diff --git a/fedora/jellyfin.spec b/fedora/jellyfin.spec index c56a189ce9..fb9fb2f7da 100644 --- a/fedora/jellyfin.spec +++ b/fedora/jellyfin.spec @@ -26,7 +26,7 @@ BuildRequires: systemd BuildRequires: libcurl-devel, fontconfig-devel, freetype-devel, openssl-devel, glibc-devel, libicu-devel # Requirements not packaged in RHEL 7 main repos, added via Makefile # https://packages.microsoft.com/rhel/7/prod/ -BuildRequires: dotnet-runtime-7.0, dotnet-sdk-7.0 +BuildRequires: dotnet-runtime-8.0, dotnet-sdk-8.0 Requires: %{name}-server = %{version}-%{release}, %{name}-web = %{version}-%{release} # Temporary (hopefully?) fix for https://github.com/jellyfin/jellyfin/issues/7471 From 8eb2fa53b5ca0e0bc03b70029e3669e36d54d10c Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Wed, 15 Nov 2023 16:23:51 -0500 Subject: [PATCH 839/858] Use pattern matching for EnableRefreshMessage --- .../EntryPoints/LibraryChangedNotifier.cs | 30 ++----------------- 1 file changed, 2 insertions(+), 28 deletions(-) diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs index be36bbd2c1..6654f48672 100644 --- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs @@ -147,34 +147,8 @@ namespace Emby.Server.Implementations.EntryPoints } private static bool EnableRefreshMessage(BaseItem item) - { - if (item is not Folder folder) - { - return false; - } - - if (folder.IsRoot) - { - return false; - } - - if (folder is AggregateFolder || folder is UserRootFolder) - { - return false; - } - - if (folder is UserView || folder is Channel) - { - return false; - } - - if (!folder.IsTopParent) - { - return false; - } - - return true; - } + => item is Folder { IsRoot: false, IsTopParent: true } + and not (AggregateFolder or UserRootFolder or UserView or Channel); /// <summary> /// Handles the ItemAdded event of the libraryManager control. From 98f8cb2ad04d23edb9a6dfdf5d0b6e577d7ebb85 Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Wed, 15 Nov 2023 16:28:35 -0500 Subject: [PATCH 840/858] Use target-typed new for fields --- .../EntryPoints/LibraryChangedNotifier.cs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs index 6654f48672..7a8686ec84 100644 --- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs @@ -34,17 +34,13 @@ namespace Emby.Server.Implementations.EntryPoints private readonly IUserManager _userManager; private readonly ILogger<LibraryChangedNotifier> _logger; - /// <summary> - /// The library changed sync lock. - /// </summary> - private readonly object _libraryChangedSyncLock = new object(); - - private readonly List<Folder> _foldersAddedTo = new List<Folder>(); - private readonly List<Folder> _foldersRemovedFrom = new List<Folder>(); - private readonly List<BaseItem> _itemsAdded = new List<BaseItem>(); - private readonly List<BaseItem> _itemsRemoved = new List<BaseItem>(); - private readonly List<BaseItem> _itemsUpdated = new List<BaseItem>(); - private readonly ConcurrentDictionary<Guid, DateTime> _lastProgressMessageTimes = new ConcurrentDictionary<Guid, DateTime>(); + private readonly object _libraryChangedSyncLock = new(); + private readonly List<Folder> _foldersAddedTo = new(); + private readonly List<Folder> _foldersRemovedFrom = new(); + private readonly List<BaseItem> _itemsAdded = new(); + private readonly List<BaseItem> _itemsRemoved = new(); + private readonly List<BaseItem> _itemsUpdated = new(); + private readonly ConcurrentDictionary<Guid, DateTime> _lastProgressMessageTimes = new(); public LibraryChangedNotifier( ILibraryManager libraryManager, From eb4d8e13dfa10123c84b1676e9f20983b6abcfee Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Wed, 15 Nov 2023 16:35:01 -0500 Subject: [PATCH 841/858] Break up long lines --- .../EntryPoints/LibraryChangedNotifier.cs | 58 ++++++++++++++----- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs index 7a8686ec84..5a2f8a6dd4 100644 --- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs @@ -296,7 +296,13 @@ namespace Emby.Server.Implementations.EntryPoints /// <param name="foldersAddedTo">The folders added to.</param> /// <param name="foldersRemovedFrom">The folders removed from.</param> /// <param name="cancellationToken">The cancellation token.</param> - private async Task SendChangeNotifications(List<BaseItem> itemsAdded, List<BaseItem> itemsUpdated, List<BaseItem> itemsRemoved, List<Folder> foldersAddedTo, List<Folder> foldersRemovedFrom, CancellationToken cancellationToken) + private async Task SendChangeNotifications( + List<BaseItem> itemsAdded, + List<BaseItem> itemsUpdated, + List<BaseItem> itemsRemoved, + List<Folder> foldersAddedTo, + List<Folder> foldersRemovedFrom, + CancellationToken cancellationToken) { var userIds = _sessionManager.Sessions .Select(i => i.UserId) @@ -325,7 +331,12 @@ namespace Emby.Server.Implementations.EntryPoints try { - await _sessionManager.SendMessageToUserSessions(new List<Guid> { userId }, SessionMessageType.LibraryChanged, info, cancellationToken).ConfigureAwait(false); + await _sessionManager.SendMessageToUserSessions( + new List<Guid> { userId }, + SessionMessageType.LibraryChanged, + info, + cancellationToken) + .ConfigureAwait(false); } catch (Exception ex) { @@ -344,7 +355,13 @@ namespace Emby.Server.Implementations.EntryPoints /// <param name="foldersRemovedFrom">The folders removed from.</param> /// <param name="userId">The user id.</param> /// <returns>LibraryUpdateInfo.</returns> - private LibraryUpdateInfo GetLibraryUpdateInfo(List<BaseItem> itemsAdded, List<BaseItem> itemsUpdated, List<BaseItem> itemsRemoved, List<Folder> foldersAddedTo, List<Folder> foldersRemovedFrom, Guid userId) + private LibraryUpdateInfo GetLibraryUpdateInfo( + List<BaseItem> itemsAdded, + List<BaseItem> itemsUpdated, + List<BaseItem> itemsRemoved, + List<Folder> foldersAddedTo, + List<Folder> foldersRemovedFrom, + Guid userId) { var user = _userManager.GetUserById(userId); @@ -352,20 +369,33 @@ namespace Emby.Server.Implementations.EntryPoints newAndRemoved.AddRange(foldersAddedTo); newAndRemoved.AddRange(foldersRemovedFrom); - var allUserRootChildren = _libraryManager.GetUserRootFolder().GetChildren(user, true).OfType<Folder>().ToList(); + var allUserRootChildren = _libraryManager.GetUserRootFolder() + .GetChildren(user, true) + .OfType<Folder>() + .ToList(); return new LibraryUpdateInfo { - ItemsAdded = itemsAdded.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)).Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)).Distinct().ToArray(), - - ItemsUpdated = itemsUpdated.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)).Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)).Distinct().ToArray(), - - ItemsRemoved = itemsRemoved.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user, true)).Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)).Distinct().ToArray(), - - FoldersAddedTo = foldersAddedTo.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)).Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)).Distinct().ToArray(), - - FoldersRemovedFrom = foldersRemovedFrom.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)).Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)).Distinct().ToArray(), - + ItemsAdded = itemsAdded.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)) + .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)) + .Distinct() + .ToArray(), + ItemsUpdated = itemsUpdated.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)) + .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)) + .Distinct() + .ToArray(), + ItemsRemoved = itemsRemoved.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user, true)) + .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)) + .Distinct() + .ToArray(), + FoldersAddedTo = foldersAddedTo.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)) + .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)) + .Distinct() + .ToArray(), + FoldersRemovedFrom = foldersRemovedFrom.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)) + .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)) + .Distinct() + .ToArray(), CollectionFolders = GetTopParentIds(newAndRemoved, allUserRootChildren).ToArray() }; } From 8f5f0a0310ca7dd55b64623782c05bbb810f946f Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Wed, 15 Nov 2023 16:49:25 -0500 Subject: [PATCH 842/858] Combine library item event handlers --- .../EntryPoints/LibraryChangedNotifier.cs | 83 +++---------------- 1 file changed, 13 insertions(+), 70 deletions(-) diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs index 5a2f8a6dd4..4d5065b927 100644 --- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs @@ -146,98 +146,41 @@ namespace Emby.Server.Implementations.EntryPoints => item is Folder { IsRoot: false, IsTopParent: true } and not (AggregateFolder or UserRootFolder or UserView or Channel); - /// <summary> - /// Handles the ItemAdded event of the libraryManager control. - /// </summary> - /// <param name="sender">The source of the event.</param> - /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param> private void OnLibraryItemAdded(object sender, ItemChangeEventArgs e) - { - if (!FilterItem(e.Item)) - { - return; - } + => OnLibraryChange(e.Item, e.Parent, _itemsAdded, _foldersAddedTo); - lock (_libraryChangedSyncLock) - { - if (LibraryUpdateTimer is null) - { - LibraryUpdateTimer = new Timer( - LibraryUpdateTimerCallback, - null, - TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), - Timeout.InfiniteTimeSpan); - } - else - { - LibraryUpdateTimer.Change(TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan); - } - - if (e.Item.GetParent() is Folder parent) - { - _foldersAddedTo.Add(parent); - } - - _itemsAdded.Add(e.Item); - } - } - - /// <summary> - /// Handles the ItemUpdated event of the libraryManager control. - /// </summary> - /// <param name="sender">The source of the event.</param> - /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param> private void OnLibraryItemUpdated(object sender, ItemChangeEventArgs e) - { - if (!FilterItem(e.Item)) - { - return; - } + => OnLibraryChange(e.Item, e.Parent, _itemsUpdated, null); - lock (_libraryChangedSyncLock) - { - if (LibraryUpdateTimer is null) - { - LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan); - } - else - { - LibraryUpdateTimer.Change(TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan); - } - - _itemsUpdated.Add(e.Item); - } - } - - /// <summary> - /// Handles the ItemRemoved event of the libraryManager control. - /// </summary> - /// <param name="sender">The source of the event.</param> - /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param> private void OnLibraryItemRemoved(object sender, ItemChangeEventArgs e) + => OnLibraryChange(e.Item, e.Parent, _itemsRemoved, _foldersRemovedFrom); + + private void OnLibraryChange(BaseItem item, BaseItem parent, List<BaseItem> itemsList, List<Folder> foldersList) { - if (!FilterItem(e.Item)) + if (!FilterItem(item)) { return; } lock (_libraryChangedSyncLock) { + var updateDuration = TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration); + if (LibraryUpdateTimer is null) { - LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan); + LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, updateDuration, Timeout.InfiniteTimeSpan); } else { - LibraryUpdateTimer.Change(TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan); + LibraryUpdateTimer.Change(updateDuration, Timeout.InfiniteTimeSpan); } - if (e.Parent is Folder parent) + if (foldersList is not null && parent is Folder folder) { - _foldersRemovedFrom.Add(parent); + foldersList.Add(folder); } - _itemsRemoved.Add(e.Item); + itemsList.Add(item); } } From 7e645dcfc0809ed2bfa9d31235839bf649bc76d2 Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Wed, 15 Nov 2023 19:48:50 -0500 Subject: [PATCH 843/858] Make ILibraryChangedNotifier sealed --- .../EntryPoints/LibraryChangedNotifier.cs | 39 ++++++------------- 1 file changed, 12 insertions(+), 27 deletions(-) diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs index 4d5065b927..0df8c2a5a5 100644 --- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs @@ -25,7 +25,7 @@ using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.EntryPoints { - public class LibraryChangedNotifier : IServerEntryPoint + public sealed class LibraryChangedNotifier : IServerEntryPoint { private readonly ILibraryManager _libraryManager; private readonly IServerConfigurationManager _configurationManager; @@ -405,36 +405,21 @@ namespace Emby.Server.Implementations.EntryPoints return Array.Empty<T>(); } - /// <summary> - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// </summary> + /// <inheritdoc /> public void Dispose() { - Dispose(true); - GC.SuppressFinalize(this); - } + _libraryManager.ItemAdded -= OnLibraryItemAdded; + _libraryManager.ItemUpdated -= OnLibraryItemUpdated; + _libraryManager.ItemRemoved -= OnLibraryItemRemoved; - /// <summary> - /// Releases unmanaged and - optionally - managed resources. - /// </summary> - /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> - protected virtual void Dispose(bool dispose) - { - if (dispose) + _providerManager.RefreshCompleted -= OnProviderRefreshCompleted; + _providerManager.RefreshStarted -= OnProviderRefreshStarted; + _providerManager.RefreshProgress -= OnProviderRefreshProgress; + + if (LibraryUpdateTimer is not null) { - if (LibraryUpdateTimer is not null) - { - LibraryUpdateTimer.Dispose(); - LibraryUpdateTimer = null; - } - - _libraryManager.ItemAdded -= OnLibraryItemAdded; - _libraryManager.ItemUpdated -= OnLibraryItemUpdated; - _libraryManager.ItemRemoved -= OnLibraryItemRemoved; - - _providerManager.RefreshCompleted -= OnProviderRefreshCompleted; - _providerManager.RefreshStarted -= OnProviderRefreshStarted; - _providerManager.RefreshProgress -= OnProviderRefreshProgress; + LibraryUpdateTimer.Dispose(); + LibraryUpdateTimer = null; } } } From c38bfd281c9616a626eceb22ad5f5e2a4a120b86 Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Wed, 15 Nov 2023 19:49:15 -0500 Subject: [PATCH 844/858] Use file-scoped namespace --- .../EntryPoints/LibraryChangedNotifier.cs | 705 +++++++++--------- 1 file changed, 352 insertions(+), 353 deletions(-) diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs index 0df8c2a5a5..40dc00e1af 100644 --- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs @@ -23,404 +23,403 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Session; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.EntryPoints +namespace Emby.Server.Implementations.EntryPoints; + +public sealed class LibraryChangedNotifier : IServerEntryPoint { - public sealed class LibraryChangedNotifier : IServerEntryPoint + private readonly ILibraryManager _libraryManager; + private readonly IServerConfigurationManager _configurationManager; + private readonly IProviderManager _providerManager; + private readonly ISessionManager _sessionManager; + private readonly IUserManager _userManager; + private readonly ILogger<LibraryChangedNotifier> _logger; + + private readonly object _libraryChangedSyncLock = new(); + private readonly List<Folder> _foldersAddedTo = new(); + private readonly List<Folder> _foldersRemovedFrom = new(); + private readonly List<BaseItem> _itemsAdded = new(); + private readonly List<BaseItem> _itemsRemoved = new(); + private readonly List<BaseItem> _itemsUpdated = new(); + private readonly ConcurrentDictionary<Guid, DateTime> _lastProgressMessageTimes = new(); + + public LibraryChangedNotifier( + ILibraryManager libraryManager, + IServerConfigurationManager configurationManager, + ISessionManager sessionManager, + IUserManager userManager, + ILogger<LibraryChangedNotifier> logger, + IProviderManager providerManager) { - private readonly ILibraryManager _libraryManager; - private readonly IServerConfigurationManager _configurationManager; - private readonly IProviderManager _providerManager; - private readonly ISessionManager _sessionManager; - private readonly IUserManager _userManager; - private readonly ILogger<LibraryChangedNotifier> _logger; + _libraryManager = libraryManager; + _configurationManager = configurationManager; + _sessionManager = sessionManager; + _userManager = userManager; + _logger = logger; + _providerManager = providerManager; + } - private readonly object _libraryChangedSyncLock = new(); - private readonly List<Folder> _foldersAddedTo = new(); - private readonly List<Folder> _foldersRemovedFrom = new(); - private readonly List<BaseItem> _itemsAdded = new(); - private readonly List<BaseItem> _itemsRemoved = new(); - private readonly List<BaseItem> _itemsUpdated = new(); - private readonly ConcurrentDictionary<Guid, DateTime> _lastProgressMessageTimes = new(); + /// <summary> + /// Gets or sets the library update timer. + /// </summary> + /// <value>The library update timer.</value> + private Timer LibraryUpdateTimer { get; set; } - public LibraryChangedNotifier( - ILibraryManager libraryManager, - IServerConfigurationManager configurationManager, - ISessionManager sessionManager, - IUserManager userManager, - ILogger<LibraryChangedNotifier> logger, - IProviderManager providerManager) + public Task RunAsync() + { + _libraryManager.ItemAdded += OnLibraryItemAdded; + _libraryManager.ItemUpdated += OnLibraryItemUpdated; + _libraryManager.ItemRemoved += OnLibraryItemRemoved; + + _providerManager.RefreshCompleted += OnProviderRefreshCompleted; + _providerManager.RefreshStarted += OnProviderRefreshStarted; + _providerManager.RefreshProgress += OnProviderRefreshProgress; + + return Task.CompletedTask; + } + + private void OnProviderRefreshProgress(object sender, GenericEventArgs<Tuple<BaseItem, double>> e) + { + var item = e.Argument.Item1; + + if (!EnableRefreshMessage(item)) { - _libraryManager = libraryManager; - _configurationManager = configurationManager; - _sessionManager = sessionManager; - _userManager = userManager; - _logger = logger; - _providerManager = providerManager; + return; } - /// <summary> - /// Gets or sets the library update timer. - /// </summary> - /// <value>The library update timer.</value> - private Timer LibraryUpdateTimer { get; set; } + var progress = e.Argument.Item2; - public Task RunAsync() + if (_lastProgressMessageTimes.TryGetValue(item.Id, out var lastMessageSendTime)) { - _libraryManager.ItemAdded += OnLibraryItemAdded; - _libraryManager.ItemUpdated += OnLibraryItemUpdated; - _libraryManager.ItemRemoved += OnLibraryItemRemoved; - - _providerManager.RefreshCompleted += OnProviderRefreshCompleted; - _providerManager.RefreshStarted += OnProviderRefreshStarted; - _providerManager.RefreshProgress += OnProviderRefreshProgress; - - return Task.CompletedTask; - } - - private void OnProviderRefreshProgress(object sender, GenericEventArgs<Tuple<BaseItem, double>> e) - { - var item = e.Argument.Item1; - - if (!EnableRefreshMessage(item)) + if (progress > 0 && progress < 100 && (DateTime.UtcNow - lastMessageSendTime).TotalMilliseconds < 1000) { return; } + } - var progress = e.Argument.Item2; + _lastProgressMessageTimes.AddOrUpdate(item.Id, _ => DateTime.UtcNow, (_, _) => DateTime.UtcNow); - if (_lastProgressMessageTimes.TryGetValue(item.Id, out var lastMessageSendTime)) + var dict = new Dictionary<string, string>(); + dict["ItemId"] = item.Id.ToString("N", CultureInfo.InvariantCulture); + dict["Progress"] = progress.ToString(CultureInfo.InvariantCulture); + + try + { + _sessionManager.SendMessageToAdminSessions(SessionMessageType.RefreshProgress, dict, CancellationToken.None); + } + catch + { + } + + var collectionFolders = _libraryManager.GetCollectionFolders(item); + + foreach (var collectionFolder in collectionFolders) + { + var collectionFolderDict = new Dictionary<string, string> { - if (progress > 0 && progress < 100 && (DateTime.UtcNow - lastMessageSendTime).TotalMilliseconds < 1000) - { - return; - } - } - - _lastProgressMessageTimes.AddOrUpdate(item.Id, _ => DateTime.UtcNow, (_, _) => DateTime.UtcNow); - - var dict = new Dictionary<string, string>(); - dict["ItemId"] = item.Id.ToString("N", CultureInfo.InvariantCulture); - dict["Progress"] = progress.ToString(CultureInfo.InvariantCulture); + ["ItemId"] = collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture), + ["Progress"] = (collectionFolder.GetRefreshProgress() ?? 0).ToString(CultureInfo.InvariantCulture) + }; try { - _sessionManager.SendMessageToAdminSessions(SessionMessageType.RefreshProgress, dict, CancellationToken.None); + _sessionManager.SendMessageToAdminSessions(SessionMessageType.RefreshProgress, collectionFolderDict, CancellationToken.None); } catch { } + } + } - var collectionFolders = _libraryManager.GetCollectionFolders(item); + private void OnProviderRefreshStarted(object sender, GenericEventArgs<BaseItem> e) + { + OnProviderRefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 0))); + } - foreach (var collectionFolder in collectionFolders) + private void OnProviderRefreshCompleted(object sender, GenericEventArgs<BaseItem> e) + { + OnProviderRefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 100))); + + _lastProgressMessageTimes.TryRemove(e.Argument.Id, out _); + } + + private static bool EnableRefreshMessage(BaseItem item) + => item is Folder { IsRoot: false, IsTopParent: true } + and not (AggregateFolder or UserRootFolder or UserView or Channel); + + private void OnLibraryItemAdded(object sender, ItemChangeEventArgs e) + => OnLibraryChange(e.Item, e.Parent, _itemsAdded, _foldersAddedTo); + + private void OnLibraryItemUpdated(object sender, ItemChangeEventArgs e) + => OnLibraryChange(e.Item, e.Parent, _itemsUpdated, null); + + private void OnLibraryItemRemoved(object sender, ItemChangeEventArgs e) + => OnLibraryChange(e.Item, e.Parent, _itemsRemoved, _foldersRemovedFrom); + + private void OnLibraryChange(BaseItem item, BaseItem parent, List<BaseItem> itemsList, List<Folder> foldersList) + { + if (!FilterItem(item)) + { + return; + } + + lock (_libraryChangedSyncLock) + { + var updateDuration = TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration); + + if (LibraryUpdateTimer is null) { - var collectionFolderDict = new Dictionary<string, string> - { - ["ItemId"] = collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture), - ["Progress"] = (collectionFolder.GetRefreshProgress() ?? 0).ToString(CultureInfo.InvariantCulture) - }; - - try - { - _sessionManager.SendMessageToAdminSessions(SessionMessageType.RefreshProgress, collectionFolderDict, CancellationToken.None); - } - catch - { - } + LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, updateDuration, Timeout.InfiniteTimeSpan); } - } - - private void OnProviderRefreshStarted(object sender, GenericEventArgs<BaseItem> e) - { - OnProviderRefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 0))); - } - - private void OnProviderRefreshCompleted(object sender, GenericEventArgs<BaseItem> e) - { - OnProviderRefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 100))); - - _lastProgressMessageTimes.TryRemove(e.Argument.Id, out _); - } - - private static bool EnableRefreshMessage(BaseItem item) - => item is Folder { IsRoot: false, IsTopParent: true } - and not (AggregateFolder or UserRootFolder or UserView or Channel); - - private void OnLibraryItemAdded(object sender, ItemChangeEventArgs e) - => OnLibraryChange(e.Item, e.Parent, _itemsAdded, _foldersAddedTo); - - private void OnLibraryItemUpdated(object sender, ItemChangeEventArgs e) - => OnLibraryChange(e.Item, e.Parent, _itemsUpdated, null); - - private void OnLibraryItemRemoved(object sender, ItemChangeEventArgs e) - => OnLibraryChange(e.Item, e.Parent, _itemsRemoved, _foldersRemovedFrom); - - private void OnLibraryChange(BaseItem item, BaseItem parent, List<BaseItem> itemsList, List<Folder> foldersList) - { - if (!FilterItem(item)) + else { - return; + LibraryUpdateTimer.Change(updateDuration, Timeout.InfiniteTimeSpan); } - lock (_libraryChangedSyncLock) + if (foldersList is not null && parent is Folder folder) { - var updateDuration = TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration); - - if (LibraryUpdateTimer is null) - { - LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, updateDuration, Timeout.InfiniteTimeSpan); - } - else - { - LibraryUpdateTimer.Change(updateDuration, Timeout.InfiniteTimeSpan); - } - - if (foldersList is not null && parent is Folder folder) - { - foldersList.Add(folder); - } - - itemsList.Add(item); - } - } - - /// <summary> - /// Libraries the update timer callback. - /// </summary> - /// <param name="state">The state.</param> - private async void LibraryUpdateTimerCallback(object state) - { - List<Folder> foldersAddedTo; - List<Folder> foldersRemovedFrom; - List<BaseItem> itemsUpdated; - List<BaseItem> itemsAdded; - List<BaseItem> itemsRemoved; - lock (_libraryChangedSyncLock) - { - // Remove dupes in case some were saved multiple times - foldersAddedTo = _foldersAddedTo - .DistinctBy(x => x.Id) - .ToList(); - - foldersRemovedFrom = _foldersRemovedFrom - .DistinctBy(x => x.Id) - .ToList(); - - itemsUpdated = _itemsUpdated - .Where(i => !_itemsAdded.Contains(i)) - .DistinctBy(x => x.Id) - .ToList(); - - itemsAdded = _itemsAdded.ToList(); - itemsRemoved = _itemsRemoved.ToList(); - - if (LibraryUpdateTimer is not null) - { - LibraryUpdateTimer.Dispose(); - LibraryUpdateTimer = null; - } - - _itemsAdded.Clear(); - _itemsRemoved.Clear(); - _itemsUpdated.Clear(); - _foldersAddedTo.Clear(); - _foldersRemovedFrom.Clear(); + foldersList.Add(folder); } - await SendChangeNotifications(itemsAdded, itemsUpdated, itemsRemoved, foldersAddedTo, foldersRemovedFrom, CancellationToken.None).ConfigureAwait(false); + itemsList.Add(item); } + } - /// <summary> - /// Sends the change notifications. - /// </summary> - /// <param name="itemsAdded">The items added.</param> - /// <param name="itemsUpdated">The items updated.</param> - /// <param name="itemsRemoved">The items removed.</param> - /// <param name="foldersAddedTo">The folders added to.</param> - /// <param name="foldersRemovedFrom">The folders removed from.</param> - /// <param name="cancellationToken">The cancellation token.</param> - private async Task SendChangeNotifications( - List<BaseItem> itemsAdded, - List<BaseItem> itemsUpdated, - List<BaseItem> itemsRemoved, - List<Folder> foldersAddedTo, - List<Folder> foldersRemovedFrom, - CancellationToken cancellationToken) + /// <summary> + /// Libraries the update timer callback. + /// </summary> + /// <param name="state">The state.</param> + private async void LibraryUpdateTimerCallback(object state) + { + List<Folder> foldersAddedTo; + List<Folder> foldersRemovedFrom; + List<BaseItem> itemsUpdated; + List<BaseItem> itemsAdded; + List<BaseItem> itemsRemoved; + lock (_libraryChangedSyncLock) { - var userIds = _sessionManager.Sessions - .Select(i => i.UserId) - .Where(i => !i.Equals(default)) - .Distinct() - .ToArray(); - - foreach (var userId in userIds) - { - LibraryUpdateInfo info; - - try - { - info = GetLibraryUpdateInfo(itemsAdded, itemsUpdated, itemsRemoved, foldersAddedTo, foldersRemovedFrom, userId); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error in GetLibraryUpdateInfo"); - return; - } - - if (info.IsEmpty) - { - continue; - } - - try - { - await _sessionManager.SendMessageToUserSessions( - new List<Guid> { userId }, - SessionMessageType.LibraryChanged, - info, - cancellationToken) - .ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error sending LibraryChanged message"); - } - } - } - - /// <summary> - /// Gets the library update info. - /// </summary> - /// <param name="itemsAdded">The items added.</param> - /// <param name="itemsUpdated">The items updated.</param> - /// <param name="itemsRemoved">The items removed.</param> - /// <param name="foldersAddedTo">The folders added to.</param> - /// <param name="foldersRemovedFrom">The folders removed from.</param> - /// <param name="userId">The user id.</param> - /// <returns>LibraryUpdateInfo.</returns> - private LibraryUpdateInfo GetLibraryUpdateInfo( - List<BaseItem> itemsAdded, - List<BaseItem> itemsUpdated, - List<BaseItem> itemsRemoved, - List<Folder> foldersAddedTo, - List<Folder> foldersRemovedFrom, - Guid userId) - { - var user = _userManager.GetUserById(userId); - - var newAndRemoved = new List<BaseItem>(); - newAndRemoved.AddRange(foldersAddedTo); - newAndRemoved.AddRange(foldersRemovedFrom); - - var allUserRootChildren = _libraryManager.GetUserRootFolder() - .GetChildren(user, true) - .OfType<Folder>() + // Remove dupes in case some were saved multiple times + foldersAddedTo = _foldersAddedTo + .DistinctBy(x => x.Id) .ToList(); - return new LibraryUpdateInfo - { - ItemsAdded = itemsAdded.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)) - .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)) - .Distinct() - .ToArray(), - ItemsUpdated = itemsUpdated.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)) - .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)) - .Distinct() - .ToArray(), - ItemsRemoved = itemsRemoved.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user, true)) - .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)) - .Distinct() - .ToArray(), - FoldersAddedTo = foldersAddedTo.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)) - .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)) - .Distinct() - .ToArray(), - FoldersRemovedFrom = foldersRemovedFrom.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)) - .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)) - .Distinct() - .ToArray(), - CollectionFolders = GetTopParentIds(newAndRemoved, allUserRootChildren).ToArray() - }; - } + foldersRemovedFrom = _foldersRemovedFrom + .DistinctBy(x => x.Id) + .ToList(); - private static bool FilterItem(BaseItem item) - { - if (!item.IsFolder && !item.HasPathProtocol) - { - return false; - } + itemsUpdated = _itemsUpdated + .Where(i => !_itemsAdded.Contains(i)) + .DistinctBy(x => x.Id) + .ToList(); - if (item is IItemByName && item is not MusicArtist) - { - return false; - } - - return item.SourceType == SourceType.Library; - } - - private IEnumerable<string> GetTopParentIds(List<BaseItem> items, List<Folder> allUserRootChildren) - { - var list = new List<string>(); - - foreach (var item in items) - { - // If the physical root changed, return the user root - if (item is AggregateFolder) - { - continue; - } - - foreach (var folder in allUserRootChildren) - { - list.Add(folder.Id.ToString("N", CultureInfo.InvariantCulture)); - } - } - - return list.Distinct(StringComparer.Ordinal); - } - - /// <summary> - /// Translates the physical item to user library. - /// </summary> - /// <typeparam name="T">The type of item.</typeparam> - /// <param name="item">The item.</param> - /// <param name="user">The user.</param> - /// <param name="includeIfNotFound">if set to <c>true</c> [include if not found].</param> - /// <returns>IEnumerable{``0}.</returns> - private IEnumerable<T> TranslatePhysicalItemToUserLibrary<T>(T item, User user, bool includeIfNotFound = false) - where T : BaseItem - { - // If the physical root changed, return the user root - if (item is AggregateFolder) - { - return new[] { _libraryManager.GetUserRootFolder() as T }; - } - - // Return it only if it's in the user's library - if (includeIfNotFound || item.IsVisibleStandalone(user)) - { - return new[] { item }; - } - - return Array.Empty<T>(); - } - - /// <inheritdoc /> - public void Dispose() - { - _libraryManager.ItemAdded -= OnLibraryItemAdded; - _libraryManager.ItemUpdated -= OnLibraryItemUpdated; - _libraryManager.ItemRemoved -= OnLibraryItemRemoved; - - _providerManager.RefreshCompleted -= OnProviderRefreshCompleted; - _providerManager.RefreshStarted -= OnProviderRefreshStarted; - _providerManager.RefreshProgress -= OnProviderRefreshProgress; + itemsAdded = _itemsAdded.ToList(); + itemsRemoved = _itemsRemoved.ToList(); if (LibraryUpdateTimer is not null) { LibraryUpdateTimer.Dispose(); LibraryUpdateTimer = null; } + + _itemsAdded.Clear(); + _itemsRemoved.Clear(); + _itemsUpdated.Clear(); + _foldersAddedTo.Clear(); + _foldersRemovedFrom.Clear(); + } + + await SendChangeNotifications(itemsAdded, itemsUpdated, itemsRemoved, foldersAddedTo, foldersRemovedFrom, CancellationToken.None).ConfigureAwait(false); + } + + /// <summary> + /// Sends the change notifications. + /// </summary> + /// <param name="itemsAdded">The items added.</param> + /// <param name="itemsUpdated">The items updated.</param> + /// <param name="itemsRemoved">The items removed.</param> + /// <param name="foldersAddedTo">The folders added to.</param> + /// <param name="foldersRemovedFrom">The folders removed from.</param> + /// <param name="cancellationToken">The cancellation token.</param> + private async Task SendChangeNotifications( + List<BaseItem> itemsAdded, + List<BaseItem> itemsUpdated, + List<BaseItem> itemsRemoved, + List<Folder> foldersAddedTo, + List<Folder> foldersRemovedFrom, + CancellationToken cancellationToken) + { + var userIds = _sessionManager.Sessions + .Select(i => i.UserId) + .Where(i => !i.Equals(default)) + .Distinct() + .ToArray(); + + foreach (var userId in userIds) + { + LibraryUpdateInfo info; + + try + { + info = GetLibraryUpdateInfo(itemsAdded, itemsUpdated, itemsRemoved, foldersAddedTo, foldersRemovedFrom, userId); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error in GetLibraryUpdateInfo"); + return; + } + + if (info.IsEmpty) + { + continue; + } + + try + { + await _sessionManager.SendMessageToUserSessions( + new List<Guid> { userId }, + SessionMessageType.LibraryChanged, + info, + cancellationToken) + .ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error sending LibraryChanged message"); + } + } + } + + /// <summary> + /// Gets the library update info. + /// </summary> + /// <param name="itemsAdded">The items added.</param> + /// <param name="itemsUpdated">The items updated.</param> + /// <param name="itemsRemoved">The items removed.</param> + /// <param name="foldersAddedTo">The folders added to.</param> + /// <param name="foldersRemovedFrom">The folders removed from.</param> + /// <param name="userId">The user id.</param> + /// <returns>LibraryUpdateInfo.</returns> + private LibraryUpdateInfo GetLibraryUpdateInfo( + List<BaseItem> itemsAdded, + List<BaseItem> itemsUpdated, + List<BaseItem> itemsRemoved, + List<Folder> foldersAddedTo, + List<Folder> foldersRemovedFrom, + Guid userId) + { + var user = _userManager.GetUserById(userId); + + var newAndRemoved = new List<BaseItem>(); + newAndRemoved.AddRange(foldersAddedTo); + newAndRemoved.AddRange(foldersRemovedFrom); + + var allUserRootChildren = _libraryManager.GetUserRootFolder() + .GetChildren(user, true) + .OfType<Folder>() + .ToList(); + + return new LibraryUpdateInfo + { + ItemsAdded = itemsAdded.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)) + .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)) + .Distinct() + .ToArray(), + ItemsUpdated = itemsUpdated.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)) + .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)) + .Distinct() + .ToArray(), + ItemsRemoved = itemsRemoved.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user, true)) + .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)) + .Distinct() + .ToArray(), + FoldersAddedTo = foldersAddedTo.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)) + .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)) + .Distinct() + .ToArray(), + FoldersRemovedFrom = foldersRemovedFrom.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)) + .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)) + .Distinct() + .ToArray(), + CollectionFolders = GetTopParentIds(newAndRemoved, allUserRootChildren).ToArray() + }; + } + + private static bool FilterItem(BaseItem item) + { + if (!item.IsFolder && !item.HasPathProtocol) + { + return false; + } + + if (item is IItemByName && item is not MusicArtist) + { + return false; + } + + return item.SourceType == SourceType.Library; + } + + private IEnumerable<string> GetTopParentIds(List<BaseItem> items, List<Folder> allUserRootChildren) + { + var list = new List<string>(); + + foreach (var item in items) + { + // If the physical root changed, return the user root + if (item is AggregateFolder) + { + continue; + } + + foreach (var folder in allUserRootChildren) + { + list.Add(folder.Id.ToString("N", CultureInfo.InvariantCulture)); + } + } + + return list.Distinct(StringComparer.Ordinal); + } + + /// <summary> + /// Translates the physical item to user library. + /// </summary> + /// <typeparam name="T">The type of item.</typeparam> + /// <param name="item">The item.</param> + /// <param name="user">The user.</param> + /// <param name="includeIfNotFound">if set to <c>true</c> [include if not found].</param> + /// <returns>IEnumerable{``0}.</returns> + private IEnumerable<T> TranslatePhysicalItemToUserLibrary<T>(T item, User user, bool includeIfNotFound = false) + where T : BaseItem + { + // If the physical root changed, return the user root + if (item is AggregateFolder) + { + return new[] { _libraryManager.GetUserRootFolder() as T }; + } + + // Return it only if it's in the user's library + if (includeIfNotFound || item.IsVisibleStandalone(user)) + { + return new[] { item }; + } + + return Array.Empty<T>(); + } + + /// <inheritdoc /> + public void Dispose() + { + _libraryManager.ItemAdded -= OnLibraryItemAdded; + _libraryManager.ItemUpdated -= OnLibraryItemUpdated; + _libraryManager.ItemRemoved -= OnLibraryItemRemoved; + + _providerManager.RefreshCompleted -= OnProviderRefreshCompleted; + _providerManager.RefreshStarted -= OnProviderRefreshStarted; + _providerManager.RefreshProgress -= OnProviderRefreshProgress; + + if (LibraryUpdateTimer is not null) + { + LibraryUpdateTimer.Dispose(); + LibraryUpdateTimer = null; } } } From 0ea9f713f48e433f66a95bad7bf765cdd2589fa3 Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Wed, 15 Nov 2023 20:07:07 -0500 Subject: [PATCH 845/858] Document LibraryChangedNotifier --- .../EntryPoints/LibraryChangedNotifier.cs | 50 +++++-------------- 1 file changed, 13 insertions(+), 37 deletions(-) diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs index 40dc00e1af..8e0f37d895 100644 --- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs @@ -1,7 +1,5 @@ #nullable disable -#pragma warning disable CS1591 - using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -25,6 +23,9 @@ using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.EntryPoints; +/// <summary> +/// A <see cref="IServerEntryPoint"/> that notifies users when libraries are updated. +/// </summary> public sealed class LibraryChangedNotifier : IServerEntryPoint { private readonly ILibraryManager _libraryManager; @@ -42,6 +43,15 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint private readonly List<BaseItem> _itemsUpdated = new(); private readonly ConcurrentDictionary<Guid, DateTime> _lastProgressMessageTimes = new(); + /// <summary> + /// Initializes a new instance of the <see cref="LibraryChangedNotifier"/> class. + /// </summary> + /// <param name="libraryManager">The <see cref="ILibraryManager"/>.</param> + /// <param name="configurationManager">The <see cref="IServerConfigurationManager"/>.</param> + /// <param name="sessionManager">The <see cref="ISessionManager"/>.</param> + /// <param name="userManager">The <see cref="IUserManager"/>.</param> + /// <param name="logger">The <see cref="ILogger"/>.</param> + /// <param name="providerManager">The <see cref="IProviderManager"/>.</param> public LibraryChangedNotifier( ILibraryManager libraryManager, IServerConfigurationManager configurationManager, @@ -58,12 +68,9 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint _providerManager = providerManager; } - /// <summary> - /// Gets or sets the library update timer. - /// </summary> - /// <value>The library update timer.</value> private Timer LibraryUpdateTimer { get; set; } + /// <inheritdoc /> public Task RunAsync() { _libraryManager.ItemAdded += OnLibraryItemAdded; @@ -184,10 +191,6 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint } } - /// <summary> - /// Libraries the update timer callback. - /// </summary> - /// <param name="state">The state.</param> private async void LibraryUpdateTimerCallback(object state) { List<Folder> foldersAddedTo; @@ -230,15 +233,6 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint await SendChangeNotifications(itemsAdded, itemsUpdated, itemsRemoved, foldersAddedTo, foldersRemovedFrom, CancellationToken.None).ConfigureAwait(false); } - /// <summary> - /// Sends the change notifications. - /// </summary> - /// <param name="itemsAdded">The items added.</param> - /// <param name="itemsUpdated">The items updated.</param> - /// <param name="itemsRemoved">The items removed.</param> - /// <param name="foldersAddedTo">The folders added to.</param> - /// <param name="foldersRemovedFrom">The folders removed from.</param> - /// <param name="cancellationToken">The cancellation token.</param> private async Task SendChangeNotifications( List<BaseItem> itemsAdded, List<BaseItem> itemsUpdated, @@ -288,16 +282,6 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint } } - /// <summary> - /// Gets the library update info. - /// </summary> - /// <param name="itemsAdded">The items added.</param> - /// <param name="itemsUpdated">The items updated.</param> - /// <param name="itemsRemoved">The items removed.</param> - /// <param name="foldersAddedTo">The folders added to.</param> - /// <param name="foldersRemovedFrom">The folders removed from.</param> - /// <param name="userId">The user id.</param> - /// <returns>LibraryUpdateInfo.</returns> private LibraryUpdateInfo GetLibraryUpdateInfo( List<BaseItem> itemsAdded, List<BaseItem> itemsUpdated, @@ -379,14 +363,6 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint return list.Distinct(StringComparer.Ordinal); } - /// <summary> - /// Translates the physical item to user library. - /// </summary> - /// <typeparam name="T">The type of item.</typeparam> - /// <param name="item">The item.</param> - /// <param name="user">The user.</param> - /// <param name="includeIfNotFound">if set to <c>true</c> [include if not found].</param> - /// <returns>IEnumerable{``0}.</returns> private IEnumerable<T> TranslatePhysicalItemToUserLibrary<T>(T item, User user, bool includeIfNotFound = false) where T : BaseItem { From 4e61c2b4ec7ff1a25ac7b8e71bcf6a2833f78e75 Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Wed, 15 Nov 2023 20:31:07 -0500 Subject: [PATCH 846/858] Enable nullable in LibraryChangedNotifier --- .../EntryPoints/LibraryChangedNotifier.cs | 43 +++++++++---------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs index 8e0f37d895..a83d7a4105 100644 --- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -43,6 +41,8 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint private readonly List<BaseItem> _itemsUpdated = new(); private readonly ConcurrentDictionary<Guid, DateTime> _lastProgressMessageTimes = new(); + private Timer? _libraryUpdateTimer; + /// <summary> /// Initializes a new instance of the <see cref="LibraryChangedNotifier"/> class. /// </summary> @@ -68,8 +68,6 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint _providerManager = providerManager; } - private Timer LibraryUpdateTimer { get; set; } - /// <inheritdoc /> public Task RunAsync() { @@ -84,7 +82,7 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint return Task.CompletedTask; } - private void OnProviderRefreshProgress(object sender, GenericEventArgs<Tuple<BaseItem, double>> e) + private void OnProviderRefreshProgress(object? sender, GenericEventArgs<Tuple<BaseItem, double>> e) { var item = e.Argument.Item1; @@ -137,12 +135,12 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint } } - private void OnProviderRefreshStarted(object sender, GenericEventArgs<BaseItem> e) + private void OnProviderRefreshStarted(object? sender, GenericEventArgs<BaseItem> e) { OnProviderRefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 0))); } - private void OnProviderRefreshCompleted(object sender, GenericEventArgs<BaseItem> e) + private void OnProviderRefreshCompleted(object? sender, GenericEventArgs<BaseItem> e) { OnProviderRefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 100))); @@ -153,16 +151,16 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint => item is Folder { IsRoot: false, IsTopParent: true } and not (AggregateFolder or UserRootFolder or UserView or Channel); - private void OnLibraryItemAdded(object sender, ItemChangeEventArgs e) + private void OnLibraryItemAdded(object? sender, ItemChangeEventArgs e) => OnLibraryChange(e.Item, e.Parent, _itemsAdded, _foldersAddedTo); - private void OnLibraryItemUpdated(object sender, ItemChangeEventArgs e) + private void OnLibraryItemUpdated(object? sender, ItemChangeEventArgs e) => OnLibraryChange(e.Item, e.Parent, _itemsUpdated, null); - private void OnLibraryItemRemoved(object sender, ItemChangeEventArgs e) + private void OnLibraryItemRemoved(object? sender, ItemChangeEventArgs e) => OnLibraryChange(e.Item, e.Parent, _itemsRemoved, _foldersRemovedFrom); - private void OnLibraryChange(BaseItem item, BaseItem parent, List<BaseItem> itemsList, List<Folder> foldersList) + private void OnLibraryChange(BaseItem item, BaseItem parent, List<BaseItem> itemsList, List<Folder>? foldersList) { if (!FilterItem(item)) { @@ -173,13 +171,13 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint { var updateDuration = TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration); - if (LibraryUpdateTimer is null) + if (_libraryUpdateTimer is null) { - LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, updateDuration, Timeout.InfiniteTimeSpan); + _libraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, updateDuration, Timeout.InfiniteTimeSpan); } else { - LibraryUpdateTimer.Change(updateDuration, Timeout.InfiniteTimeSpan); + _libraryUpdateTimer.Change(updateDuration, Timeout.InfiniteTimeSpan); } if (foldersList is not null && parent is Folder folder) @@ -191,7 +189,7 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint } } - private async void LibraryUpdateTimerCallback(object state) + private async void LibraryUpdateTimerCallback(object? state) { List<Folder> foldersAddedTo; List<Folder> foldersRemovedFrom; @@ -217,10 +215,10 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint itemsAdded = _itemsAdded.ToList(); itemsRemoved = _itemsRemoved.ToList(); - if (LibraryUpdateTimer is not null) + if (_libraryUpdateTimer is not null) { - LibraryUpdateTimer.Dispose(); - LibraryUpdateTimer = null; + _libraryUpdateTimer.Dispose(); + _libraryUpdateTimer = null; } _itemsAdded.Clear(); @@ -291,6 +289,7 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint Guid userId) { var user = _userManager.GetUserById(userId); + ArgumentNullException.ThrowIfNull(user); var newAndRemoved = new List<BaseItem>(); newAndRemoved.AddRange(foldersAddedTo); @@ -369,7 +368,7 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint // If the physical root changed, return the user root if (item is AggregateFolder) { - return new[] { _libraryManager.GetUserRootFolder() as T }; + return _libraryManager.GetUserRootFolder() is T t ? new[] { t } : Array.Empty<T>(); } // Return it only if it's in the user's library @@ -392,10 +391,10 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint _providerManager.RefreshStarted -= OnProviderRefreshStarted; _providerManager.RefreshProgress -= OnProviderRefreshProgress; - if (LibraryUpdateTimer is not null) + if (_libraryUpdateTimer is not null) { - LibraryUpdateTimer.Dispose(); - LibraryUpdateTimer = null; + _libraryUpdateTimer.Dispose(); + _libraryUpdateTimer = null; } } } From 9fdac2b01f53c932fe8a7229803d580133a807c2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 16 Nov 2023 14:06:29 +0000 Subject: [PATCH 847/858] chore(deps): update github/codeql-action action to v2.22.7 --- .github/workflows/ci-codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index 7e30591533..143b11d6f3 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '8.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@689fdc5193eeb735ecb2e52e819e3382876f93f4 # v2.22.6 + uses: github/codeql-action/init@66b90a5db151a8042fa97405c6cf843bbe433f7b # v2.22.7 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@689fdc5193eeb735ecb2e52e819e3382876f93f4 # v2.22.6 + uses: github/codeql-action/autobuild@66b90a5db151a8042fa97405c6cf843bbe433f7b # v2.22.7 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@689fdc5193eeb735ecb2e52e819e3382876f93f4 # v2.22.6 + uses: github/codeql-action/analyze@66b90a5db151a8042fa97405c6cf843bbe433f7b # v2.22.7 From 380c367e4985899f24277282a74f01bf5b922d87 Mon Sep 17 00:00:00 2001 From: Lucas Fogolin <lucasfogolin@gmail.com> Date: Fri, 17 Nov 2023 20:08:52 +0000 Subject: [PATCH 848/858] Translated using Weblate (Portuguese (Brazil)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt_BR/ --- Emby.Server.Implementations/Localization/Core/pt-BR.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json index b9b93b7b6f..2c8c46050e 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-BR.json +++ b/Emby.Server.Implementations/Localization/Core/pt-BR.json @@ -124,5 +124,7 @@ "TaskKeyframeExtractor": "Extrator de quadro-chave", "TaskKeyframeExtractorDescription": "Extrai quadros-chave de arquivos de vídeo para criar listas de reprodução HLS mais precisas. Esta tarefa pode ser executada por um longo tempo.", "External": "Externo", - "HearingImpaired": "Deficiência Auditiva" + "HearingImpaired": "Deficiência Auditiva", + "TaskRefreshTrickplayImages": "Gerar imagens Trickplay", + "TaskRefreshTrickplayImagesDescription": "Cria prévias Trickplay para vídeos em bibliotecas em que o recurso está habilitado." } From 2f87158e682fc4f7257053d4792bf390c0fe4a0f Mon Sep 17 00:00:00 2001 From: Atharva Vaidya <atharva16vaidya@gmail.com> Date: Mon, 20 Nov 2023 17:16:30 +0000 Subject: [PATCH 849/858] Translated using Weblate (Marathi) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/mr/ --- Emby.Server.Implementations/Localization/Core/mr.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/mr.json b/Emby.Server.Implementations/Localization/Core/mr.json index a8fb26b91a..13c58e0ab3 100644 --- a/Emby.Server.Implementations/Localization/Core/mr.json +++ b/Emby.Server.Implementations/Localization/Core/mr.json @@ -123,5 +123,7 @@ "DeviceOnlineWithName": "{0} कनेक्ट झाले", "DeviceOfflineWithName": "{0} डिस्कनेक्ट झाला आहे", "AuthenticationSucceededWithUserName": "{0} यशस्वीरित्या प्रमाणीकृत", - "HearingImpaired": "कर्णबधीर" + "HearingImpaired": "कर्णबधीर", + "TaskRefreshTrickplayImages": "ट्रिकप्ले प्रतिमा तयार करा", + "TaskRefreshTrickplayImagesDescription": "सक्षम लायब्ररीमधील व्हिडिओंसाठी ट्रिकप्ले पूर्वावलोकन तयार करते." } From 6b940e141e014f5d67caf68dd8dec04bc1d723dc Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Wed, 22 Nov 2023 09:34:14 -0500 Subject: [PATCH 850/858] Remove unnecessary AsQueryable() --- Jellyfin.Server.Implementations/Activity/ActivityManager.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs index ce1c54cbb2..a2ea6b2251 100644 --- a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs +++ b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs @@ -68,7 +68,6 @@ namespace Jellyfin.Server.Implementations.Activity Date = entity.DateCreated, Severity = entity.LogSeverity }) - .AsQueryable() .ToListAsync() .ConfigureAwait(false)); } From c9c133bc43231609a237291ae40ecb57bfc2576d Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Wed, 22 Nov 2023 09:35:35 -0500 Subject: [PATCH 851/858] Use ExecuteDelete for cleaning activity logs --- .../Activity/ActivityManager.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs index a2ea6b2251..54272aeafa 100644 --- a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs +++ b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs @@ -79,11 +79,10 @@ namespace Jellyfin.Server.Implementations.Activity var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false); await using (dbContext.ConfigureAwait(false)) { - var entries = dbContext.ActivityLogs - .Where(entry => entry.DateCreated <= startDate); - - dbContext.RemoveRange(entries); - await dbContext.SaveChangesAsync().ConfigureAwait(false); + await dbContext.ActivityLogs + .Where(entry => entry.DateCreated <= startDate) + .ExecuteDeleteAsync() + .ConfigureAwait(false); } } From ad58d1f77c2039a62b225545fb795d3395046303 Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Wed, 22 Nov 2023 09:40:49 -0500 Subject: [PATCH 852/858] Use ExecuteDelete for removing API keys --- .../Security/AuthenticationManager.cs | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs b/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs index b2dfe60a14..07ac27e3c2 100644 --- a/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs +++ b/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs @@ -58,19 +58,10 @@ namespace Jellyfin.Server.Implementations.Security var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); await using (dbContext.ConfigureAwait(false)) { - var key = await dbContext.ApiKeys + await dbContext.ApiKeys .Where(apiKey => apiKey.AccessToken == accessToken) - .FirstOrDefaultAsync() + .ExecuteDeleteAsync() .ConfigureAwait(false); - - if (key is null) - { - return; - } - - dbContext.Remove(key); - - await dbContext.SaveChangesAsync().ConfigureAwait(false); } } } From fd1dc860c9981055634595e2064d13b21227927b Mon Sep 17 00:00:00 2001 From: Ahmad Mujahid <ahmad.h.mujahid@gmail.com> Date: Wed, 22 Nov 2023 09:55:04 +0000 Subject: [PATCH 853/858] Translated using Weblate (Arabic) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ar/ --- Emby.Server.Implementations/Localization/Core/ar.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json index 93d50e6e3b..ecdc01a3dc 100644 --- a/Emby.Server.Implementations/Localization/Core/ar.json +++ b/Emby.Server.Implementations/Localization/Core/ar.json @@ -124,5 +124,6 @@ "TaskKeyframeExtractorDescription": "يستخرج الإطارات الرئيسية من ملفات الفيديو لكي ينشئ قوائم تشغيل بث HTTP المباشر. قد تستمر هذه العملية لوقت طويل.", "TaskKeyframeExtractor": "مستخرج الإطار الرئيسي", "External": "خارجي", - "HearingImpaired": "ضعاف السمع" + "HearingImpaired": "ضعاف السمع", + "TaskRefreshTrickplayImages": "توليد صور Trickplay" } From 87adf7b600f9f58b4e34636c3a39d16f9220c101 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 23 Nov 2023 12:40:07 +0000 Subject: [PATCH 854/858] chore(deps): update github/codeql-action action to v2.22.8 --- .github/workflows/ci-codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index 143b11d6f3..2a60d18054 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '8.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@66b90a5db151a8042fa97405c6cf843bbe433f7b # v2.22.7 + uses: github/codeql-action/init@407ffafae6a767df3e0230c3df91b6443ae8df75 # v2.22.8 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@66b90a5db151a8042fa97405c6cf843bbe433f7b # v2.22.7 + uses: github/codeql-action/autobuild@407ffafae6a767df3e0230c3df91b6443ae8df75 # v2.22.8 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@66b90a5db151a8042fa97405c6cf843bbe433f7b # v2.22.7 + uses: github/codeql-action/analyze@407ffafae6a767df3e0230c3df91b6443ae8df75 # v2.22.8 From 25251efa193c1b2aaa73f08498605162590728f9 Mon Sep 17 00:00:00 2001 From: queeup <queeup@zoho.com> Date: Sun, 26 Nov 2023 11:02:31 +0000 Subject: [PATCH 855/858] Translated using Weblate (Turkish) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/tr/ --- Emby.Server.Implementations/Localization/Core/tr.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json index 3ce928859a..b28bfaf244 100644 --- a/Emby.Server.Implementations/Localization/Core/tr.json +++ b/Emby.Server.Implementations/Localization/Core/tr.json @@ -124,5 +124,7 @@ "TaskKeyframeExtractorDescription": "Daha hassas HLS çalma listeleri oluşturmak için video dosyalarından kareleri çıkarır. Bu görev uzun bir süre çalışabilir.", "TaskKeyframeExtractor": "Kare Ayırt Edici", "External": "Harici", - "HearingImpaired": "Duyma engelli" + "HearingImpaired": "Duyma engelli", + "TaskRefreshTrickplayImages": "Trickplay Görselleri Oluştur", + "TaskRefreshTrickplayImagesDescription": "Etkin kütüphanelerdeki videolar için trickplay önizlemeleri oluşturur." } From ff682953abfe9843aec3c10970db6a6e154edaca Mon Sep 17 00:00:00 2001 From: leap123 <leapofazzam@protonmail.com> Date: Sun, 26 Nov 2023 11:03:28 +0000 Subject: [PATCH 856/858] Translated using Weblate (Indonesian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/id/ --- Emby.Server.Implementations/Localization/Core/id.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/id.json b/Emby.Server.Implementations/Localization/Core/id.json index 87ce07da31..78a4433480 100644 --- a/Emby.Server.Implementations/Localization/Core/id.json +++ b/Emby.Server.Implementations/Localization/Core/id.json @@ -13,7 +13,7 @@ "HomeVideos": "Video Rumahan", "HeaderRecordingGroups": "Grup Rekaman", "HeaderNextUp": "Selanjutnya", - "HeaderLiveTV": "TV Live", + "HeaderLiveTV": "Siaran langsung", "HeaderFavoriteSongs": "Lagu Favorit", "HeaderFavoriteShows": "Tayangan Favorit", "HeaderFavoriteEpisodes": "Episode Favorit", @@ -123,5 +123,7 @@ "TaskKeyframeExtractorDescription": "Ekstrak bingkai utama dari file video untuk membuat daftar putar HLS yang lebih tepat. Tugas ini dapat berjalan untuk waktu yang lama.", "TaskKeyframeExtractor": "Ekstraktor Bingkai Utama", "External": "Luar", - "HearingImpaired": "Gangguan Pendengaran" + "HearingImpaired": "Gangguan Pendengaran", + "TaskRefreshTrickplayImages": "Hasilkan Gambar Trickplay", + "TaskRefreshTrickplayImagesDescription": "Buat pratinjau trickplay untuk video di perpustakaan yang diaktifkan." } From 099632f37e559c1aad4944fb328600a242679988 Mon Sep 17 00:00:00 2001 From: Tushar Joshi <tj.joshi@gmail.com> Date: Sun, 26 Nov 2023 08:03:17 +0000 Subject: [PATCH 857/858] Translated using Weblate (Hindi) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hi/ --- .../Localization/Core/hi.json | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/hi.json b/Emby.Server.Implementations/Localization/Core/hi.json index 47d3eeac5d..3f4dea5237 100644 --- a/Emby.Server.Implementations/Localization/Core/hi.json +++ b/Emby.Server.Implementations/Localization/Core/hi.json @@ -12,17 +12,17 @@ "HeaderAlbumArtists": "एल्बम कलाकार", "Genres": "शैली", "Forced": "बलपूर्वक", - "Folders": "फोल्डेरें", + "Folders": "फ़ोल्डरें", "Favorites": "पसंदीदा", - "FailedLoginAttemptWithUserName": "लॉगिन असफल हुआ, पुनः {0} से प्रयास करें", + "FailedLoginAttemptWithUserName": "{0} से लॉगिन असफल हुआ", "DeviceOnlineWithName": "{0} से संयोग हो गया है", "DeviceOfflineWithName": "{0} से संयोग विच्छिन्न हो गया है", "Default": "प्राथमिक", - "Collections": "संग्रह", + "Collections": "संग्रहों", "ChapterNameValue": "अध्याय", "Channels": "चैनल", - "CameraImageUploadedFrom": "कैमरा से एक नया चित्र अपलोड किया गया है", - "Books": "किताब", + "CameraImageUploadedFrom": "{0} से एक नया कैमरावाला चित्र अपलोड किया गया है", + "Books": "पुस्तकों", "AuthenticationSucceededWithUserName": "सफलता से प्रमाणीकृत", "Artists": "कलाकारों", "Application": "एप्लिकेशन", @@ -123,5 +123,7 @@ "TaskRefreshPeopleDescription": "आपकी मीडिया लाइब्रेरी में अभिनेताओं और निर्देशकों के लिए मेटाडेटा अपडेट करता है।", "TaskCleanCache": "स्वच्छ कैश निर्देशिका", "TaskDownloadMissingSubtitlesDescription": "मेटाडेटा कॉन्फ़िगरेशन के आधार पर लापता उपशीर्षक के लिए इंटरनेट खोजता है।", - "TaskKeyframeExtractorDescription": "अधिक सटीक एचएलएस प्लेलिस्ट बनाने के लिए वीडियो फ़ाइलों से मुख्य-फ़्रेम निकालता है। यह कार्य लंबे समय तक चल सकता है।" + "TaskKeyframeExtractorDescription": "अधिक सटीक एचएलएस प्लेलिस्ट बनाने के लिए वीडियो फ़ाइलों से मुख्य-फ़्रेम निकालता है। यह कार्य लंबे समय तक चल सकता है।", + "TaskRefreshTrickplayImages": "ट्रिकप्लै चित्रों को सृजन करे", + "TaskRefreshTrickplayImagesDescription": "नियत संग्रहों में चलचित्रों का ट्रीकप्लै दर्शनों को सृजन करे." } From 435c1431401b3b765ea46e45f29bda6eb879a998 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=87a=C4=9Fr=C4=B1=20Sakao=C4=9Flu?= <cagrisakaoglu@gmail.com> Date: Sun, 26 Nov 2023 00:25:52 +0000 Subject: [PATCH 858/858] Fix:Plugin Installed Alerts missing from Admin Dashboard #10620 --- CONTRIBUTORS.md | 3 ++- .../Updates/InstallationManager.cs | 12 +++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 571da95bbf..d208879d17 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -173,6 +173,7 @@ - [scampower3](https://github.com/scampower3) - [Chris-Codes-It] (https://github.com/Chris-Codes-It) - [Pithaya](https://github.com/Pithaya) + - [Çağrı Sakaoğlu](https://github.com/ilovepilav) # Emby Contributors @@ -243,4 +244,4 @@ - [Jakob Kukla](https://github.com/jakobkukla) - [Utku Özdemir](https://github.com/utkuozdemir) - [JPUC1143](https://github.com/Jpuc1143/) - - [0x25CBFC4F](https://github.com/0x25CBFC4F) \ No newline at end of file + - [0x25CBFC4F](https://github.com/0x25CBFC4F) diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index b31b4116db..15c4cfdf02 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -321,9 +321,15 @@ namespace Emby.Server.Implementations.Updates } _completedInstallationsInternal.Add(package); - await _eventManager.PublishAsync(isUpdate - ? (GenericEventArgs<InstallationInfo>)new PluginUpdatedEventArgs(package) - : new PluginInstalledEventArgs(package)).ConfigureAwait(false); + + if (isUpdate) + { + await _eventManager.PublishAsync(new PluginUpdatedEventArgs(package)).ConfigureAwait(false); + } + else + { + await _eventManager.PublishAsync(new PluginInstalledEventArgs(package)).ConfigureAwait(false); + } _applicationHost.NotifyPendingRestart(); }